├── .gitignore ├── .vscode ├── arduino.json └── extensions.json ├── BUILD.md ├── CHANGELOG.md ├── LICENSE.txt ├── MANUAL.md ├── README.md ├── UPLOADING.md ├── include └── README ├── lib └── README ├── photos ├── din.png ├── galczo5_1.jpg ├── galczo5_2.jpg ├── map.png ├── upload_1.png ├── upload_2.png ├── upload_3.png ├── upload_4.png ├── upload_5.png └── wiring.png ├── platformio.ini ├── src ├── config │ ├── command-type.h │ ├── controller-button-entity.h │ ├── midi-controller-config.cpp │ └── midi-controller-config.h ├── configuration │ ├── configuration-state-machine.cpp │ ├── configuration-state-machine.h │ ├── configuration-state.h │ ├── configurator.cpp │ └── configurator.h ├── consts.h ├── controller │ ├── controller-state-machine.cpp │ ├── controller-state-machine.h │ └── controller-state.h ├── executor │ ├── command-executor.cpp │ └── command-executor.h ├── footswitch │ ├── footswitch-state.h │ ├── footswitch.h │ └── footswith.cpp ├── open-midi-controller.ino └── printer │ ├── printer.cpp │ └── printer.h └── test └── README /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .vscode/.browse.c_cpp.db* 3 | .vscode/c_cpp_properties.json 4 | .vscode/launch.json 5 | .vscode/ipch 6 | -------------------------------------------------------------------------------- /.vscode/arduino.json: -------------------------------------------------------------------------------- 1 | { 2 | "board": "arduino:avr:nano", 3 | "configuration": "cpu=atmega328", 4 | "programmer": "AVRISP mkII", 5 | "port": "/dev/ttyUSB0", 6 | "sketch": "open-midi-controller.ino" 7 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "platformio.platformio-ide" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /BUILD.md: -------------------------------------------------------------------------------- 1 | ## Load firmware 2 | 3 | This repo contains PlatformIO project with firmware for your controller. Please follow [official guide](https://docs.platformio.org/en/latest/core/quickstart.html) and upload code into your arduino nano board. 4 | 5 | See [UPLOADING](./UPLOADING.md). 6 | 7 | ## Wiring 8 | 9 | ![wiring](./photos/wiring.png) 10 | 11 | ## DIN 5 socket 12 | 13 | ![din](./photos/din.png) 14 | 15 | ## LCD 16 | 17 | Connect lcd with provided instruction. I2C should have 4 pins. Connect GND to GND pin, VCC to 5V pin. 18 | 19 | If you are using the arduino nano board, SDA should be connected to A4 pin, SCL to A5 pin. 20 | 21 | ## Power supply 22 | 23 | Power whole board using usb or connect selected power source (for me the best choice is 9V battery) to GND and VCC pins of arduino board. 24 | 25 | ## Is it everything? 26 | 27 | Yup. Easy, right? 28 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## REV. 20210725 4 | 5 | Fixed USB MIDI mode. Check [MANUAL](./MANUAL.md). 6 | 7 | ## REV. 20210724 8 | 9 | Initial version. 10 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Francois Best 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANUAL.md: -------------------------------------------------------------------------------- 1 | # Manual 2 | 3 | This document contains how to use and configure the controller. 4 | 5 | ## Footswitch map 6 | 7 | ![map](./photos/map.png) 8 | 9 | ## Enter configuration mode 10 | 11 | Press both FS2 and FS4 footswitches. LCD should display information that you are in the configuration mode. 12 | 13 | ## Configuration 14 | 15 | First of all, you have to select the footswitch that you want to configure. Press the footswitch you want to configure. If you want to configure the long press, you should press it for more than 1 second. If you want to configure the double click, you should press it twice in short period of time. 16 | 17 | Next, you have to set MIDI channel. 18 | 19 | **Use FS1 and FS2 to change value** 20 | 21 | **Use FS4 to submit selected value** 22 | 23 | After that, you have to configure command type that will be assigned to the footswitch. 24 | 25 | Available commands: 26 | * Note - send MIDI note 27 | * CC - send MIDI CC 28 | * Toggle CC - Toggle two values of one CC 29 | * Next page - go to next configuration page 30 | * Prev page - go to the previous configuration page 31 | * Go to page - go to the selected page 32 | * Temporary page - go to page and go back after next command 33 | 34 | Pass the rest of the configuration using FS1, FS2 and FS4. 35 | 36 | ## Exit configuration mode 37 | 38 | Press both FS2 and FS4 footswitches. LCD should display information that you are not in the configuration mode. 39 | 40 | ## Display current configuration 41 | 42 | Press both FS1 and FS3 footswitches. 43 | 44 | ## USB MIDI mode 45 | To enter USB MIDI mode, push both FS4 and FS6. The device should restart in USB MIDI mode. To go back to MIDI mode, push both FS4 and FS6. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Open Midi Controller 2 | Open source firmware for 6 button midi controller based on arduino. It's fully configurable and doesn't need a computer connection. Build your own midi controller and save a lot of money. 3 | 4 | # Features 5 | 6 | Most important features: 7 | 8 | * 6 buttons, each can be assigned to three actions. The first action is triggered on footswitch click. The second one is triggered when the switch is pressed for more than 1 second. The last one is triggered on double click. 9 | * LCD status screen 10 | * Configurable without a computer connection 11 | * Supported midi commands: Note, CC - send one value, CC - toggle two values 12 | * Five "pages" of configuration. You can assign an action to footswitch and toggle between five different configurations 13 | * Working with USB MIDI using the serial port. You'll need to use additional software like [Hairless Midi](https://projectgus.github.io/hairless-midiserial/), [ttymidi](https://github.com/cjbarnes18/ttymidi) or [SerialMidiBridge](https://github.com/RuudMulder/SerialMidiBridge) to control your DAW or plugins. Just set port and baudrate of 115200. 14 | 15 | It's easy to calculate that with 6 footswitches, 3 actions for each footswitch and 3 possible configuration pages allow you to configure 54 different commands! That's a lot. 16 | 17 | # Build cost 18 | Cost of parts if you decide to order them in the popular Chinese store. 19 | 20 | | Part | Cost | 21 | | ---- | ---- | 22 | | Arduino Nano | 2 - 3 USD | 23 | | Arduino Nano Shield (if you don't want to solder) | 1 USD | 24 | | LCD with I2C module | 2 - 2.5 USD | 25 | | Momentary Footswitches | 6 x 1 - 1.5 USD | 26 | | DIN 5 and other stuff | 1 USD | 27 | | TOTAL | 12 - 16.5 USD | 28 | 29 | *You have to add the cost of the case and soldering supplies. I'm using a case described as T25. It costs about 8 USD. You can make the case of almost anything.* 30 | 31 | # Branches 32 | 33 | The idea is to keep in the main branch only the code that don't need any hardware changes. So once you build your own device, you won't need to change it. Adding mods to hardware is possible using other branches. 34 | 35 | So features like: 36 | * 10 switches 37 | * additional jack outputs for expression pedals 38 | * led status diodes 39 | 40 | are possible and can be implemented, but they will never be part of the main branch. 41 | 42 | I want to keep the device easy and cheap to build but still highly moddable. 43 | 44 | # Build instruction 45 | 46 | FAQ: 47 | 48 | * **Do I need to know how to solder?** - A little bit. If you can solder two wires together, you have the necessary skills. 49 | * **Do I need to know C++?** - No 50 | * **Is it hard to create my own controller?** - Relatively easy 51 | 52 | See [BUILD](./BUILD.md). 53 | 54 | *Remember to follow the build instructions. I cannot get the responsibility if you mess something up. Everything you do, you do on your own responsibility. I just provided a free firmware for your device.* 55 | 56 | # Manual 57 | 58 | See [MANUAL](./MANUAL.md). 59 | 60 | # Feature requests 61 | 62 | Leave me an issue on github. It's highly possible that I will implement it. 63 | 64 | # Changelog 65 | 66 | See [CHANGELOG](./CHANGELOG.md). 67 | 68 | # Build photos 69 | 70 | Send me your build photos. I will post it here :) 71 | 72 | My build: 73 | 74 | ![photo_1](./photos/galczo5_1.jpg) 75 | 76 | ![photo_2](./photos/galczo5_2.jpg) -------------------------------------------------------------------------------- /UPLOADING.md: -------------------------------------------------------------------------------- 1 | # How to upload code into Arduino board 2 | 3 | Guide for total beginners. 4 | 5 | # 1. Install Visual Studio Code 6 | 7 | Just download it from the official site and follow the installer steps. 8 | 9 | https://code.visualstudio.com/ 10 | 11 | # 2. Open Visual Studio Code and go to extensions 12 | 13 | ![upload_1](./photos/upload_1.png) 14 | 15 | # 3. Install PlatformIO IDE extension 16 | 17 | ![upload_2](./photos/upload_2.png) 18 | 19 | # 4. PIO Home should appear, open project 20 | 21 | ![upload_3](./photos/upload_3.png) 22 | 23 | # 5. Compile code 24 | 25 | ![upload_4](./photos/upload_4.png) 26 | 27 | # 6. Upload code 28 | 29 | ![upload_5](./photos/upload_5.png) 30 | 31 | 32 | ## Troubleshooting 33 | 34 | If something is not working try to follow official PlatformIO instructions or just google error messages. I had no problems with installation and code uploading, it just worked out of the box. 35 | 36 | -------------------------------------------------------------------------------- /include/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for project header files. 3 | 4 | A header file is a file containing C declarations and macro definitions 5 | to be shared between several project source files. You request the use of a 6 | header file in your project source file (C, C++, etc) located in `src` folder 7 | by including it, with the C preprocessing directive `#include'. 8 | 9 | ```src/main.c 10 | 11 | #include "header.h" 12 | 13 | int main (void) 14 | { 15 | ... 16 | } 17 | ``` 18 | 19 | Including a header file produces the same results as copying the header file 20 | into each source file that needs it. Such copying would be time-consuming 21 | and error-prone. With a header file, the related declarations appear 22 | in only one place. If they need to be changed, they can be changed in one 23 | place, and programs that include the header file will automatically use the 24 | new version when next recompiled. The header file eliminates the labor of 25 | finding and changing all the copies as well as the risk that a failure to 26 | find one copy will result in inconsistencies within a program. 27 | 28 | In C, the usual convention is to give header files names that end with `.h'. 29 | It is most portable to use only letters, digits, dashes, and underscores in 30 | header file names, and at most one dot. 31 | 32 | Read more about using header files in official GCC documentation: 33 | 34 | * Include Syntax 35 | * Include Operation 36 | * Once-Only Headers 37 | * Computed Includes 38 | 39 | https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html 40 | -------------------------------------------------------------------------------- /lib/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for project specific (private) libraries. 3 | PlatformIO will compile them to static libraries and link into executable file. 4 | 5 | The source code of each library should be placed in a an own separate directory 6 | ("lib/your_library_name/[here are source files]"). 7 | 8 | For example, see a structure of the following two libraries `Foo` and `Bar`: 9 | 10 | |--lib 11 | | | 12 | | |--Bar 13 | | | |--docs 14 | | | |--examples 15 | | | |--src 16 | | | |- Bar.c 17 | | | |- Bar.h 18 | | | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html 19 | | | 20 | | |--Foo 21 | | | |- Foo.c 22 | | | |- Foo.h 23 | | | 24 | | |- README --> THIS FILE 25 | | 26 | |- platformio.ini 27 | |--src 28 | |- main.c 29 | 30 | and a contents of `src/main.c`: 31 | ``` 32 | #include 33 | #include 34 | 35 | int main (void) 36 | { 37 | ... 38 | } 39 | 40 | ``` 41 | 42 | PlatformIO Library Dependency Finder will find automatically dependent 43 | libraries scanning project source files. 44 | 45 | More information about PlatformIO Library Dependency Finder 46 | - https://docs.platformio.org/page/librarymanager/ldf.html 47 | -------------------------------------------------------------------------------- /photos/din.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galczo5/open-midi-controller/a09a216226a474328ef5e79db78161c04f65fea8/photos/din.png -------------------------------------------------------------------------------- /photos/galczo5_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galczo5/open-midi-controller/a09a216226a474328ef5e79db78161c04f65fea8/photos/galczo5_1.jpg -------------------------------------------------------------------------------- /photos/galczo5_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galczo5/open-midi-controller/a09a216226a474328ef5e79db78161c04f65fea8/photos/galczo5_2.jpg -------------------------------------------------------------------------------- /photos/map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galczo5/open-midi-controller/a09a216226a474328ef5e79db78161c04f65fea8/photos/map.png -------------------------------------------------------------------------------- /photos/upload_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galczo5/open-midi-controller/a09a216226a474328ef5e79db78161c04f65fea8/photos/upload_1.png -------------------------------------------------------------------------------- /photos/upload_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galczo5/open-midi-controller/a09a216226a474328ef5e79db78161c04f65fea8/photos/upload_2.png -------------------------------------------------------------------------------- /photos/upload_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galczo5/open-midi-controller/a09a216226a474328ef5e79db78161c04f65fea8/photos/upload_3.png -------------------------------------------------------------------------------- /photos/upload_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galczo5/open-midi-controller/a09a216226a474328ef5e79db78161c04f65fea8/photos/upload_4.png -------------------------------------------------------------------------------- /photos/upload_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galczo5/open-midi-controller/a09a216226a474328ef5e79db78161c04f65fea8/photos/upload_5.png -------------------------------------------------------------------------------- /photos/wiring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galczo5/open-midi-controller/a09a216226a474328ef5e79db78161c04f65fea8/photos/wiring.png -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | [env:nanoatmega328] 12 | platform = atmelavr 13 | board = nanoatmega328new 14 | framework = arduino 15 | lib_extra_dirs = ~/Documents/Arduino/libraries 16 | lib_deps = 17 | marcoschwartz/LiquidCrystal_I2C@^1.1.4 18 | fortyseveneffects/MIDI Library@^5.0.2 19 | -------------------------------------------------------------------------------- /src/config/command-type.h: -------------------------------------------------------------------------------- 1 | #ifndef COMMAND_TYPE_H 2 | #define COMMAND_TYPE_H 3 | 4 | enum CommandType { 5 | UNSET = 0, 6 | NOTE = 1, 7 | CC = 2, 8 | TOGGLE_CC = 3, 9 | NEXT_PAGE = 4, 10 | PREV_PAGE = 5, 11 | PAGE = 6, 12 | TEMP_PAGE = 7 13 | }; 14 | 15 | #define NUMBER_OF_COMMAND_TYPES 8 16 | 17 | #endif 18 | -------------------------------------------------------------------------------- /src/config/controller-button-entity.h: -------------------------------------------------------------------------------- 1 | #ifndef CONTROLLER_BUTTON_H 2 | #define CONTROLLER_BUTTON_H 3 | 4 | #include 5 | 6 | struct ControllerButtonEntity { 7 | byte channel; 8 | byte type; 9 | byte value1; 10 | byte value2; 11 | byte value3; 12 | }; 13 | 14 | #endif -------------------------------------------------------------------------------- /src/config/midi-controller-config.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "midi-controller-config.h" 4 | #include "footswitch/footswitch-state.h" 5 | 6 | MidiControllerConfig::MidiControllerConfig() { 7 | this->page = 0; 8 | for (int i = 0; i < BUFFER_SIZE; i++) { 9 | this->storedData[i] = EEPROM.read(i); 10 | } 11 | } 12 | 13 | int MidiControllerConfig::getPage() { 14 | return this->page; 15 | } 16 | 17 | ControllerButtonEntity MidiControllerConfig::getButtonData(int no, FootswitchState click) { 18 | int mode = 0; 19 | 20 | if (click & FootswitchState::LONG_CLICK) { 21 | mode = LONG_CLICK_BUFFER_START; 22 | } else if (click & FootswitchState::DOUBLE_CLICK) { 23 | mode = 2 * LONG_CLICK_BUFFER_START; 24 | } 25 | 26 | int index = ((PAGE_SIZE * this->page) + (no * BUTTON_SIZE)) + mode; 27 | 28 | ControllerButtonEntity button = { 29 | this->storedData[index], 30 | this->storedData[index + 1], 31 | this->storedData[index + 2], 32 | this->storedData[index + 3], 33 | this->storedData[index + 4], 34 | }; 35 | 36 | return button; 37 | } 38 | 39 | void MidiControllerConfig::setButton(int no, ControllerButtonEntity button, FootswitchState click) { 40 | int mode = 0; 41 | 42 | if (click == FootswitchState::LONG_CLICK) { 43 | mode = LONG_CLICK_BUFFER_START; 44 | } else if (click == FootswitchState::DOUBLE_CLICK) { 45 | mode = 2 * LONG_CLICK_BUFFER_START; 46 | } 47 | 48 | int index = ((PAGE_SIZE * this->page) + (no * BUTTON_SIZE)) + mode; 49 | 50 | this->storedData[index] = button.channel; 51 | this->storedData[index + 1] = button.type; 52 | this->storedData[index + 2] = button.value1; 53 | this->storedData[index + 3] = button.value2; 54 | this->storedData[index + 4] = button.value3; 55 | 56 | EEPROM.write(index, button.channel); 57 | EEPROM.write(index + 1, button.type); 58 | EEPROM.write(index + 2, button.value1); 59 | EEPROM.write(index + 3, button.value2); 60 | EEPROM.write(index + 4, button.value3); 61 | } 62 | 63 | void MidiControllerConfig::setPage(int page) { 64 | this->page = page % PAGE_NO; 65 | 66 | if (this->page < 0) { 67 | this->page = PAGE_NO - 1; 68 | } 69 | } 70 | 71 | 72 | boolean MidiControllerConfig::isInUsbMidiMode() { 73 | return EEPROM.read(USB_MODE_EEPROM_ADDR); 74 | } 75 | 76 | void MidiControllerConfig::setUsbMidiMode(boolean enabled) { 77 | EEPROM.write(USB_MODE_EEPROM_ADDR, enabled ? 1 : 0); 78 | } -------------------------------------------------------------------------------- /src/config/midi-controller-config.h: -------------------------------------------------------------------------------- 1 | #ifndef CONFIG_H 2 | #define CONFIG_H 3 | 4 | #include 5 | #include "controller-button-entity.h" 6 | #include "footswitch/footswitch-state.h" 7 | #include "command-type.h" 8 | 9 | #define BUTTON_NO 6 10 | #define PAGE_NO 3 11 | #define PAGE_SIZE 48 12 | #define BUTTON_SIZE 5 13 | #define LONG_CLICK_BUFFER_START BUTTON_NO * PAGE_NO * BUTTON_SIZE 14 | #define ACTIONS_NO 3 15 | #define BUFFER_SIZE BUTTON_NO * BUTTON_SIZE * ACTIONS_NO * PAGE_NO 16 | 17 | #define USB_MODE_EEPROM_ADDR 800 18 | 19 | class MidiControllerConfig { 20 | 21 | private: 22 | const int MAX_PAGES = PAGE_NO; 23 | 24 | byte storedData[BUFFER_SIZE]; 25 | int page; 26 | 27 | public: 28 | MidiControllerConfig(); 29 | 30 | int getPage(); 31 | void setPage(int page); 32 | 33 | ControllerButtonEntity getButtonData(int no, FootswitchState click); 34 | void setButton(int no, ControllerButtonEntity button, FootswitchState click); 35 | 36 | 37 | bool isInUsbMidiMode(); 38 | void setUsbMidiMode(boolean enabled); 39 | }; 40 | 41 | #endif -------------------------------------------------------------------------------- /src/configuration/configuration-state-machine.cpp: -------------------------------------------------------------------------------- 1 | #include "configuration-state-machine.h" 2 | #include "configuration-state.h" 3 | #include "config/command-type.h" 4 | #include "config/controller-button-entity.h" 5 | #include "consts.h" 6 | 7 | #define CHANNEL 0 8 | #define TYPE 1 9 | #define VALUE1 2 10 | #define VALUE2 3 11 | #define VALUE3 4 12 | 13 | void ConfigurationStateMachine::reset() { 14 | this->state = ConfigurationState::SELECT_FOOTSWITCH; 15 | 16 | this->value = 0; 17 | this->footswitchNo = 0; 18 | 19 | this->configBytes[CHANNEL] = 0; 20 | this->configBytes[TYPE] = 0; 21 | this->configBytes[VALUE1] = 0; 22 | this->configBytes[VALUE2] = 0; 23 | this->configBytes[VALUE3] = 0; 24 | } 25 | 26 | ConfigurationState ConfigurationStateMachine::next() { 27 | this->configBytes[this->state] = this->value; 28 | this->value = 0; 29 | 30 | if (this->state == ConfigurationState::SELECT_VALUE2 && this->configBytes[TYPE] == CommandType::CC) { 31 | this->state = ConfigurationState::EXIT; 32 | } else if (this->state == ConfigurationState::SELECT_VALUE2 && this->configBytes[TYPE] == CommandType::NOTE) { 33 | this->state = ConfigurationState::EXIT; 34 | } else if (this->state == ConfigurationState::SELECT_TYPE && this->configBytes[TYPE] == CommandType::NEXT_PAGE) { 35 | this->state = ConfigurationState::EXIT; 36 | } else if (this->state == ConfigurationState::SELECT_TYPE && this->configBytes[TYPE] == CommandType::PREV_PAGE) { 37 | this->state = ConfigurationState::EXIT; 38 | } else if (this->state == ConfigurationState::SELECT_TYPE && this->configBytes[TYPE] == CommandType::UNSET) { 39 | this->state = ConfigurationState::EXIT; 40 | } else if (this->state == ConfigurationState::SELECT_VALUE1 && this->configBytes[TYPE] == CommandType::PAGE) { 41 | this->state = ConfigurationState::EXIT; 42 | } else if (this->state == ConfigurationState::SELECT_VALUE1 && this->configBytes[TYPE] == CommandType::TEMP_PAGE) { 43 | this->state = ConfigurationState::EXIT; 44 | } else { 45 | this->state = static_cast(this->state + 1); 46 | } 47 | 48 | return this->state; 49 | } 50 | 51 | ConfigurationState ConfigurationStateMachine::getState() { 52 | return this->state; 53 | } 54 | 55 | void ConfigurationStateMachine::incrementValue() { 56 | 57 | int numberOfValues = MIDI_MAX_VALUE; 58 | 59 | if (this->state == ConfigurationState::SELECT_TYPE) { 60 | numberOfValues = NUMBER_OF_COMMAND_TYPES; 61 | } else if (this->state == ConfigurationState::SELECT_VALUE1 && (this->configBytes[TYPE] == CommandType::PAGE || this->configBytes[TYPE] == CommandType::TEMP_PAGE)) { 62 | numberOfValues = NUMBER_OF_PAGES; 63 | } 64 | 65 | this->value = (this->value + 1) % numberOfValues; 66 | } 67 | 68 | void ConfigurationStateMachine::decrementValue() { 69 | 70 | int numberOfValues = MIDI_MAX_VALUE; 71 | 72 | if (this->state == ConfigurationState::SELECT_TYPE) { 73 | numberOfValues = NUMBER_OF_COMMAND_TYPES; 74 | } else if (this->state == ConfigurationState::SELECT_VALUE1 && (this->configBytes[TYPE] == CommandType::PAGE || this->configBytes[TYPE] == CommandType::TEMP_PAGE)) { 75 | numberOfValues = NUMBER_OF_PAGES; 76 | } 77 | 78 | int newValue = this->value - 1; 79 | if (newValue < 0) { 80 | newValue = numberOfValues - 1; 81 | } 82 | 83 | this->value = newValue % numberOfValues; 84 | } 85 | 86 | byte ConfigurationStateMachine::getValue() { 87 | return this->value; 88 | } 89 | 90 | void ConfigurationStateMachine::setFootswitch(int no, FootswitchState click) { 91 | this->footswitchNo = no; 92 | this->click = click; 93 | } 94 | 95 | int ConfigurationStateMachine::getFootswitch() { 96 | return this->footswitchNo; 97 | } 98 | 99 | ControllerButtonEntity ConfigurationStateMachine::getControllerButton() { 100 | ControllerButtonEntity button = { 101 | this->configBytes[CHANNEL], 102 | this->configBytes[TYPE], 103 | this->configBytes[VALUE1], 104 | this->configBytes[VALUE2], 105 | this->configBytes[VALUE3] 106 | }; 107 | 108 | return button; 109 | } 110 | 111 | FootswitchState ConfigurationStateMachine::getFootswitchState() { 112 | return this->click; 113 | } 114 | 115 | CommandType ConfigurationStateMachine::getCommandType() { 116 | return static_cast(this->configBytes[TYPE]); 117 | } 118 | 119 | boolean ConfigurationStateMachine::shouldEnterConfiguration() { 120 | return this->enterConfiguration; 121 | } 122 | 123 | void ConfigurationStateMachine::setShouldEnterConfiguration(boolean state) { 124 | this->enterConfiguration = state; 125 | } 126 | 127 | boolean ConfigurationStateMachine::shouldPrintInfo() { 128 | return this->printInfo; 129 | } 130 | 131 | void ConfigurationStateMachine::setShouldPrintInfo(boolean state) { 132 | this->printInfo = state; 133 | } -------------------------------------------------------------------------------- /src/configuration/configuration-state-machine.h: -------------------------------------------------------------------------------- 1 | #ifndef CONFIGURATION_H 2 | #define CONFIGURATION_H 3 | 4 | #include 5 | #include "configuration-state.h" 6 | #include "config/controller-button-entity.h" 7 | #include "config/command-type.h" 8 | #include "footswitch/footswitch-state.h" 9 | 10 | class ConfigurationStateMachine { 11 | private: 12 | byte configBytes[5]; 13 | ConfigurationState state; 14 | int footswitchNo; 15 | byte value; 16 | FootswitchState click; 17 | boolean enterConfiguration; 18 | boolean printInfo; 19 | 20 | public: 21 | void reset(); 22 | ConfigurationState next(); 23 | 24 | ConfigurationState getState(); 25 | 26 | void incrementValue(); 27 | void decrementValue(); 28 | byte getValue(); 29 | 30 | void setFootswitch(int no, FootswitchState click); 31 | int getFootswitch(); 32 | FootswitchState getFootswitchState(); 33 | 34 | ControllerButtonEntity getControllerButton(); 35 | CommandType getCommandType(); 36 | 37 | boolean shouldEnterConfiguration(); 38 | void setShouldEnterConfiguration(boolean state); 39 | 40 | boolean shouldPrintInfo(); 41 | void setShouldPrintInfo(boolean state); 42 | }; 43 | 44 | #endif -------------------------------------------------------------------------------- /src/configuration/configuration-state.h: -------------------------------------------------------------------------------- 1 | #ifndef CONFIGURATION_STATE_H 2 | #define CONFIGURATION_STATE_H 3 | 4 | enum ConfigurationState { 5 | SELECT_FOOTSWITCH = -1, 6 | SELECT_CHANNEL = 0, 7 | SELECT_TYPE = 1, 8 | SELECT_VALUE1 = 2, 9 | SELECT_VALUE2 = 3, 10 | SELECT_VALUE3 = 4, 11 | EXIT = 5 12 | }; 13 | 14 | #endif -------------------------------------------------------------------------------- /src/configuration/configurator.cpp: -------------------------------------------------------------------------------- 1 | #include "configurator.h" 2 | #include "configuration-state.h" 3 | #include "footswitch/footswitch.h" 4 | #include "config/command-type.h" 5 | #include "consts.h" 6 | 7 | Configurator::Configurator(MidiControllerConfig *config, ConfigurationStateMachine *configurationStateMachine, Printer *printer) { 8 | this->configurationStateMachine = configurationStateMachine; 9 | this->printer = printer; 10 | this->config = config; 11 | } 12 | 13 | void Configurator::configure(Footswitch* footswitches[]) { 14 | ConfigurationState configurationState = this->configurationStateMachine->getState(); 15 | 16 | // Select footswitch 17 | if (configurationState == ConfigurationState::SELECT_FOOTSWITCH) { 18 | for (int i = 0; i < NUMBER_OF_FOOTSWITCHES; i++) { 19 | FootswitchState click = footswitches[i]->checkClicked(); 20 | if (click & FootswitchState::ANY_CLICK) { 21 | this->printer->clickType(click); 22 | this->configurationStateMachine->setFootswitch(footswitches[i]->getNumber(), click); 23 | this->configurationStateMachine->next(); 24 | 25 | this->printer->configurationPrompt( 26 | this->configurationStateMachine->getState(), 27 | this->configurationStateMachine->getValue(), 28 | CommandType::UNSET 29 | ); 30 | return; 31 | } 32 | } 33 | 34 | return; 35 | } 36 | 37 | if (configurationState == ConfigurationState::EXIT) { 38 | return; 39 | } 40 | 41 | ConfigurationState state = this->configurationStateMachine->getState(); 42 | CommandType commandType = this->configurationStateMachine->getCommandType(); 43 | 44 | FootswitchState fsIncrementState = footswitches[FS_CONFIG_INCREMENT]->checkClicked(); 45 | FootswitchState fsDecrementState = footswitches[FS_CONFIG_DECREMENT]->checkClicked(); 46 | FootswitchState fsNextState = footswitches[FS_CONFIG_NEXT]->checkClicked(); 47 | 48 | if (fsIncrementState & FootswitchState::ANY_CLICK) { 49 | this->configurationStateMachine->incrementValue(); 50 | byte value = this->configurationStateMachine->getValue(); 51 | this->printer->configurationPrompt(state, value, commandType); 52 | 53 | } else if (fsDecrementState & FootswitchState::ANY_CLICK) { 54 | this->configurationStateMachine->decrementValue(); 55 | byte value = this->configurationStateMachine->getValue(); 56 | this->printer->configurationPrompt(state, value, commandType); 57 | 58 | } 59 | 60 | if (fsNextState & FootswitchState::ANY_CLICK) { 61 | ConfigurationState newState = this->configurationStateMachine->next(); 62 | 63 | if (newState == ConfigurationState::EXIT) { 64 | this->config->setButton( 65 | this->configurationStateMachine->getFootswitch(), 66 | this->configurationStateMachine->getControllerButton(), 67 | this->configurationStateMachine->getFootswitchState() 68 | ); 69 | 70 | this->configurationStateMachine->reset(); 71 | this->printer->selectFootswitchPrompt(); 72 | } else { 73 | this->printer->configurationPrompt( 74 | this->configurationStateMachine->getState(), 75 | this->configurationStateMachine->getValue(), 76 | this->configurationStateMachine->getCommandType() 77 | ); 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /src/configuration/configurator.h: -------------------------------------------------------------------------------- 1 | #ifndef CONFIGURATOR_H 2 | #define CONFIGURATOR_H 3 | 4 | #include "configuration-state-machine.h" 5 | #include "printer/printer.h" 6 | #include "footswitch/footswitch.h" 7 | #include "config/midi-controller-config.h" 8 | 9 | class Configurator { 10 | 11 | private: 12 | MidiControllerConfig *config; 13 | ConfigurationStateMachine *configurationStateMachine; 14 | Printer *printer; 15 | 16 | public: 17 | Configurator(MidiControllerConfig *config, ConfigurationStateMachine *configurationStateMachine, Printer *printer); 18 | void configure(Footswitch* footswitches[]); 19 | }; 20 | 21 | #endif -------------------------------------------------------------------------------- /src/consts.h: -------------------------------------------------------------------------------- 1 | #define NUMBER_OF_FOOTSWITCHES 6 2 | #define NUMBER_OF_PAGES 3 3 | #define MIDI_MAX_VALUE 128 4 | 5 | #define FS_1_PIN 2 6 | #define FS_2_PIN 3 7 | #define FS_3_PIN 4 8 | #define FS_4_PIN 5 9 | #define FS_5_PIN 6 10 | #define FS_6_PIN 7 11 | 12 | #define FS_CONFIG_1 1 13 | #define FS_CONFIG_2 3 14 | 15 | #define FS_CONFIG_NEXT 3 16 | #define FS_CONFIG_INCREMENT 1 17 | #define FS_CONFIG_DECREMENT 0 18 | 19 | #define FS_INFO_1 0 20 | #define FS_INFO_2 2 21 | 22 | #define FS_USB_MIDI_1 3 23 | #define FS_USB_MIDI_2 5 -------------------------------------------------------------------------------- /src/controller/controller-state-machine.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "controller-state-machine.h" 3 | #include "controller-state.h" 4 | 5 | ControllerStateMachine::ControllerStateMachine(ControllerState initState) { 6 | this->state = initState; 7 | this->stateChanged = false; 8 | } 9 | 10 | ControllerState ControllerStateMachine::getState() { 11 | return this->state; 12 | } 13 | 14 | void ControllerStateMachine::enterState(ControllerState state) { 15 | this->state = state; 16 | this->stateChanged = true; 17 | } 18 | 19 | boolean ControllerStateMachine::checkChanges() { 20 | boolean prevState = this->stateChanged; 21 | this->stateChanged = false; 22 | 23 | return prevState; 24 | } 25 | 26 | void ControllerStateMachine::toggleState() { 27 | if (this->getState() == ControllerState::SEND_COMMAND) { 28 | this->enterState(ControllerState::CONFIGURE); 29 | } else if (this->getState() == ControllerState::CONFIGURE) { 30 | this->enterState(ControllerState::SEND_COMMAND); 31 | } 32 | } -------------------------------------------------------------------------------- /src/controller/controller-state-machine.h: -------------------------------------------------------------------------------- 1 | #ifndef CONTROLLER_H 2 | #define CONTROLLER_H 3 | 4 | #include 5 | #include "controller-state.h" 6 | 7 | class ControllerStateMachine { 8 | private: 9 | boolean stateChanged; 10 | boolean usbMode; 11 | 12 | public: 13 | ControllerState state; 14 | ControllerStateMachine(ControllerState initState); 15 | ControllerState getState(); 16 | void enterState(ControllerState state); 17 | void toggleState(); 18 | boolean checkChanges(); 19 | }; 20 | 21 | #endif -------------------------------------------------------------------------------- /src/controller/controller-state.h: -------------------------------------------------------------------------------- 1 | #ifndef CONTROLLER_STATE_H 2 | #define CONTROLLER_STATE_H 3 | 4 | enum ControllerState { 5 | SEND_COMMAND = 0, 6 | CONFIGURE = 1 7 | }; 8 | 9 | #endif -------------------------------------------------------------------------------- /src/executor/command-executor.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "executor/command-executor.h" 4 | #include "config/command-type.h" 5 | #include "consts.h" 6 | 7 | MIDI_CREATE_DEFAULT_INSTANCE(); 8 | 9 | CommandExecutor::CommandExecutor(MidiControllerConfig* config, Printer *printer) { 10 | this->config = config; 11 | this->printer = printer; 12 | this->toggleIterator = 0; 13 | this->prevPage = -1; 14 | } 15 | 16 | void CommandExecutor::init() { 17 | MIDI.begin(); 18 | } 19 | 20 | int CommandExecutor::getLastValue(int no, int page) { 21 | String key = this->composeKey(no, page); 22 | int keyIndex = -1; 23 | 24 | for (int i = 0; i < TOGGLE_HISTORY_SIZE; i++) { 25 | if (this->toggleKeys[i] == key) { 26 | keyIndex = i; 27 | } 28 | } 29 | 30 | if (keyIndex >= 0) { 31 | return this->toggleValues[keyIndex]; 32 | } else { 33 | return -1; 34 | } 35 | } 36 | 37 | String CommandExecutor::composeKey(int no, int page) { 38 | return String(page) + 'x' + String(no); 39 | } 40 | 41 | void CommandExecutor::saveToggleHistory(int no, int page, byte value) { 42 | String key = this->composeKey(no, page); 43 | for (int i = 0; i < TOGGLE_HISTORY_SIZE; i++) { 44 | if (this->toggleKeys[i] == key) { 45 | this->toggleValues[i] = value; 46 | return; 47 | } 48 | } 49 | 50 | this->toggleKeys[this->toggleIterator] = key; 51 | this->toggleValues[this->toggleIterator] = value; 52 | this->toggleIterator = (this->toggleIterator + 1) % TOGGLE_HISTORY_SIZE; 53 | } 54 | 55 | void CommandExecutor::executeCommand(int no, FootswitchState click) { 56 | ControllerButtonEntity entity = this->config->getButtonData(no, click); 57 | int page = this->config->getPage(); 58 | 59 | switch (entity.type) { 60 | case byte(CommandType::NOTE): { 61 | MIDI.sendNoteOn(entity.value1, 127, entity.channel); 62 | this->lastValue = 127; 63 | break; 64 | } 65 | 66 | case byte(CommandType::CC): { 67 | MIDI.sendControlChange(entity.value1, entity.value2, entity.channel); 68 | this->lastValue = entity.value2; 69 | break; 70 | } 71 | 72 | case byte(CommandType::TOGGLE_CC): { 73 | int lastValue = this->getLastValue(no, page); 74 | 75 | byte valueToSend = entity.value2 != lastValue 76 | ? entity.value2 77 | : entity.value3; 78 | 79 | MIDI.sendControlChange(entity.value1, valueToSend, entity.channel); 80 | this->saveToggleHistory(no, page, valueToSend); 81 | this->lastValue = valueToSend; 82 | break; 83 | } 84 | 85 | case byte(CommandType::NEXT_PAGE): { 86 | this->config->setPage(page + 1); 87 | break; 88 | } 89 | 90 | case byte(CommandType::PREV_PAGE): { 91 | this->config->setPage(page - 1); 92 | break; 93 | } 94 | 95 | case byte(CommandType::PAGE): { 96 | this->config->setPage(entity.value1); 97 | break; 98 | } 99 | 100 | case byte(CommandType::TEMP_PAGE): { 101 | this->prevPage = this->config->getPage(); 102 | this->config->setPage(entity.value1); 103 | break; 104 | } 105 | } 106 | } 107 | 108 | byte CommandExecutor::getExecutedValue() { 109 | return this->lastValue; 110 | } 111 | 112 | void CommandExecutor::sendCommands(Footswitch* footswitches[]) { 113 | for (int i = 0; i < NUMBER_OF_FOOTSWITCHES; i++) { 114 | FootswitchState state = footswitches[i]->checkClicked(); 115 | 116 | if (state & FootswitchState::ANY_CLICK) { 117 | int no = footswitches[i]->getNumber(); 118 | 119 | int goBackToPage = this->getPrevPage(); 120 | 121 | this->executeCommand(no, state); 122 | this->printer->commandInfo(no, state, this->getExecutedValue()); 123 | 124 | if (goBackToPage >= 0) { 125 | this->config->setPage(goBackToPage); 126 | } 127 | 128 | return; 129 | } 130 | } 131 | } 132 | 133 | int CommandExecutor::getPrevPage() { 134 | int result = this->prevPage; 135 | this->prevPage = -1; 136 | return result; 137 | } -------------------------------------------------------------------------------- /src/executor/command-executor.h: -------------------------------------------------------------------------------- 1 | #ifndef COMMAND_EXECUTOR_H 2 | #define COMMAND_EXECUTOR_H 3 | 4 | #include 5 | #include 6 | #include "config/controller-button-entity.h" 7 | #include "config/midi-controller-config.h" 8 | #include "footswitch/footswitch.h" 9 | #include "footswitch/footswitch-state.h" 10 | #include "printer/printer.h" 11 | 12 | #define TOGGLE_HISTORY_SIZE 20 13 | 14 | class CommandExecutor { 15 | private: 16 | MidiControllerConfig *config; 17 | Printer *printer; 18 | 19 | int toggleIterator; 20 | String toggleKeys[TOGGLE_HISTORY_SIZE]; 21 | byte toggleValues[TOGGLE_HISTORY_SIZE]; 22 | 23 | int getLastValue(int no, int page); 24 | String composeKey(int no, int page); 25 | void saveToggleHistory(int no, int page, byte value); 26 | byte lastValue; 27 | int prevPage; 28 | 29 | public: 30 | CommandExecutor(MidiControllerConfig* config, Printer *printer); 31 | void init(); 32 | void executeCommand(int no, FootswitchState click); 33 | void sendCommands(Footswitch* footswitches[]); 34 | byte getExecutedValue(); 35 | int getPrevPage(); 36 | }; 37 | 38 | #endif -------------------------------------------------------------------------------- /src/footswitch/footswitch-state.h: -------------------------------------------------------------------------------- 1 | #ifndef CLICK_TYPE_H 2 | #define CLICK_TYPE_H 3 | 4 | enum FootswitchState { 5 | NONE = 0, 6 | PRESSED = 1, 7 | CLICK = 2, 8 | LONG_CLICK = 4, 9 | DOUBLE_CLICK = 8, 10 | ANY_CLICK = CLICK | LONG_CLICK | DOUBLE_CLICK 11 | }; 12 | 13 | #endif 14 | -------------------------------------------------------------------------------- /src/footswitch/footswitch.h: -------------------------------------------------------------------------------- 1 | #ifndef FOOTSWITH_H 2 | #define FOOTSWITH_H 3 | 4 | #include 5 | #include "footswitch-state.h" 6 | 7 | class Footswitch { 8 | private: 9 | int pin; 10 | int no; 11 | 12 | int lastState; 13 | boolean doubleClick; 14 | 15 | unsigned long clickTime; 16 | unsigned long repeatTime; 17 | 18 | unsigned long lastStateChange; 19 | 20 | boolean wasPressed; 21 | FootswitchState click; 22 | 23 | public: 24 | Footswitch(int no, int pin); 25 | void init(); 26 | void scan(); 27 | 28 | FootswitchState checkClicked(); 29 | int getNumber(); 30 | }; 31 | 32 | #endif 33 | -------------------------------------------------------------------------------- /src/footswitch/footswith.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "footswitch.h" 3 | #include "footswitch-state.h" 4 | 5 | #define LONG_CLICK_TIME 1000 6 | #define CLICK_TIME 200 7 | #define DEBOUNCE_TIME 10 8 | #define TIME_TO_REPEAT 3000 9 | #define REPEAT_TIMEOUT 100 10 | 11 | Footswitch::Footswitch(int no, int pin) { 12 | this->pin = pin; 13 | this->no = no; 14 | this->wasPressed = false; 15 | this->lastStateChange = 0; 16 | this->clickTime = 0; 17 | this->doubleClick = false; 18 | this->lastState = HIGH; 19 | } 20 | 21 | void Footswitch::init() { 22 | pinMode(this->pin, INPUT_PULLUP); 23 | } 24 | 25 | FootswitchState Footswitch::checkClicked() { 26 | return this->click; 27 | } 28 | 29 | int Footswitch::getNumber() { 30 | return this->no; 31 | } 32 | 33 | void Footswitch::scan() { 34 | int state = digitalRead(this->pin); 35 | unsigned long now = millis(); 36 | 37 | // DEBOUNCE - DO NOT CHANGE STATE 38 | if (state != this->lastState && now - this->lastStateChange < 50) { 39 | return; 40 | } 41 | 42 | if (state != this->lastState) { 43 | this->lastStateChange = now; 44 | } 45 | 46 | // BUTTON DOWN 47 | if (state == LOW) { 48 | 49 | // SECOND CLICK 50 | if (this->wasPressed && this->lastState != LOW) { 51 | this->doubleClick = true; 52 | } 53 | 54 | // FIRST CLICK 55 | if (!wasPressed && this->lastState != LOW) { 56 | this->clickTime = now; 57 | this->repeatTime = now; 58 | this->wasPressed = true; 59 | } 60 | 61 | // REPEAT ON PRESS 62 | if (now - this->clickTime > TIME_TO_REPEAT && now - this->repeatTime > REPEAT_TIMEOUT) { 63 | this->click = FootswitchState::CLICK; 64 | this->repeatTime = now; 65 | this->lastState = LOW; 66 | return; 67 | } 68 | 69 | this->click = FootswitchState::PRESSED; 70 | this->lastState = LOW; 71 | return; 72 | } 73 | 74 | // BUTTON UP 75 | 76 | unsigned long timeFromClick = now - this->clickTime; 77 | if (this->wasPressed && timeFromClick > LONG_CLICK_TIME && !this->doubleClick) { 78 | this->click = FootswitchState::LONG_CLICK; 79 | this->lastState = HIGH; 80 | this->wasPressed = false; 81 | return; 82 | } 83 | 84 | if (this->wasPressed && timeFromClick > CLICK_TIME) { 85 | if (this->doubleClick) { 86 | this->click = FootswitchState::DOUBLE_CLICK; 87 | } else { 88 | this->click = FootswitchState::CLICK; 89 | } 90 | 91 | this->lastState = HIGH; 92 | this->wasPressed = false; 93 | this->doubleClick = false; 94 | return; 95 | } 96 | 97 | // WAIT FOR DOUBLE CLICK 98 | if (this->wasPressed && timeFromClick <= CLICK_TIME) { 99 | this->lastState = state; 100 | return; 101 | } 102 | 103 | this->click = FootswitchState::NONE; 104 | } -------------------------------------------------------------------------------- /src/open-midi-controller.ino: -------------------------------------------------------------------------------- 1 | #define REVISION "20210801" 2 | 3 | #include 4 | 5 | #include "consts.h" 6 | #include "config/midi-controller-config.h" 7 | #include "footswitch/footswitch.h" 8 | #include "config/command-type.h" 9 | #include "controller/controller-state-machine.h" 10 | #include "printer/printer.h" 11 | #include "configuration/configuration-state-machine.h" 12 | #include "executor/command-executor.h" 13 | #include "configuration/configurator.h" 14 | 15 | /** 16 | * Open Midi Controller 17 | * Footswitch pin map: 18 | * __________________________ 19 | * | | 20 | * | fs1 D2 fs3 D4 fs5 D6 | 21 | * | | 22 | * | | 23 | * | fs2 D3 fs4 D5 fs6 D7 | 24 | * |__________________________| 25 | */ 26 | 27 | // INIT FOOTSWITCHES 28 | Footswitch fs1(0, FS_1_PIN); 29 | Footswitch fs2(1, FS_2_PIN); 30 | Footswitch fs3(2, FS_3_PIN); 31 | Footswitch fs4(3, FS_4_PIN); 32 | Footswitch fs5(4, FS_5_PIN); 33 | Footswitch fs6(5, FS_6_PIN); 34 | 35 | Footswitch* footswitches[6] = { &fs1, &fs2, &fs3, &fs4, &fs5, &fs6 }; 36 | 37 | MidiControllerConfig config; 38 | ControllerStateMachine controllerStateMachine(ControllerState::SEND_COMMAND); 39 | ConfigurationStateMachine configurationStateMachine; 40 | Printer printer(&config); 41 | CommandExecutor commandExecutor(&config, &printer); 42 | Configurator configurator(&config, &configurationStateMachine, &printer); 43 | boolean usbModeButtonsPressed = false; 44 | void(* resetFunc) (void) = 0; 45 | 46 | void setup() { 47 | commandExecutor.init(); 48 | 49 | printer.init(); 50 | printer.welcome(REVISION); 51 | 52 | boolean usbMode = config.isInUsbMidiMode(); 53 | if (usbMode) { 54 | Serial.begin(115200); 55 | } 56 | 57 | printer.usbMode(usbMode); 58 | 59 | for (Footswitch* fs : footswitches) { 60 | fs->init(); 61 | } 62 | } 63 | 64 | void loop() { 65 | 66 | for (Footswitch* fs : footswitches) { 67 | fs->scan(); 68 | } 69 | 70 | if (infoSwitchesPressed() || configSwitchesPressed() || usbModeSwitchesPressed()) { 71 | return; 72 | } 73 | 74 | ControllerState controllerState = controllerStateMachine.getState(); 75 | 76 | if (controllerStateMachine.checkChanges()) { 77 | configurationStateMachine.reset(); 78 | printer.changeModeMessage(controllerState == ControllerState::CONFIGURE); 79 | return; 80 | } 81 | 82 | switch (controllerState) { 83 | case ControllerState::CONFIGURE: 84 | configurator.configure(footswitches); 85 | break; 86 | 87 | case ControllerState::SEND_COMMAND: 88 | commandExecutor.sendCommands(footswitches); 89 | break; 90 | } 91 | 92 | } 93 | 94 | boolean usbModeSwitchesPressed() { 95 | FootswitchState fs1State = footswitches[FS_USB_MIDI_1]->checkClicked(); 96 | FootswitchState fs2State = footswitches[FS_USB_MIDI_2]->checkClicked(); 97 | 98 | if (fs1State == FootswitchState::PRESSED && fs2State == FootswitchState::PRESSED) { 99 | usbModeButtonsPressed = true; 100 | } 101 | 102 | if (fs1State == FootswitchState::NONE && usbModeButtonsPressed) { 103 | config.setUsbMidiMode(!config.isInUsbMidiMode()); 104 | resetFunc(); 105 | return true; 106 | } 107 | 108 | return false; 109 | } 110 | 111 | 112 | boolean infoSwitchesPressed() { 113 | FootswitchState fs1State = footswitches[FS_INFO_1]->checkClicked(); 114 | FootswitchState fs2State = footswitches[FS_INFO_2]->checkClicked(); 115 | 116 | if (fs1State == FootswitchState::PRESSED && fs2State == FootswitchState::PRESSED) { 117 | configurationStateMachine.setShouldPrintInfo(true); 118 | } 119 | 120 | if (fs1State == FootswitchState::NONE && configurationStateMachine.shouldPrintInfo()) { 121 | configurationStateMachine.setShouldPrintInfo(false); 122 | printer.printConfigPage(&config); 123 | return true; 124 | } 125 | 126 | return false; 127 | } 128 | 129 | boolean configSwitchesPressed() { 130 | FootswitchState fs1State = footswitches[FS_CONFIG_1]->checkClicked(); 131 | FootswitchState fs2State = footswitches[FS_CONFIG_2]->checkClicked(); 132 | 133 | if (fs1State == FootswitchState::PRESSED && fs2State == FootswitchState::PRESSED) { 134 | configurationStateMachine.setShouldEnterConfiguration(true); 135 | } 136 | 137 | if (fs1State == FootswitchState::NONE && configurationStateMachine.shouldEnterConfiguration()) { 138 | controllerStateMachine.toggleState(); 139 | configurationStateMachine.setShouldEnterConfiguration(false); 140 | return true; 141 | } 142 | 143 | return configurationStateMachine.shouldEnterConfiguration();; 144 | } 145 | -------------------------------------------------------------------------------- /src/printer/printer.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "configuration/configuration-state.h" 5 | #include "config/command-type.h" 6 | #include "footswitch/footswitch-state.h" 7 | #include "printer.h" 8 | 9 | #define MESSAGE_TIMEOUT 1500 10 | 11 | Printer::Printer(MidiControllerConfig *config) : lcd(0x27, 20, 4) { 12 | this->config = config; 13 | } 14 | 15 | void Printer::init() { 16 | lcd.init(); 17 | lcd.backlight(); 18 | } 19 | 20 | void Printer::welcome(String revision) { 21 | this->lcd.setCursor(0, 0); 22 | this->lcd.print("MIDI CONTROLLER"); 23 | this->lcd.setCursor(0, 1); 24 | this->lcd.print("REV. " + revision); 25 | delay(MESSAGE_TIMEOUT); 26 | this->lcd.clear(); 27 | this->lcd.setCursor(0, 0); 28 | this->lcd.print("PRESS ANY"); 29 | this->lcd.setCursor(0, 1); 30 | this->lcd.print("FOOTSWITCH"); 31 | } 32 | 33 | void Printer::enterConfiguration() { 34 | this->lcd.clear(); 35 | this->lcd.setCursor(0, 0); 36 | this->lcd.print("MIDI CONTROLLER"); 37 | this->lcd.setCursor(0, 1); 38 | this->lcd.print("CONFIGURATION"); 39 | delay(MESSAGE_TIMEOUT); 40 | this->lcd.clear(); 41 | 42 | this->selectFootswitchPrompt(); 43 | } 44 | 45 | void Printer::leaveConfiguration() { 46 | this->lcd.clear(); 47 | this->lcd.setCursor(0, 0); 48 | this->lcd.print("MIDI CONTROLLER"); 49 | this->lcd.setCursor(0, 1); 50 | this->lcd.print("SAVED"); 51 | delay(MESSAGE_TIMEOUT); 52 | this->lcd.clear(); 53 | } 54 | 55 | void Printer::selectFootswitchPrompt() { 56 | this->lcd.clear(); 57 | this->lcd.setCursor(0, 0); 58 | this->lcd.print("SELECT"); 59 | this->lcd.setCursor(0, 1); 60 | this->lcd.print("FOOTSWITCH"); 61 | } 62 | 63 | void Printer::configurationPrompt(ConfigurationState state, byte value, CommandType commandType) { 64 | String line1; 65 | String line2; 66 | 67 | if (state == ConfigurationState::SELECT_CHANNEL) { 68 | line1 = "MIDI CHANNEL"; 69 | line2 = String(value); 70 | } else if (state == ConfigurationState::SELECT_TYPE) { 71 | line1 = "COMMAND TYPE"; 72 | line2 = this->valueToCommandTypeLabel(value); 73 | } else if (state == ConfigurationState::SELECT_VALUE1) { 74 | 75 | int offset = 0; 76 | 77 | if (commandType == CommandType::CC || commandType == CommandType::TOGGLE_CC) { 78 | line1 = "CC"; 79 | } else if (commandType == CommandType::NOTE) { 80 | line1 = "NOTE"; 81 | } else if (commandType == CommandType::PAGE || commandType == CommandType::TEMP_PAGE) { 82 | line1 = "PAGE"; 83 | offset = 1; 84 | } else { 85 | line1 = "VALUE"; 86 | } 87 | 88 | line2 = String(value + offset); 89 | } else if (state == ConfigurationState::SELECT_VALUE2) { 90 | line1 = "VALUE"; 91 | line2 = String(value); 92 | } else if (state == ConfigurationState::SELECT_VALUE3) { 93 | line1 = "TOGGLE VALUE"; 94 | line2 = String(value); 95 | } else { 96 | line1 = "ERROR - UNKNOWN"; 97 | line2 = "STATE: " + String(state); 98 | } 99 | 100 | this->lcd.clear(); 101 | this->lcd.setCursor(0, 0); 102 | this->lcd.print(line1); 103 | this->lcd.setCursor(0, 1); 104 | this->lcd.print(line2); 105 | } 106 | 107 | String Printer::valueToCommandTypeLabel(byte value) { 108 | switch (value) { 109 | case CommandType::UNSET: 110 | return "EMPTY"; 111 | case CommandType::NOTE: 112 | return "NOTE"; 113 | case CommandType::CC: 114 | return "CC"; 115 | case CommandType::TOGGLE_CC: 116 | return "TOGGLE CC"; 117 | case CommandType::NEXT_PAGE: 118 | return "NEXT PAGE"; 119 | case CommandType::PREV_PAGE: 120 | return "PREV PAGE"; 121 | case CommandType::PAGE: 122 | return "GO TO PAGE"; 123 | case CommandType::TEMP_PAGE: 124 | return "TEMP PAGE"; 125 | 126 | default: 127 | return "ERROR: UNKNOWN"; 128 | } 129 | } 130 | 131 | String footswitchStateToTwoLetters(FootswitchState click) { 132 | if (click & FootswitchState::CLICK) { 133 | return ""; 134 | } 135 | 136 | if (click & FootswitchState::LONG_CLICK) { 137 | return "LONG "; 138 | } 139 | 140 | if (click & FootswitchState::DOUBLE_CLICK) { 141 | return "DOUBLE "; 142 | } 143 | 144 | return ""; 145 | } 146 | 147 | void Printer::commandInfo(int footswitchNo, FootswitchState click, byte lastValue) { 148 | 149 | ControllerButtonEntity btn = this->config->getButtonData(footswitchNo, click); 150 | int page = this->config->getPage(); 151 | 152 | this->lcd.clear(); 153 | this->lcd.setCursor(0, 0); 154 | this->lcd.print(footswitchStateToTwoLetters(click) + "FS " + String(footswitchNo + 1)); 155 | this->lcd.setCursor(14, 0); 156 | this->lcd.print("P" + String(page + 1)); 157 | this->lcd.setCursor(0, 1); 158 | 159 | if (btn.type == CommandType::CC) { 160 | this->lcd.print( 161 | this->valueToCommandTypeLabel(CommandType::CC) + " " + String(btn.value1) + " " + String(btn.value2) 162 | ); 163 | } else if (btn.type == CommandType::TOGGLE_CC) { 164 | 165 | String value2 = btn.value2 == lastValue 166 | ? "(" + String(btn.value2) + ")" 167 | : String(btn.value2); 168 | 169 | String value3 = btn.value3 == lastValue 170 | ? "(" + String(btn.value3) + ")" 171 | : String(btn.value3); 172 | 173 | this->lcd.print( 174 | this->valueToCommandTypeLabel(CommandType::CC) + " " + 175 | String(btn.value1) + " " + 176 | value2 + " " + 177 | value3 178 | ); 179 | } else if ( 180 | btn.type == CommandType::PAGE || 181 | btn.type == CommandType::NEXT_PAGE || 182 | btn.type == CommandType::PREV_PAGE || 183 | btn.type == CommandType::TEMP_PAGE) { 184 | 185 | this->lcd.print( 186 | this->valueToCommandTypeLabel(CommandType::PAGE) + " " + String(page + 1) 187 | ); 188 | 189 | } else if (btn.type == CommandType::UNSET) { 190 | this->lcd.print(this->valueToCommandTypeLabel(CommandType::UNSET)); 191 | } else if (btn.type == CommandType::NOTE) { 192 | this->lcd.print( 193 | this->valueToCommandTypeLabel(btn.type) + " " + 194 | String(btn.value1) + " " + 195 | String(btn.value2) 196 | ); 197 | } else { 198 | this->lcd.print("EMPTY"); 199 | } 200 | 201 | } 202 | 203 | void Printer::printConfigPage(MidiControllerConfig *config) { 204 | this->lcd.clear(); 205 | this->lcd.setCursor(0, 0); 206 | this->lcd.print("CURRENT"); 207 | this->lcd.setCursor(0, 1); 208 | this->lcd.print("CONFIGURATION"); 209 | delay(MESSAGE_TIMEOUT); 210 | 211 | for (int i = 0; i < BUTTON_NO; i++) { 212 | this->lcd.clear(); 213 | this->commandInfo(i, FootswitchState::CLICK, 0); 214 | delay(MESSAGE_TIMEOUT); 215 | } 216 | 217 | this->lcd.clear(); 218 | this->lcd.setCursor(0, 0); 219 | this->lcd.print("LONG CLICK"); 220 | this->lcd.setCursor(0, 1); 221 | this->lcd.print("CONFIGURATION"); 222 | delay(MESSAGE_TIMEOUT); 223 | 224 | for (int i = 0; i < BUTTON_NO; i++) { 225 | this->lcd.clear(); 226 | this->commandInfo(i, FootswitchState::LONG_CLICK, 0); 227 | delay(MESSAGE_TIMEOUT); 228 | } 229 | 230 | this->lcd.clear(); 231 | this->lcd.setCursor(0, 0); 232 | this->lcd.print("DOUBLE CLICK"); 233 | this->lcd.setCursor(0, 1); 234 | this->lcd.print("CONFIGURATION"); 235 | delay(MESSAGE_TIMEOUT); 236 | 237 | for (int i = 0; i < BUTTON_NO; i++) { 238 | this->lcd.clear(); 239 | this->commandInfo(i, FootswitchState::DOUBLE_CLICK, 0); 240 | delay(MESSAGE_TIMEOUT); 241 | } 242 | 243 | this->lcd.clear(); 244 | } 245 | 246 | void Printer::changeModeMessage(boolean inConfigurationMode) { 247 | if (inConfigurationMode) { 248 | this->enterConfiguration(); 249 | } else { 250 | this->leaveConfiguration(); 251 | } 252 | } 253 | 254 | void Printer::usbMode(boolean enabled) { 255 | if (enabled) { 256 | this->lcd.clear(); 257 | this->lcd.setCursor(0, 0); 258 | this->lcd.print("USB MODE"); 259 | this->lcd.setCursor(0, 1); 260 | this->lcd.print("ENABLED"); 261 | } else { 262 | this->lcd.clear(); 263 | this->lcd.setCursor(0, 0); 264 | this->lcd.print("MIDI MODE"); 265 | this->lcd.setCursor(0, 1); 266 | this->lcd.print("ENABLED"); 267 | } 268 | 269 | delay(MESSAGE_TIMEOUT); 270 | this->lcd.clear(); 271 | } 272 | 273 | void Printer::debug(String txt) { 274 | this->lcd.clear(); 275 | this->lcd.setCursor(0, 0); 276 | this->lcd.print(txt); 277 | delay(500); 278 | this->lcd.clear(); 279 | } 280 | 281 | void Printer::clickType(FootswitchState click) { 282 | this->lcd.clear(); 283 | this->lcd.setCursor(0, 0); 284 | 285 | if (click & FootswitchState::CLICK) { 286 | this->lcd.print("CLICK"); 287 | } 288 | 289 | if (click & FootswitchState::LONG_CLICK) { 290 | this->lcd.print("LONG CLICK"); 291 | } 292 | 293 | if (click & FootswitchState::DOUBLE_CLICK) { 294 | this->lcd.print("DOUBLE CLICK"); 295 | } 296 | 297 | delay(MESSAGE_TIMEOUT); 298 | this->lcd.clear(); 299 | 300 | } -------------------------------------------------------------------------------- /src/printer/printer.h: -------------------------------------------------------------------------------- 1 | #ifndef PRINTER_H 2 | #define PRINTER_H 3 | 4 | #include 5 | #include 6 | #include "configuration/configuration-state.h" 7 | #include "config/controller-button-entity.h" 8 | #include "config/midi-controller-config.h" 9 | #include "config/command-type.h" 10 | 11 | class Printer { 12 | private: 13 | String valueToCommandTypeLabel(byte value); 14 | LiquidCrystal_I2C lcd; 15 | MidiControllerConfig *config; 16 | 17 | public: 18 | Printer(MidiControllerConfig *config); 19 | void init(); 20 | void welcome(String revision); 21 | void enterConfiguration(); 22 | void leaveConfiguration(); 23 | void selectFootswitchPrompt(); 24 | void configurationPrompt(ConfigurationState state, byte value, CommandType commandType); 25 | void commandInfo(int footswitchNo, FootswitchState click, byte lastValue); 26 | void printConfigPage(MidiControllerConfig *config); 27 | void changeModeMessage(boolean inConfigurationMode); 28 | void usbMode(boolean enabled); 29 | void debug(String txt); 30 | void clickType(FootswitchState click); 31 | }; 32 | 33 | #endif -------------------------------------------------------------------------------- /test/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for PlatformIO Unit Testing and project tests. 3 | 4 | Unit Testing is a software testing method by which individual units of 5 | source code, sets of one or more MCU program modules together with associated 6 | control data, usage procedures, and operating procedures, are tested to 7 | determine whether they are fit for use. Unit testing finds problems early 8 | in the development cycle. 9 | 10 | More information about PlatformIO Unit Testing: 11 | - https://docs.platformio.org/page/plus/unit-testing.html 12 | --------------------------------------------------------------------------------