Merge pull request #32 from Trasselfrisyr/sysex

Config management mode
This commit is contained in:
John Stäck 2019-08-26 16:10:15 +02:00 committed by GitHub
commit dcd4862616
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 574 additions and 59 deletions

View file

@ -243,6 +243,8 @@ byte subOctaveDouble = 0;
Adafruit_MPR121 touchSensor = Adafruit_MPR121(); // This is the 12-input touch sensor
FilterOnePole breathFilter;
bool configManagementMode = false;
//_______________________________________________________________________________________________ SETUP
@ -267,9 +269,19 @@ void setup() {
pinMode(biteJumperGndPin, OUTPUT); //PBITE
digitalWrite(biteJumperGndPin, LOW); //PBITE
bool factoryReset = !digitalRead(ePin) && !digitalRead(mPin);
configManagementMode = !factoryReset && !digitalRead(uPin) && !digitalRead(dPin);
initDisplay(); //Start up display and show logo
//If going into config management mode, stop here before we even touch the EEPROM.
if(configManagementMode) {
configModeSetup();
return;
}
//Read eeprom data into global vars
readEEPROM();
readEEPROM(factoryReset);
activePatch = patch;
@ -280,8 +292,6 @@ void setup() {
}
breathFilter.setFilter(LOWPASS, filterFreq, 0.0); // create a one pole (RC) lowpass filter
initDisplay(); //Start up display and show logo
biteJumper = !digitalRead(biteJumperPin);
if (biteJumper){
@ -340,6 +350,13 @@ void setup() {
//_______________________________________________________________________________________________ MAIN LOOP
void loop() {
//If in config mgmt loop, do that and nothing else
if(configManagementMode) {
configModeLoop();
return;
}
breathFilter.input(analogRead(breathSensorPin));
pressureSensor = constrain((int) breathFilter.output(), 0, 4095); // Get the filtered pressure sensor reading from analog pin A0, input from sensor MP3V5004GP
readSwitches();
@ -367,7 +384,7 @@ void loop() {
bool bothPB = (pbUp > ((pitchbMaxVal + pitchbThrVal) / 2)) && (pbDn > ((pitchbMaxVal + pitchbThrVal) / 2));
bool brSuck = analogRead(breathSensorPin) < (breathCalZero - (bcasMode?900:800));
if (
(bothPB && legacy) ||
(brSuck && legacyBrAct && (bothPB || bcasMode))
@ -375,9 +392,9 @@ void loop() {
fingeredNoteUntransposed = patchLimit(fingeredNoteUntransposed + 1);
if (exSensor >= ((extracThrVal + extracMaxVal) / 2)) { // instant midi setting
if (exSensor >= ((extracThrVal + extracMaxVal) / 2)) { // instant midi setting
if ((fingeredNoteUntransposed >= 73) && (fingeredNoteUntransposed <= 88)) {
MIDIchannel = fingeredNoteUntransposed - 72; // Mid C and up
MIDIchannel = fingeredNoteUntransposed - 72; // Mid C and up
}
} else {
if (!pinkyKey) { // note number to patch number
@ -419,7 +436,7 @@ void loop() {
doPatchUpdate = 1;
}
if (!K1 && !K2 && K3 && !K4) { //send reverb pitchlatch value
if (!K1 && !K2 && K3 && !K4) { //send reverb pitchlatch value
reverb = ((pitchlatch - 36) * 2);
reverb = constrain(reverb, 0, 127);
@ -1104,7 +1121,7 @@ void portamento_() {
if (biteJumper) { //PBITE (if pulled low with jumper, use pressure sensor instead of capacitive bite sensor)
biteSensor=analogRead(bitePressurePin); // alternative kind bite sensor (air pressure tube and sensor) PBITE
} else {
} else {
biteSensor = touchRead(bitePin); // get sensor data, do some smoothing - SENSOR PIN 17 - PCB PINS LABELED "BITE" (GND left, sensor pin right)
}
if (0 == vibControl) {
@ -1180,7 +1197,7 @@ void readTeensySwitches() { //these seem to slow things down, so do it less ofte
}
void readSwitches() {
// Read touch pads (MPR121), compare against threshold value
bool touchKeys[12];
for (byte i = 0; i < 12; i++) {
@ -1207,11 +1224,12 @@ void readSwitches() {
K6 = touchKeys[K6Pin];
K7 = touchKeys[K7Pin];
pinkyKey = (touchRead(halfPitchBendKeyPin) > touch_Thr); // SENSOR PIN 1 - PCB PIN "S1"
int qTransp = (pinkyKey && (pinkySetting < 25)) ? pinkySetting-12 : 0;
// Calculate midi note number from pressed keys
// Calculate midi note number from pressed keys
fingeredNoteUntransposed = startNote
- 2*K1 - K2 - 3*K3 //"Trumpet valves"
@ -1230,7 +1248,7 @@ void readSwitches() {
if ((millis() - lastDeglitchTime) > deglitch) {
// whatever the reading is at, it's been there for longer
// than the debounce delay, so take it as the actual current state
fingeredNote = fingeredNoteRead;
fingeredNote = fingeredNoteRead;
}
lastFingering = fingeredNoteRead;
}

View file

@ -1,7 +1,7 @@
#ifndef __GLOBALS_H
#define __GLOBALS_H
#include "Wiring.h"
#include "wiring.h"
// The three states of our main state machine

View file

@ -1,7 +1,7 @@
#ifndef __MENU_H
#define __MENU_H
#include "Wiring.h"
#include "wiring.h"
#include "numenu.h"
#define MENU_ROW_HEIGHT 9

View file

@ -141,6 +141,7 @@ void dinMIDIsendProgramChange(uint8_t value, uint8_t ch) {
midiSend2B((0xC0 | ch), value);
}
// Send sysex commands to wireless module
void dinMIDIsendSysex(const uint8_t data[], const uint8_t length) {
MIDI_SERIAL.write(0xF0); //Sysex command
for(int i=0; i<length; ++i) {
@ -149,7 +150,6 @@ void dinMIDIsendSysex(const uint8_t data[], const uint8_t length) {
MIDI_SERIAL.write(0xF7); //Sysex end
}
void sendWLPower(const uint8_t level) {
uint8_t buf[6] = {
0x00, 0x21, 0x11, //Manufacturer id
@ -165,7 +165,6 @@ void sendWLPower(const uint8_t level) {
}
void sendWLChannel(const uint8_t channel) {
uint8_t buf[6] = {
0x00, 0x21, 0x11, //Manufacturer id
@ -180,3 +179,34 @@ void sendWLChannel(const uint8_t channel) {
dinMIDIsendSysex(buf, 6);
}
//Translate between "midi data" (only use 7 LSB per byte, big endian) and "teensy data" (little endian)
//Only 14 LSB of int value are used (2MSB are discarded), so only works for unsigned data 0-16383
//NOTE: This assumes code is running on a little-endian CPU, both for real device (Teensy) and simulator.
uint16_t convertToMidiValue(const uint16_t realdata) {
return (realdata & 0x3F80) >>7 | (realdata & 0x007F) <<8;
}
uint16_t convertFromMidiValue(const uint16_t mididata) {
return (mididata & 0x7F00) >> 8 | (mididata & 0x007F) <<7 ;
}
//Read from a memory location, such as MIDI receive buffer
uint16_t convertFromMidiValue(const uint8_t* mididata) {
uint8_t msb = *mididata;
uint8_t lsb = *(mididata+1);
return (msb & 0x007F) <<7 | (lsb & 0x007F);
}
//This is a bit different. MSB of each byte is just discarded (instead of discarding MSB for whole value). Just used for CRC (easier to compare)
uint32_t convertToMidiCRC(const uint32_t realdata) {
uint8_t* p = (uint8_t*)&realdata;
uint32_t r=0;
for(int i=0; i<4; ++i) {
r = r<<8 | (p[i] & 0x7F);
}
return r;
}

View file

@ -1,6 +1,9 @@
#ifndef __MIDI_H
#define __MIDI_H
//This is a completely made up "European" SysEx manufacturer ID.
static const char sysex_id[] = { 0x00, 0x3e, 0x7f };
//Enable use of USB and serial MIDI
#define USE_MIDI_USB
#define USE_MIDI_SERIAL
@ -33,4 +36,11 @@ void dinMIDIsendSysex(const uint8_t data[], const uint8_t length);
void sendWLPower(const uint8_t level);
void sendWLChannel(const uint8_t channel);
//Convert things between "regular data" and MIDI data (byte order and 7-bits-per-byte)
uint16_t convertToMidiValue(const uint16_t realdata);
uint16_t convertFromMidiValue(const uint16_t mididata);
uint16_t convertFromMidiValue(const uint8_t* mididata);
uint32_t convertToMidiCRC(const uint32_t realdata);
#endif

View file

@ -1,15 +1,18 @@
#include <Arduino.h>
#include <EEPROM.h>
#include <Adafruit_SSD1306.h>
#include "settings.h"
#include "globals.h"
#include "menu.h"
#include "hardware.h"
#include "config.h"
#include "midi.h"
#include "led.h"
//Read settings from eeprom. Returns wether or not anything was written (due to factory reset or upgrade)
void readEEPROM() {
bool factoryReset = !digitalRead(ePin) && !digitalRead(mPin);
void readEEPROM(const bool factoryReset) {
// if stored settings are not for current version, or Enter+Menu are pressed at startup, they are replaced by factory settings
uint16_t settingsVersion = readSetting(VERSION_ADDR);
@ -26,11 +29,11 @@ void readEEPROM() {
writeSetting(BREATH_THR_ADDR, BREATH_THR_FACTORY);
writeSetting(BREATH_MAX_ADDR, BREATH_MAX_FACTORY);
if (digitalRead(biteJumperPin)){ //PBITE (if pulled low with jumper, pressure sensor is used instead of capacitive bite sensing)
writeSetting(PORTAM_THR_ADDR, PORTAM_THR_FACTORY);
writeSetting(PORTAM_MAX_ADDR, PORTAM_MAX_FACTORY);
writeSetting(PORTAM_THR_ADDR, PORTAM_THR_FACTORY);
writeSetting(PORTAM_MAX_ADDR, PORTAM_MAX_FACTORY);
} else {
writeSetting(PORTAM_THR_ADDR, PORTPR_THR_FACTORY);
writeSetting(PORTAM_MAX_ADDR, PORTPR_MAX_FACTORY);
writeSetting(PORTAM_THR_ADDR, PORTPR_THR_FACTORY);
writeSetting(PORTAM_MAX_ADDR, PORTPR_MAX_FACTORY);
}
writeSetting(PITCHB_THR_ADDR, PITCHB_THR_FACTORY);
writeSetting(PITCHB_MAX_ADDR, PITCHB_MAX_FACTORY);
@ -104,7 +107,7 @@ void readEEPROM() {
writeSetting(VERSION_ADDR, EEPROM_VERSION);
}
// read all settings from EEPROM
breathThrVal = readSettingBounded(BREATH_THR_ADDR, breathLoLimit, breathHiLimit, BREATH_THR_FACTORY);
breathMaxVal = readSettingBounded(BREATH_MAX_ADDR, breathLoLimit, breathHiLimit, BREATH_MAX_FACTORY);
@ -114,7 +117,7 @@ void readEEPROM() {
pitchbMaxVal = readSettingBounded(PITCHB_MAX_ADDR, pitchbLoLimit, pitchbHiLimit, PITCHB_MAX_FACTORY);
transpose = readSettingBounded(TRANSP_ADDR, 0, 24, TRANSP_FACTORY);
MIDIchannel = readSettingBounded(MIDI_ADDR, 1, 16, MIDI_FACTORY);
breathCC = readSettingBounded(BREATH_CC_ADDR, 0, 127, BREATH_CC_FACTORY);
breathCC = readSettingBounded(BREATH_CC_ADDR, 0, 10, BREATH_CC_FACTORY);
breathAT = readSettingBounded(BREATH_AT_ADDR, 0, 1, BREATH_AT_FACTORY);
velocity = readSettingBounded(VELOCITY_ADDR, 0, 127, VELOCITY_FACTORY);
portamento = readSettingBounded(PORTAM_ADDR, 0, 2, PORTAM_FACTORY);
@ -180,7 +183,7 @@ void setBit(uint16_t &bitfield, const uint8_t pos, const uint16_t value) {
//Read and write EEPROM data
void writeSetting(uint16_t address, uint16_t value) {
void writeSetting(const uint16_t address, const uint16_t value) {
union {
uint8_t v[2];
uint16_t val;
@ -190,7 +193,7 @@ void writeSetting(uint16_t address, uint16_t value) {
EEPROM.update(address+1, data.v[1]);
}
uint16_t readSetting(uint16_t address) {
uint16_t readSetting(const uint16_t address) {
union {
uint8_t v[2];
uint16_t val;
@ -200,7 +203,7 @@ uint16_t readSetting(uint16_t address) {
return data.val;
}
uint16_t readSettingBounded(uint16_t address, uint16_t min, uint16_t max, uint16_t defaultValue) {
uint16_t readSettingBounded(const uint16_t address, const uint16_t min, const uint16_t max, const uint16_t defaultValue) {
uint16_t val = readSetting(address);
if(val < min || val > max) {
val = defaultValue;
@ -208,3 +211,290 @@ uint16_t readSettingBounded(uint16_t address, uint16_t min, uint16_t max, uint16
}
return val;
}
//Functions to send and receive config (and other things) via USB MIDI SysEx messages
uint32_t crc32(const uint8_t *message, const size_t length) {
size_t pos=0;
uint32_t crc=0xFFFFFFFF;
while (pos<length) {
crc ^= message[pos++]; //Get next byte and increment position
for (uint8_t j=0; j<8; ++j) { //Mask off 8 next bits
crc = (crc >> 1) ^ (0xEDB88320 & -(crc & 1));
}
}
return ~crc;
}
/*
Send EEPROM config dump as sysex message. Message format is structured like this:
+------------------------------------------------------------------------------------+
| vendor(3) | "NuEVIc01" (8) | Payload size (2) | EEPROM data (variable) | crc32 (4) |
+------------------------------------------------------------------------------------+
Payload size is for the EEPROM data chunk (not including anything else before or after
CRC32 covers the entire buffer up to and including the eeprom data (but not the checksum itself)
This currently operates under the assumption that the whole EEPROM chunk only consists of unsigned 16 bit ints, only using the range 0-16383
*/
void sendSysexSettings() {
const char *header = "NuEVIc01"; //NuEVI config dump 01
//Build a send buffer of all the things
size_t sysex_size = 3 + strlen(header) + 2 + EEPROM_SIZE + 4;
uint8_t *sysex_data = (uint8_t*)malloc(sysex_size);
//Positions (offsets) of parts in send buffer
int header_pos = 3;
int size_pos = header_pos + strlen(header);
int payload_pos = size_pos + 2;
int checksum_pos = payload_pos + EEPROM_SIZE;
//SysEX manufacturer ID
memcpy(sysex_data, sysex_id, 3);
//Header with command code
memcpy(sysex_data+header_pos, header, strlen(header));
//Payload length
*(uint16_t*)(sysex_data+size_pos) = convertToMidiValue(EEPROM_SIZE);
//Config data
uint16_t* config_buffer_start = (uint16_t*)(sysex_data+payload_pos);
//Read one settings item at a time, change data format, and put in send buffer
for(uint16_t idx=0; idx<EEPROM_SIZE/2; idx++) {
uint16_t eepromval = readSetting(idx*2);
config_buffer_start[idx] = convertToMidiValue(eepromval);
}
uint32_t checksum = crc32(sysex_data, checksum_pos);
*(uint32_t*)(sysex_data+checksum_pos) = convertToMidiCRC(checksum);
usbMIDI.sendSysEx(sysex_size, sysex_data);
free(sysex_data);
}
//Send a simple 3-byte message code as sysex
void sendSysexMessage(const char* messageCode) {
char sysexMessage[] = "vvvNuEVIccc"; //Placeholders for vendor and code
memcpy(sysexMessage, sysex_id, 3);
memcpy(sysexMessage+8, messageCode, 3);
usbMIDI.sendSysEx(11, (const uint8_t *)sysexMessage);
}
bool receiveSysexSettings(const uint8_t* data, const uint16_t length) {
//Expected size of data (vendor+NuEVIc02+len+payload+crc32)
uint16_t expected_size = 3 + 8 + 2 + EEPROM_SIZE + 4;
//Positions (offsets) of parts in buffer
int size_pos = 11;
int payload_pos = size_pos + 2;
int checksum_pos = payload_pos + EEPROM_SIZE;
//Make sure length of receive buffer is enough to read all we need to. We can accept extra junk at the end though.
if(length<expected_size) {
configShowMessage("Invalid config format");
return false;
}
//No need to verify vendor or header/command, already done before we get here.
//Calculate checksum of stuff received (everything before checksum), transform to midi format
//(being a one-way operation, we can't do the reverse anyway)
uint32_t crc=convertToMidiCRC(crc32(data, checksum_pos));
uint32_t crc_rcv;
memcpy(&crc_rcv, data+checksum_pos, 4);
if(crc != crc_rcv && crc_rcv != NO_CHECKSUM) {
configShowMessage("Invalid checksum");
return false;
}
//Verify that payload size matches the size of our EEPROM config
uint16_t payload_size = convertFromMidiValue(data+size_pos);
if(payload_size != EEPROM_SIZE) {
configShowMessage("Invalid config size");
return false;
}
uint16_t eeprom_version_rcv = convertFromMidiValue(data+(payload_pos+VERSION_ADDR));
if(eeprom_version_rcv != EEPROM_VERSION) {
configShowMessage("Invalid config version");
return false;
}
//Grab all the items in payload and save to EEPROM
for(uint16_t i=0; i<payload_size/2; i++) {
uint16_t addr = i*2;
uint16_t val;
val = convertFromMidiValue(data+(payload_pos+addr));
//Skip sensor calibration values if they are "out of bounds". This makes it possible to send a config that does
//not overwrite sensor calibration.
if(addr == BREATH_THR_ADDR || addr == BREATH_MAX_ADDR) {
if(val<breathLoLimit || val>breathHiLimit) continue;
}
if(addr == PORTAM_THR_ADDR || addr == PORTAM_MAX_ADDR) {
if(val<portamLoLimit || val>portamHiLimit) continue;
}
if(addr == PITCHB_THR_ADDR || addr == PITCHB_MAX_ADDR) {
if(val<pitchbLoLimit || val>pitchbHiLimit) continue;
}
if(addr == EXTRAC_THR_ADDR || addr == EXTRAC_MAX_ADDR) {
if(val<extracLoLimit || val>extracHiLimit) continue;
}
if(addr == CTOUCH_THR_ADDR) {
if(val<ctouchLoLimit || val>ctouchHiLimit) continue;
}
writeSetting(addr, val);
}
//All went well
return true;
}
//Send EEPROM and firmware versions
void sendSysexVersion() {
char sysexMessage[] = "vvvNuEVIc04eevvvvvvvv"; //Placeholders for vendor and code
uint8_t fwStrLen = min(strlen(FIRMWARE_VERSION), 8); //Limit firmware version string to 8 bytes
memcpy(sysexMessage, sysex_id, 3);
memcpy(sysexMessage+13, FIRMWARE_VERSION, fwStrLen);
*(uint16_t*)(sysexMessage+11) = convertToMidiValue(EEPROM_VERSION);
uint8_t message_length = 13+fwStrLen;
usbMIDI.sendSysEx(message_length, (const uint8_t *)sysexMessage);
}
extern Adafruit_SSD1306 display;
void configShowMessage(const char* message) {
display.fillRect(0,32,128,64,BLACK);
display.setCursor(0,32);
display.setTextColor(WHITE);
display.print(message);
display.display();
}
uint8_t* sysex_rcv_buffer = NULL;
uint16_t sysex_buf_size = 0;
void handleSysexChunk(const uint8_t *data, uint16_t length, bool last) {
uint16_t pos;
if(!sysex_rcv_buffer) {
//Start out with an empty buffer
pos = 0;
sysex_buf_size = length;
sysex_rcv_buffer = (uint8_t *)malloc(sysex_buf_size);
} else {
//Increase size of current buffer
pos = sysex_buf_size;
sysex_buf_size += length;
sysex_rcv_buffer = (uint8_t *)realloc(sysex_rcv_buffer, sysex_buf_size);
}
//Append this chunk to buffer
memcpy(sysex_rcv_buffer + pos, data, length);
//If it's the last one, call the regular handler to process it
if(last) {
handleSysex(sysex_rcv_buffer, sysex_buf_size);
//Discard the buffer
free(sysex_rcv_buffer);
sysex_rcv_buffer = NULL;
sysex_buf_size = 0;
}
}
void handleSysex(uint8_t *data, unsigned int length) {
//Note: Sysex data as received here contains sysex start and end markers (0xF0 and 0xF7)
//Too short to even contain a 3-byte vendor id is not for us.
if(length<4) return;
//Verify vendor
if(strncmp((char*)(data+1), sysex_id, 3)) return; //Silently ignore different vendor id
//Verify header. Min length is 3+5+3 bytes (vendor+header+message code)
if(length<12 || strncmp((char*)(data+4), "NuEVI", 5)) {
configShowMessage("Invalid message.");
sendSysexMessage("e00");
return;
}
//Get message code
char messageCode[3];
strncpy(messageCode, (char*)(data+9), 3);
if(!strncmp(messageCode, "c00", 3)) { //Config dump request
configShowMessage("Sending config...");
sendSysexSettings();
configShowMessage("Config sent.");
} else if(!strncmp(messageCode, "c03", 3)) { //Version info request
configShowMessage("Sending version.");
sendSysexVersion();
} else if(!strncmp(messageCode, "c02", 3)) { //New config incoming
configShowMessage("Receiving config...");
//Tell receiveSysexSettings about what's between sysex start and end markers
if(receiveSysexSettings(data+1, length-2)) configShowMessage("New config saved.");
} else {
configShowMessage("Unknown message.");
sendSysexMessage("e01"); //Unimplemented message code
}
}
void configModeSetup() {
statusLedFlash(500);
display.clearDisplay();
display.setCursor(0,0);
display.setTextColor(WHITE);
display.setTextSize(0);
display.println("Config mgmt");
display.println("Power off NuEVI");
display.println("to exit");
display.display();
usbMIDI.setHandleSystemExclusive(handleSysexChunk);
statusLedFlash(500);
sendSysexVersion(); //Friendly hello
configShowMessage("Ready.");
}
//"Main loop". Just sits and wait for midi messages and lets the sysex handler do all the work.
void configModeLoop() {
usbMIDI.read();
}

View file

@ -59,6 +59,7 @@
#define LEVEL_CC_ADDR 104
#define LEVEL_VAL_ADDR 106
#define EEPROM_SIZE 102
//DAC output modes
@ -125,12 +126,28 @@
#define LEVEL_CC_FACTORY 11
#define LEVEL_VAL_FACTORY 127
#define NO_CHECKSUM 0x7F007F00
void readEEPROM();
void readEEPROM(const bool factoryReset);
void setBit(uint16_t &bitfield, const uint8_t pos, const uint16_t value);
uint16_t readSetting(uint16_t address);
void writeSetting(uint16_t address, uint16_t value);
uint16_t readSettingBounded(uint16_t address, uint16_t min, uint16_t max, uint16_t defaultValue);
uint16_t readSetting(const uint16_t address);
void writeSetting(const uint16_t address, const uint16_t value);
uint16_t readSettingBounded(const uint16_t address, const uint16_t min, const uint16_t max, const uint16_t defaultValue);
//Functions for config management mode
void sendSysexSettings();
void sendSysexMessage(const char* messageCode);
void sendSysexVersion();
void handleSysex(uint8_t *data, unsigned int length);
void handleSysexChunk(const uint8_t *data, uint16_t length, bool last);
uint32_t crc32(const uint8_t *message, const size_t length);
void configInitScreen();
void configShowMessage(const char* message);
void configModeSetup();
void configModeLoop();
#endif

View file

@ -8,6 +8,7 @@
#include <inttypes.h>
#include "Wiring.h"
#include "simusbmidi.h"
#include "core_pins.h"
@ -15,7 +16,7 @@
#ifndef _BV
#define _BV(x) (1u<<(x))
#endif
#endif
// SPI CTRL REG BITS
#define SPIE 7
@ -72,20 +73,6 @@ public:
};
class SimUsbMidi
{
public:
void sendNoteOff(uint8_t note, uint8_t velocity, uint8_t channel, uint8_t cable=0);
void sendNoteOn(uint8_t note, uint8_t velocity, uint8_t channel, uint8_t cable=0);
void sendPolyPressure(uint8_t note, uint8_t pressure, uint8_t channel, uint8_t cable=0);
void sendAfterTouchPoly(uint8_t note, uint8_t pressure, uint8_t channel, uint8_t cable=0);
void sendControlChange(uint8_t control, uint8_t value, uint8_t channel, uint8_t cable=0);
void sendProgramChange(uint8_t program, uint8_t channel, uint8_t cable=0);
void sendAfterTouch(uint8_t pressure, uint8_t channel, uint8_t cable=0);
void sendPitchBend(int value, uint8_t channel, uint8_t cable=0);
void sendSysEx(uint16_t length, const uint8_t *data, bool hasTerm=false, uint8_t cable=0);
bool read(uint8_t channel=0);
};
extern SimSerial Serial;
extern SimSerial Serial3; //Used for MIDI serial putput with default hardware

View file

@ -0,0 +1,35 @@
#ifndef __SIMUSBMIDI_H__
#define __SIMUSBMIDI_H__
#include <string>
class SimUsbMidi
{
public:
void sendNoteOff(uint8_t note, uint8_t velocity, uint8_t channel, uint8_t cable=0);
void sendNoteOn(uint8_t note, uint8_t velocity, uint8_t channel, uint8_t cable=0);
void sendPolyPressure(uint8_t note, uint8_t pressure, uint8_t channel, uint8_t cable=0);
void sendAfterTouchPoly(uint8_t note, uint8_t pressure, uint8_t channel, uint8_t cable=0);
void sendControlChange(uint8_t control, uint8_t value, uint8_t channel, uint8_t cable=0);
void sendProgramChange(uint8_t program, uint8_t channel, uint8_t cable=0);
void sendAfterTouch(uint8_t pressure, uint8_t channel, uint8_t cable=0);
void sendPitchBend(int value, uint8_t channel, uint8_t cable=0);
void sendSysEx(uint16_t length, const uint8_t *data, bool hasTerm=false, uint8_t cable=0);
bool read(uint8_t channel=0);
void setHandleSystemExclusive(void (*fptr) (const uint8_t *array, unsigned int size));
void setHandleSystemExclusive(void (*fptr) (const uint8_t *data, uint16_t length, bool complete));
//Things not part of Teensy USBMidi, but used to simulate sending data to it
void receiveMidiData(const uint8_t *data, const uint16_t length); //Send midi data "into simulator"
void setMidiFile(std::string filename); //MIDI data to send to device
void triggerMidi(); //"Arm" so data is sent to device next time it tries to read anything
private:
//Handlers registered to receive MIDI
void (*usb_midi_handleSysExPartial)(const uint8_t *data, uint16_t length, uint8_t complete);
void (*usb_midi_handleSysExComplete)(const uint8_t *data, unsigned int size);
std::string midiFile;
bool sendMidi;
};
#endif

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,5 +1,6 @@
#include <functional>
#include <string>
#include <iostream>
#include <SDL2/SDL.h>
@ -13,15 +14,17 @@
#include "examples/imgui_impl_sdl.h"
#include "examples/imgui_impl_opengl3.h"
#include "EEPROM.h"
#include "simusbmidi.h"
#include <Arduino.h>
#define ARGS_NOEXCEPT
#include "args.hxx"
// Forward declarations
static void SimQuit(void);
static int SimInit(void);
static int SimRun(std::string eepromFile, bool eepromWrite, bool factoryReset);
static int SimRun(std::string eepromFile, bool eepromWrite, bool factoryReset, bool ConfigMode);
static void SimLoop(std::function<bool()>, std::function<void()>);
@ -39,6 +42,8 @@ static const int scale = 3;
static SDL_Window *window;
bool no_delay = false;
void _reboot_Teensyduino_()
{
// TODO: reboot
@ -81,6 +86,8 @@ uint8_t digitalRead(uint8_t pin) {
void delay(unsigned int ms)
{
if(no_delay) return;
uint32_t endTick = SDL_GetTicks() + ms;
auto checktime = [endTick]() -> bool { return endTick > SDL_GetTicks(); };
SimLoop(checktime,NULL);
@ -407,6 +414,9 @@ static void toggleAnalogAnimation() {
printf("Analog input variations: %s\n", animateAnalogs ? "ON": "OFF");
}
static void sendMidiData() {
usbMIDI.triggerMidi();
}
static void SimLoop(std::function<bool()> continue_predicate, std::function<void()> loopFunc)
{
@ -449,6 +459,7 @@ static void SimLoop(std::function<bool()> continue_predicate, std::function<void
case SDLK_UP: digitalInputs[uPin] = 1; break;
case SDLK_DOWN: digitalInputs[dPin] = 1; break;
case SDLK_w: toggleAnalogAnimation(); break;
case SDLK_m: sendMidiData(); break;
case SDLK_1: touchSensor.mockFilteredData(K1Pin, ctouchThrVal +100); break;
case SDLK_2: touchSensor.mockFilteredData(K2Pin, ctouchThrVal +100); break;
@ -508,7 +519,7 @@ static void SimLoop(std::function<bool()> continue_predicate, std::function<void
}
}
static int SimRun(std::string eepromFile, bool eepromWrite, bool factoryReset)
static int SimRun(std::string eepromFile, bool eepromWrite, bool factoryReset, bool configMode)
{
if( 0 != SimInit() ) { return 1; }
@ -523,11 +534,19 @@ static int SimRun(std::string eepromFile, bool eepromWrite, bool factoryReset)
digitalInputs[ePin] = 0;
}
//Go into config management mode. This should not happen (in NuEVI) if factory reset is done
if(configMode) {
digitalInputs[uPin] = 0;
digitalInputs[dPin] = 0;
}
setup();
//Let it go, let it go, not resetting any more
digitalInputs[mPin] = 1;
digitalInputs[ePin] = 1;
digitalInputs[uPin] = 1;
digitalInputs[dPin] = 1;
SimLoop( []() -> bool { return true; }, loop );
SimQuit();
@ -583,7 +602,7 @@ static int SimInit()
analogInputs[vMeterPin] = 3025;
// Initialize touch sensors to not be poked
// Initialize touch sensors to not be poked
for(int i = 0; i < 12; ++i) {
touchSensor.mockFilteredData(i, 4095);
}
@ -610,23 +629,47 @@ static void SimQuit()
int main(int argc, const char** argv)
{
args::ArgumentParser parser("This is a test program.", "This goes after the options.");
args::ArgumentParser parser("NuEVI simulator.");
args::ValueFlag<std::string> eepromFile(parser, "eeprom-write", "File to use for EEPROM data", {'e', "eeprom-file"});
args::HelpFlag help(parser, "help", "Display this help menu", {'h', "help"});
args::ValueFlag<std::string> eepromFile(parser, "filename", "File to use for EEPROM data", {'e', "eeprom-file"});
args::Flag eepromWrite(parser, "eeprom-write", "Write EEPROM changes to file", {'w', "eeprom-write"});
args::Flag factoryReset(parser, "factory-reset", "Trigger factory reset", {'r', "factory-reset"});
args::Flag configMode(parser, "config-mode", "Trigger config-management mode", {'c', "config-mode"});
args::Flag nodelay(parser, "nodelay", "Skip all delays when running", {'n', "nodelay"});
args::ValueFlag<std::string> midiFile(parser, "filename", "Name of file with raw MIDI data to send to NuEVI", {'m', "midi-file"});
args::Flag midiSend(parser, "midi-send", "Trigger automatic sending of MIDI data", {'M', "midi-send"});
parser.ParseCLI(argc, argv);
if(parser.GetError() != args::Error::None) {
if(parser.GetError() == args::Error::Help) {
std::cout << parser << std::endl;
return 0;
}
std::cerr << parser.GetErrorMsg() << std::endl;
return 1;
}
std::string eepromFileName = args::get(eepromFile);
std::string midiFileName = args::get(midiFile);
//Use a default EEPROM file if none is provided.
if(eepromFileName.length()==0)
{
if(eepromFileName.length()==0) {
eepromFileName = SDL_GetPrefPath("Vulk Data System", "NuEVI Simulator");
eepromFileName += "eeprom.bin";
}
return SimRun(eepromFileName, args::get(eepromWrite), args::get(factoryReset));
if(midiFileName.length()>0) {
usbMIDI.setMidiFile(midiFileName);
if(args::get(midiSend)) {
usbMIDI.triggerMidi();
}
}
no_delay = args::get(nodelay);
return SimRun(eepromFileName, args::get(eepromWrite), args::get(factoryReset), args::get(configMode));
}

View file

@ -63,7 +63,7 @@ int16_t EEPROMClass::setStorage(const char* filename, bool write)
autoUpdate = write;
storage = fopen(filename, "rb");
//If only reading, fail if file does not exist (makes no sense otherwise)
if(!storage && !autoUpdate) {

View file

@ -1,14 +1,15 @@
#include <cstdint>
#include <cstdio>
#include <iostream>
#include <fstream>
#include "Arduino.h"
#include "simusbmidi.h"
/*************************************
* Stub simulation of Teensy usbMidi
*/
void SimUsbMidi::sendNoteOff(uint8_t note, uint8_t velocity, uint8_t channel, uint8_t __unused cable)
{
printf( "[usbMIDI::noteOff] note %03d vel %03d ch %02d\n", note, velocity, channel);
@ -49,12 +50,96 @@ void SimUsbMidi::sendPitchBend(int value, uint8_t channel, uint8_t __unused cabl
printf( "[usbMIDI::pitchBend] pb %05d ch %02d\n", value, channel);
}
void SimUsbMidi::sendSysEx(uint16_t length, const uint8_t __unused *data, bool __unused hasTerm, uint8_t __unused cable)
void SimUsbMidi::sendSysEx(uint16_t length, const uint8_t *data, bool __unused hasTerm, uint8_t __unused cable)
{
printf( "[usbMIDI::sysEx] len %d\n", length);
printf( "[usbMIDI::sysEx] Sending %d bytes\n", length);
for(int i=0; i<length; i++) {
printf("%02x%c", data[i], (i==length-1)?'\n':':');
}
}
//Set a low chunk size on purpose just to let the receiver work for it
#define MIDI_SYSEX_CHUNK_SIZE 32
bool SimUsbMidi::read(uint8_t __unused channel) {
if(this->sendMidi) {
printf("[SimUsbMidi::read] Attempting to send midi data\n");
std::ifstream file(this->midiFile, std::ios::binary | std::ios::ate);
std::streamsize size = file.tellg();
file.seekg(0, std::ios::beg);
uint8_t *buffer = (uint8_t*)malloc(size);
if (file.read((char*)buffer, size))
{
printf("[SimUsbMidi::read] Sending %lu bytes.\n", size);
this->receiveMidiData(buffer, size);
}
free(buffer);
this->sendMidi = false;
}
return false;
}
//Provide midi data for simulation to receive
void SimUsbMidi::receiveMidiData(const uint8_t *data, const uint16_t length) {
if(length==0) return; //There is no data, what's even the point
uint8_t midi_message = data[0]; //First byte of data
if(midi_message != 0xF0) return; //Only sysex data supported (no other handlers available)
if(this->usb_midi_handleSysExPartial) {
//Chunked sysex receiver set, use that.
if(length<=MIDI_SYSEX_CHUNK_SIZE) {
//Send all in one go
printf( "[SimUsbMidi::receiveMidiData] usb_midi_handleSysExPartial(complete) %d B\n", length);
(this->usb_midi_handleSysExPartial)(data, length, true);
} else {
uint8_t* buf = (uint8_t*)malloc(MIDI_SYSEX_CHUNK_SIZE);
int pos=0;
while(pos<length) {
int remaining = length-pos;
int bytesToSend = std::min(remaining, MIDI_SYSEX_CHUNK_SIZE);
bool complete = (bytesToSend == remaining);
memcpy(buf, data+pos, bytesToSend);
printf( "[SimUsbMidi::receiveMidiData] usb_midi_handleSysExPartial(complete: %d) %d B\n", complete, bytesToSend);
(this->usb_midi_handleSysExPartial)(buf, bytesToSend, complete);
pos=pos+bytesToSend;
}
free(buf);
}
} else if(this->usb_midi_handleSysExComplete) {
printf( "[SimUsbMidi::receiveMidiData] usb_midi_handleSysExComplete() %d B\n", length);
(this->usb_midi_handleSysExComplete)(data, length);
} else {
//Nobody listening
}
}
//MIDI SysEx handlers. Choice of data types is a bit odd, but done to match Arduino/Teensy libraries
void SimUsbMidi::setHandleSystemExclusive(void (*fptr) (const uint8_t *array, unsigned int size)) {
this->usb_midi_handleSysExComplete = fptr;
}
//"Chunked" sysex handler (teensy extension), for large messages
void SimUsbMidi::setHandleSystemExclusive(void (*fptr) (const uint8_t *array, uint16_t size, bool last)) {
this->usb_midi_handleSysExPartial = (void (*)(const uint8_t *, uint16_t, uint8_t))fptr;
}
void SimUsbMidi::setMidiFile(std::string filename) {
this->midiFile = filename;
}
void SimUsbMidi::triggerMidi() {
this->sendMidi = true;
}