This program enables a RedBearlab nRF51822 to be used as a "bridge device" with full bi-directional communication between MIDI through standard MIDI cables and BLE-MIDI. MIDI cables are connected to the UART. This allows for example to send MIDI data to synthesizers directly from a computer or a tablet via BLE-MIDI and vice-versa, "wires-free" . The Midi Manufacturers Association BLE-MIDI Specification is used. This project is inspired by Matthias Frick's "blidino" project which implements a USB-MIDI to BLE-MIDI bridge with the nRF51822 and is available at https://github.com/sieren/blidino. I owe to him all the BLE-MIDI to MIDI parsing part.
Dependencies: BLE_API BufferedSerial mbed nRF51822
Video
Revision 0:244f1d0a3810, committed 2016-08-09
- Comitter:
- popcornell
- Date:
- Tue Aug 09 12:57:23 2016 +0000
- Commit message:
- first
Changed in this revision
diff -r 000000000000 -r 244f1d0a3810 BLEMIDI_MIDI_Parser.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/BLEMIDI_MIDI_Parser.h Tue Aug 09 12:57:23 2016 +0000 @@ -0,0 +1,534 @@ +/* + * Copyright (c) 2014 Matthias Frick + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +#ifndef ____BLE_MIDI_Parser__ +#define ____BLE_MIDI_Parser__ +#include <stdio.h> +#include <stdbool.h> +#include <string.h> + + + + + + const int kMaxBufferSize=256; // max lenght for sysex buffer size + + uint8_t midiBuffer[3]; + uint8_t sysExBuffer[kMaxBufferSize]; + uint8_t alterSysExBuffer[kMaxBufferSize]; + int midiBufferPtr ; // int midiBufferPtr = 0; + int sysExRecBufferPtr ; // int sysExRecBufferPtr = 0; + int sysExBufferPtr ; // int sysExBufferPtr = 0; + + + // MIDI event messages, state & stamps + int midiEventKind; + int midiEventNote; + int midiEventVelocity; + int midiState; //int midiState = MIDI_STATE_TIMESTAMP; + int timestamp; + + + bool useTimestamp ; // bool useTimestamp = true; + + int lastTimestamp; + long lastTimestampRecorded ; // long lastTimestampRecorded = 0; + int zeroTimestampCount; // int zeroTimestampCount = 0; + //Receiver *midiRecv; + + + + +//////////////////////////////////// + + + static int MIDI_STATE_TIMESTAMP = 0; + static int MIDI_STATE_WAIT = 1; + static int MIDI_STATE_SIGNAL_2BYTES_2 = 21; + static int MIDI_STATE_SIGNAL_3BYTES_2 = 31; + static int MIDI_STATE_SIGNAL_3BYTES_3 = 32; + static int MIDI_STATE_SIGNAL_SYSEX = 41; + + // for Timestamp + static int MAX_TIMESTAMP = 8192; + static int BUFFER_LENGTH_MILLIS = 10; + + + // for RPN/NRPN messages + static int PARAMETER_MODE_NONE = 0; + static int PARAMETER_MODE_RPN = 1; + static int PARAMETER_MODE_NRPN = 2; + int parameterMode = 0; + int parameterNumber = 0x3fff; + int parameterValue = 0x3fff; + + void addByteToMidiBuffer(uint8_t midiEvent) + { + midiBuffer[midiBufferPtr] = midiEvent; + midiBufferPtr++; + } + + void addByteToSysExBuffer(uint8_t midiEvent) + { + sysExBuffer[sysExBufferPtr] = midiEvent; + sysExBufferPtr++; + } + + uint8_t replaceLastByteInSysExBuffer(uint8_t midiEvent) + { + sysExBufferPtr--; + uint8_t lastEvt = sysExBuffer[sysExBufferPtr]; + sysExBuffer[sysExBufferPtr] = midiEvent; + sysExBufferPtr++; + return lastEvt; + } + + void sendSysex() + { + + for(int i = 0 ; i<=sysExBufferPtr; i++ ) { // send sysex message on the UART + UART.putc(sysExBuffer[i]) ; + } + //midiRecv->SendSysEx(sysExBuffer, sysExBufferPtr, 0); + } + + void createSysExRecovery() + { + sysExRecBufferPtr = sysExBufferPtr; + memcpy(alterSysExBuffer, sysExBuffer, sysExBufferPtr); + } + + void sendSysexRecovery() + { + + for(int i = 0 ; i<=sysExRecBufferPtr; i++ ) { + UART.putc(alterSysExBuffer[i]) ; + } + // midiRecv->SendSysEx(alterSysExBuffer, sysExRecBufferPtr, 0); + } + + uint8_t replaceLastByteInRecoveryBuffer(uint8_t midiEvent) + { + sysExRecBufferPtr--; + uint8_t lastEvt = alterSysExBuffer[sysExRecBufferPtr]; + alterSysExBuffer[sysExRecBufferPtr] = midiEvent; + sysExRecBufferPtr++; + return lastEvt; + } + + void addByteToRecoveryBuffer(uint8_t midiEvent) + { + alterSysExBuffer[sysExRecBufferPtr] = midiEvent; + sysExRecBufferPtr++; + } + + void resetMidiBuffer() + { + memset(&midiBuffer[0], 0, sizeof(midiBuffer)); + midiBufferPtr = 0; + } + + void resetSysExBuffer() + { + memset(&sysExBuffer[0], 0, kMaxBufferSize); + sysExBufferPtr = 0; + } + + void resetRecoveryBuffer() + { + memset(&alterSysExBuffer[0], 0, sizeof(alterSysExBuffer)); + sysExRecBufferPtr = 0; + } + + void sendMidi(uint8_t size) // send MIDI Message on the UART + { + for(int i = 0 ; i<=size ; i++ ) { + UART.putc(midiBuffer[i]) ; + } + + } + + + + void parseMidiEvent(uint8_t header, const uint8_t event) + { + uint8_t midiEvent = event & 0xff; + + // printf((char*)midiEvent); + if (midiState == MIDI_STATE_TIMESTAMP) + { + // printf("Timestamp"); + if ((midiEvent & 0x80) == 0) + { + // running status + midiState = MIDI_STATE_WAIT; + } + + if (midiEvent == 0xf7) + { + // make sure this is the end of sysex + // and send alternative recovery stream + if (sysExRecBufferPtr > 0) + { + uint8_t removed = replaceLastByteInRecoveryBuffer(midiEvent); + sendSysexRecovery(); + resetRecoveryBuffer(); + } + midiState = MIDI_STATE_TIMESTAMP; + return; + } + else + { + // reset alternative sysex stream + resetRecoveryBuffer(); + } + } // end of timestamp + + if (midiState == MIDI_STATE_TIMESTAMP) + { + timestamp = ((header & 0x3f) << 7) | (midiEvent & 0x7f); + midiState = MIDI_STATE_WAIT; + } + else if (midiState == MIDI_STATE_WAIT) + { + switch (midiEvent & 0xf0) { + case 0xf0: { + switch (midiEvent) { + case 0xf0: + resetRecoveryBuffer(); + resetSysExBuffer(); + addByteToSysExBuffer(midiEvent); + midiState = MIDI_STATE_SIGNAL_SYSEX; + break; + case 0xf1: + case 0xf3: + // 0xf1 MIDI Time Code Quarter Frame. : 2bytes + // 0xf3 Song Select. : 2bytes + midiEventKind = midiEvent; + addByteToMidiBuffer(midiEvent); + midiState = MIDI_STATE_SIGNAL_2BYTES_2; + break; + case 0xf2: + // 0xf2 Song Position Pointer. : 3bytes + midiEventKind = midiEvent; + addByteToMidiBuffer(midiEvent); + midiState = MIDI_STATE_SIGNAL_3BYTES_2; + break; + case 0xf6: + // 0xf6 Tune Request : 1byte + addByteToMidiBuffer(midiEvent); + midiState = MIDI_STATE_TIMESTAMP; + break; + case 0xf8: + // 0xf8 Timing Clock : 1byte + //#pragma mark send timeclock // no on mbed OS + midiState = MIDI_STATE_TIMESTAMP; + break; + case 0xfa: + // 0xfa Start : 1byte + midiState = MIDI_STATE_TIMESTAMP; + break; + case 0xfb: + // 0xfb Continue : 1byte + midiState = MIDI_STATE_TIMESTAMP; + break; + case 0xfc: + // 0xfc Stop : 1byte + midiState = MIDI_STATE_TIMESTAMP; + break; + case 0xfe: + // 0xfe Active Sensing : 1byte + midiState = MIDI_STATE_TIMESTAMP; + break; + case 0xff: + // 0xff Reset : 1byte + midiState = MIDI_STATE_TIMESTAMP; + break; + + default: + break; + } + } + break; + case 0x80: + case 0x90: + case 0xa0: + case 0xb0: + case 0xe0: + // 3bytes pattern + midiEventKind = midiEvent; + midiState = MIDI_STATE_SIGNAL_3BYTES_2; + break; + case 0xc0: // program change + case 0xd0: // channel after-touch + // 2bytes pattern + midiEventKind = midiEvent; + midiState = MIDI_STATE_SIGNAL_2BYTES_2; + break; + default: + // 0x00 - 0x70: running status + if ((midiEventKind & 0xf0) != 0xf0) { + // previous event kind is multi-bytes pattern + midiEventNote = midiEvent; + midiState = MIDI_STATE_SIGNAL_3BYTES_3; + } + break; + } + } + else if (midiState == MIDI_STATE_SIGNAL_2BYTES_2) + { + switch (midiEventKind & 0xf0) + { + // 2bytes pattern + case 0xc0: // program change + midiEventNote = midiEvent; + midiState = MIDI_STATE_TIMESTAMP; + break; + case 0xd0: // channel after-touch + midiEventNote = midiEvent; + midiState = MIDI_STATE_TIMESTAMP; + break; + case 0xf0: + { + switch (midiEventKind) + { + case 0xf1: + // 0xf1 MIDI Time Code Quarter Frame. : 2bytes + midiEventNote = midiEvent; + addByteToMidiBuffer(midiEventNote); + sendMidi(2); + resetMidiBuffer(); + midiState = MIDI_STATE_TIMESTAMP; + break; + case 0xf3: + // 0xf3 Song Select. : 2bytes + midiEventNote = midiEvent; + addByteToMidiBuffer(midiEventNote); + sendMidi(2); + resetMidiBuffer(); + midiState = MIDI_STATE_TIMESTAMP; + break; + default: + // illegal state + midiState = MIDI_STATE_TIMESTAMP; + break; + } + } + break; + default: + // illegal state + midiState = MIDI_STATE_TIMESTAMP; + break; + } + } + else if (midiState == MIDI_STATE_SIGNAL_3BYTES_2) + { + switch (midiEventKind & 0xf0) + { + case 0x80: + case 0x90: + case 0xa0: + case 0xb0: + case 0xe0: + case 0xf0: + // 3bytes pattern + midiEventNote = midiEvent; + midiState = MIDI_STATE_SIGNAL_3BYTES_3; + break; + default: + // illegal state + midiState = MIDI_STATE_TIMESTAMP; + break; + } + } + else if (midiState == MIDI_STATE_SIGNAL_3BYTES_3) + { + switch (midiEventKind & 0xf0) + { + // 3bytes pattern + case 0x80: // note off + + midiEventVelocity = midiEvent; + addByteToMidiBuffer(midiEventKind); + addByteToMidiBuffer(midiEventNote); + addByteToMidiBuffer(midiEventVelocity); + sendMidi(3); + resetMidiBuffer(); + midiState = MIDI_STATE_TIMESTAMP; + break; + case 0x90: // note on + midiEventVelocity = midiEvent; + //timeToWait = calculateTimeToWait(timestamp); + + addByteToMidiBuffer(midiEventKind); + addByteToMidiBuffer(midiEventNote); + addByteToMidiBuffer(midiEventVelocity); + sendMidi(3); + resetMidiBuffer(); + midiState = MIDI_STATE_TIMESTAMP; + break; + case 0xa0: // control polyphonic key pressure + midiEventVelocity = midiEvent; + addByteToMidiBuffer(midiEventKind); + addByteToMidiBuffer(midiEventNote); + addByteToMidiBuffer(midiEventVelocity); + sendMidi(3); + resetMidiBuffer(); + midiState = MIDI_STATE_TIMESTAMP; + break; + case 0xb0: // control change + midiEventVelocity = midiEvent; + switch (midiEventNote & 0x7f) + { + case 98: + // NRPN LSB + parameterNumber &= 0x3f80; + parameterNumber |= midiEventVelocity & 0x7f; + parameterMode = PARAMETER_MODE_NRPN; + break; + case 99: + // NRPN MSB + parameterNumber &= 0x007f; + parameterNumber |= (midiEventVelocity & 0x7f) << 7; + parameterMode = PARAMETER_MODE_NRPN; + break; + case 100: + // RPN LSB + parameterNumber &= 0x3f80; + parameterNumber |= midiEventVelocity & 0x7f; + parameterMode = PARAMETER_MODE_RPN; + break; + case 101: + // RPN MSB + parameterNumber &= 0x007f; + parameterNumber |= (midiEventVelocity & 0x7f) << 7; + parameterMode = PARAMETER_MODE_RPN; + break; + case 38: + // data LSB + parameterValue &= 0x3f80; + parameterValue |= midiEventVelocity & 0x7f; + + if (parameterNumber != 0x3fff) { + if (parameterMode == PARAMETER_MODE_RPN) + { + addByteToMidiBuffer(midiEventKind); + addByteToMidiBuffer(parameterNumber); + addByteToMidiBuffer(parameterValue); + sendMidi(3); + resetMidiBuffer(); + } + else if (parameterMode == PARAMETER_MODE_NRPN) + { + addByteToMidiBuffer(midiEventKind); + addByteToMidiBuffer(parameterNumber); + addByteToMidiBuffer(parameterValue); + sendMidi(3); + resetMidiBuffer(); + } + } + break; + case 6: + // data MSB + parameterValue &= 0x007f; + parameterValue |= (midiEventVelocity & 0x7f) << 7; + + if (parameterNumber != 0x3fff) + { + if (parameterMode == PARAMETER_MODE_RPN) + { + addByteToMidiBuffer(midiEventKind); + addByteToMidiBuffer(parameterNumber); + addByteToMidiBuffer(parameterValue); + sendMidi(3); + resetMidiBuffer(); + } + else if (parameterMode == PARAMETER_MODE_NRPN) + { + addByteToMidiBuffer(midiEventKind); + addByteToMidiBuffer(parameterNumber); + addByteToMidiBuffer(parameterValue); + sendMidi(3); + resetMidiBuffer(); + } + } + break; + default: + // do nothing + break; + } + addByteToMidiBuffer(midiEventKind); + addByteToMidiBuffer(midiEventNote); + addByteToMidiBuffer(midiEventVelocity); + sendMidi(3); + resetMidiBuffer(); + midiState = MIDI_STATE_TIMESTAMP; + break; + case 0xe0: // pitch bend + midiEventVelocity = midiEvent; + addByteToMidiBuffer(midiEventKind); + addByteToMidiBuffer(midiEventNote); + addByteToMidiBuffer(midiEventVelocity); + sendMidi(3); + resetMidiBuffer(); + midiState = MIDI_STATE_TIMESTAMP; + break; + case 0xf0: // Song Position Pointer. + midiEventVelocity = midiEvent; + addByteToMidiBuffer(midiEventKind); + addByteToMidiBuffer(midiEventNote); + addByteToMidiBuffer(midiEventVelocity); + sendMidi(3); + resetMidiBuffer(); + midiState = MIDI_STATE_TIMESTAMP; + break; + default: + // illegal state + midiState = MIDI_STATE_TIMESTAMP; + break; + } + } + else if (midiState == MIDI_STATE_SIGNAL_SYSEX) + { + if (midiEvent == 0xf7) + { + uint8_t repEvt = replaceLastByteInSysExBuffer(midiEvent); + + resetRecoveryBuffer(); + createSysExRecovery(); + replaceLastByteInRecoveryBuffer(repEvt); + addByteToRecoveryBuffer(midiEvent); + sendSysex(); + resetSysExBuffer(); + midiState = MIDI_STATE_TIMESTAMP; + } + else + { + addByteToSysExBuffer(midiEvent); + } + + } + } + + +#endif /* defined(____BLEParser__) */ \ No newline at end of file
diff -r 000000000000 -r 244f1d0a3810 BLE_API.lib --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/BLE_API.lib Tue Aug 09 12:57:23 2016 +0000 @@ -0,0 +1,1 @@ +http://mbed.org/teams/Bluetooth-Low-Energy/code/BLE_API/#d494ad3e87bd
diff -r 000000000000 -r 244f1d0a3810 BufferedSerial.lib --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/BufferedSerial.lib Tue Aug 09 12:57:23 2016 +0000 @@ -0,0 +1,1 @@ +http://mbed.org/users/sam_grove/code/BufferedSerial/#a0d37088b405
diff -r 000000000000 -r 244f1d0a3810 MIDI_BLEMIDI_Parser.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MIDI_BLEMIDI_Parser.h Tue Aug 09 12:57:23 2016 +0000 @@ -0,0 +1,473 @@ +/* + * Original work Copyright (c) 2015 Francois Best + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + + +/* +* Modified work by Samuele Cornell. +* The following code is adapted from Francois Best's Arduino MIDI Library v4.2 +* (see https://github.com/FortySevenEffects/arduino_midi_library) +*/ + + + +namespace MIDI { + +#define SysExMaxSize 128 +#define Use1ByteParsing 0 + +typedef uint8_t byte ; // byte isn't defined in mbed OS + +typedef uint8_t StatusByte; +typedef uint8_t DataByte; +typedef uint8_t Channel; + + + +/*! Enumeration of MIDI types */ +enum MidiType +{ + InvalidType = 0x00, ///< For notifying errors + NoteOff = 0x80, ///< Note Off + NoteOn = 0x90, ///< Note On + AfterTouchPoly = 0xA0, ///< Polyphonic AfterTouch + ControlChange = 0xB0, ///< Control Change / Channel Mode + ProgramChange = 0xC0, ///< Program Change + AfterTouchChannel = 0xD0, ///< Channel (monophonic) AfterTouch + PitchBend = 0xE0, ///< Pitch Bend + SystemExclusive = 0xF0, ///< System Exclusive + TimeCodeQuarterFrame = 0xF1, ///< System Common - MIDI Time Code Quarter Frame + SongPosition = 0xF2, ///< System Common - Song Position Pointer + SongSelect = 0xF3, ///< System Common - Song Select + TuneRequest = 0xF6, ///< System Common - Tune Request + Clock = 0xF8, ///< System Real Time - Timing Clock + Start = 0xFA, ///< System Real Time - Start + Continue = 0xFB, ///< System Real Time - Continue + Stop = 0xFC, ///< System Real Time - Stop + ActiveSensing = 0xFE, ///< System Real Time - Active Sensing + SystemReset = 0xFF, ///< System Real Time - System Reset +}; + + +struct MidiMessage +{ + /*! The maximum size for the System Exclusive array. + */ + static const unsigned sSysExMaxSize = SysExMaxSize; // this + + /*! The MIDI channel on which the message was recieved. + \n Value goes from 1 to 16. + */ + Channel channel; + + /*! The type of the message + (see the MidiType enum for types reference) + */ + MidiType type; + + /*! The first data byte. + \n Value goes from 0 to 127. + */ + DataByte data1; + + /*! The second data byte. + If the message is only 2 bytes long, this one is null. + \n Value goes from 0 to 127. + */ + DataByte data2; + + /*! System Exclusive dedicated byte array. + \n Array length is stocked on 16 bits, + in data1 (LSB) and data2 (MSB) + */ + DataByte sysexArray[sSysExMaxSize]; + + /*! This boolean indicates if the message is valid or not. + There is no channel consideration here, + validity means the message respects the MIDI norm. + */ + bool valid; + + inline unsigned getSysExSize() const + { + const unsigned size = unsigned(data2) << 8 | data1; + return size > sSysExMaxSize ? sSysExMaxSize : size; + } + + MidiMessage() { // initialize + + channel=0; + type= InvalidType ; + data1= 0 ; + data2= 0 ; + valid =false ; + } + + +}; + + + StatusByte mRunningStatus_RX; + StatusByte mRunningStatus_TX; + Channel mInputChannel= 1 ; // default 1 + uint8_t mPendingMessage[3]; + uint8_t mPendingMessageExpectedLenght = 0; + uint8_t mPendingMessageIndex= 0; + + + + MidiMessage mMessage; + + + +inline void resetInput() +{ + mPendingMessageIndex = 0; + mPendingMessageExpectedLenght = 0; + mRunningStatus_RX = InvalidType; +} + +MidiType getTypeFromStatusByte(uint8_t inStatus) +{ + if ((inStatus < 0x80) || + (inStatus == 0xf4) || + (inStatus == 0xf5) || + (inStatus == 0xf9) || + (inStatus == 0xfD)) + { + // Data bytes and undefined. + return InvalidType; + } + if (inStatus < 0xf0) + { + // Channel message, remove channel nibble. + return MidiType(inStatus & 0xf0); + } + + return MidiType(inStatus); +} + +/*! \brief Returns channel in the range 1-16 + */ + + inline Channel getChannelFromStatusByte(uint8_t inStatus) + { + return (inStatus & 0x0f) + 1; + } + + + bool isChannelMessage(MidiType inType) + { + return (inType == NoteOff || + inType == NoteOn || + inType == ControlChange || + inType == AfterTouchPoly || + inType == AfterTouchChannel || + inType == PitchBend || + inType == ProgramChange); + } + + + +bool MIDI_to_BLEMIDI_Parser (void ) { + + + if (UART.readable() == 0) + // No data available. + return false; + + // Parsing algorithm: + // Get a byte from the serial buffer. + // If there is no pending message to be recomposed, start a new one. + // - Find type and channel (if pertinent) + // - Look for other bytes in buffer, call parser recursively, + // until the message is assembled or the buffer is empty. + // Else, add the extracted byte to the pending message, and check validity. + // When the message is done, store it. + + const uint8_t extracted = UART.getc(); + + if (mPendingMessageIndex == 0) + { + // Start a new pending message + mPendingMessage[0] = extracted; + + // Check for running status first + if (isChannelMessage(getTypeFromStatusByte(mRunningStatus_RX))) + { + // Only these types allow Running Status + + // If the status byte is not received, prepend it + // to the pending message + if (extracted < 0x80) + { + mPendingMessage[0] = mRunningStatus_RX; + mPendingMessage[1] = extracted; + mPendingMessageIndex = 1; + } + // Else: well, we received another status byte, + // so the running status does not apply here. + // It will be updated upon completion of this message. + } + + switch (getTypeFromStatusByte(mPendingMessage[0])) + { + // 1 byte messages + case Start: + case Continue: + case Stop: + case Clock: + case ActiveSensing: + case SystemReset: + case TuneRequest: + + // Handle the message type directly here. + mMessage.type = getTypeFromStatusByte(mPendingMessage[0]); + mMessage.channel = 0; + mMessage.data1 = 0; + mMessage.data2 = 0; + mMessage.valid = true; + + // \fix Running Status broken when receiving Clock messages. + // Do not reset all input attributes, Running Status must remain unchanged. + //resetInput(); + + // We still need to reset these + mPendingMessageIndex = 0; + mPendingMessageExpectedLenght = 0; + + return true; + break; + + // 2 bytes messages + case ProgramChange: + case AfterTouchChannel: + case TimeCodeQuarterFrame: + case SongSelect: + mPendingMessageExpectedLenght = 2; + break; + + // 3 bytes messages + case NoteOn: + case NoteOff: + case ControlChange: + case PitchBend: + case AfterTouchPoly: + case SongPosition: + mPendingMessageExpectedLenght = 3; + break; + + case SystemExclusive: + // The message can be any lenght + // between 3 and MidiMessage::sSysExMaxSize bytes + mPendingMessageExpectedLenght = MidiMessage::sSysExMaxSize; + mRunningStatus_RX = InvalidType; + mMessage.sysexArray[0] = SystemExclusive; + break; + + case InvalidType: + default: + // This is obviously wrong. Let's get the hell out'a here. + resetInput(); + return false; + break; + } + + if (mPendingMessageIndex >= (mPendingMessageExpectedLenght - 1)) + { + // Reception complete + mMessage.type = getTypeFromStatusByte(mPendingMessage[0]); + mMessage.channel = getChannelFromStatusByte(mPendingMessage[0]); + mMessage.data1 = mPendingMessage[1]; + + // Save data2 only if applicable + if (mPendingMessageExpectedLenght == 3) + mMessage.data2 = mPendingMessage[2]; + else + mMessage.data2 = 0; + + mPendingMessageIndex = 0; + mPendingMessageExpectedLenght = 0; + mMessage.valid = true; + return true; + } + else + { + // Waiting for more data + mPendingMessageIndex++; + } + + if (Use1ByteParsing==1) + { + // Message is not complete. + return false; + } + else + { + // Call the parser recursively + // to parse the rest of the message. + return MIDI_to_BLEMIDI_Parser(); + } + } + else + { + // First, test if this is a status byte + if (extracted >= 0x80) + { + // Reception of status bytes in the middle of an uncompleted message + // are allowed only for interleaved Real Time message or EOX + switch (extracted) + { + case Clock: + case Start: + case Continue: + case Stop: + case ActiveSensing: + case SystemReset: + + // Here we will have to extract the one-byte message, + // pass it to the structure for being read outside + // the MIDI class, and recompose the message it was + // interleaved into. Oh, and without killing the running status.. + // This is done by leaving the pending message as is, + // it will be completed on next calls. + + mMessage.type = (MidiType)extracted; + mMessage.data1 = 0; + mMessage.data2 = 0; + mMessage.channel = 0; + mMessage.valid = true; + return true; + + break; + + // End of Exclusive + case 0xf7: + if (mMessage.sysexArray[0] == SystemExclusive) + { + // Store the last byte (EOX) + mMessage.sysexArray[mPendingMessageIndex++] = 0xf7; + mMessage.type = SystemExclusive; + + // Get length + mMessage.data1 = mPendingMessageIndex & 0xff; // LSB + mMessage.data2 = mPendingMessageIndex >> 8; // MSB + mMessage.channel = 0; + mMessage.valid = true; + + resetInput(); + return true; + } + else + { + // Well well well.. error. + resetInput(); + return false; + } + + break; + default: + break; + } + } + + // Add extracted data byte to pending message + if (mPendingMessage[0] == SystemExclusive) + mMessage.sysexArray[mPendingMessageIndex] = extracted; + else + mPendingMessage[mPendingMessageIndex] = extracted; + + // Now we are going to check if we have reached the end of the message + if (mPendingMessageIndex >= (mPendingMessageExpectedLenght - 1)) + { + // "FML" case: fall down here with an overflown SysEx.. + // This means we received the last possible data byte that can fit + // the buffer. If this happens, try increasing MidiMessage::sSysExMaxSize. + if (mPendingMessage[0] == SystemExclusive) + { + resetInput(); + return false; + } + + mMessage.type = getTypeFromStatusByte(mPendingMessage[0]); + + if (isChannelMessage(mMessage.type)) + mMessage.channel = getChannelFromStatusByte(mPendingMessage[0]); + else + mMessage.channel = 0; + + mMessage.data1 = mPendingMessage[1]; + + // Save data2 only if applicable + if (mPendingMessageExpectedLenght == 3) + mMessage.data2 = mPendingMessage[2]; + else + mMessage.data2 = 0; + + // Reset local variables + mPendingMessageIndex = 0; + mPendingMessageExpectedLenght = 0; + + mMessage.valid = true; + + // Activate running status (if enabled for the received type) + switch (mMessage.type) + { + case NoteOff: + case NoteOn: + case AfterTouchPoly: + case ControlChange: + case ProgramChange: + case AfterTouchChannel: + case PitchBend: + // Running status enabled: store it from received message + mRunningStatus_RX = mPendingMessage[0]; + break; + + default: + // No running status + mRunningStatus_RX = InvalidType; + break; + } + return true; + } + else + { + // Then update the index of the pending message. + mPendingMessageIndex++; + + if (Use1ByteParsing==1) + { + // Message is not complete. + return false; + } + else + { + // Call the parser recursively to parse the rest of the message. + return MIDI_to_BLEMIDI_Parser(); + } + } + } +} + + +} // end namespace \ No newline at end of file
diff -r 000000000000 -r 244f1d0a3810 config.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/config.h Tue Aug 09 12:57:23 2016 +0000 @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2016 Samuele Cornell + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +#define UART_RX_PIN D0 // RX UART Pin + +#define UART_TX_PIN D1 // TX UART Pin + +// i have used DFRobot MIDI Shield but there are other commercially available similar products, also it is possible to interface directly a MIDI cable via an optocoupler (several projects are already available online ) + +#define LIGHT_SHOW 1 // 0 if visual feedback isn't needed (saves energy ) + +#define TX_LED D6 // the state of this led will change whenever a message is to be sent over ble-midi + +#define RX_LED D7 // the state of this led will change whenever a message received over ble-midi + + +/******************************************************************************************************* +PERFORMANCE TWEAKS +*******************************************************************************************************/ + + +#define BUFSERIAL_LENGHT 256 // define the software circular buffer lenght used for the UART. + + +#define ONLY_MIDI_to_BLEMIDI 0 // if only unidirectional MIDI to BLE-MIDI is desired set this to 1 + + +#define ONLY_BLEMIDI_to_MIDI 0 // if only unidirectional BLE-MIDI to MIDI is desired set this to 1 + +// unidirectional operation allows to save energy. It also leads to better performance as if ONLY MIDI to BLE MIDI is required, for example it is possible to shorten the SENDBLE_INTERVAL +// without reliability issues (to a certain extent). + +#define SENDBLEMIDI_INTERVAL 0.01 // this defines how frequently MIDI Events from the UART are polled, parsed and then sent via BLE-MIDI. + // a lower value means less latency but it also increase energy comsuption and if is set too low can cause reliability issues in MIDI to BLE-MIDI operation (especially for long SysEx messages). + + +/*************************************************************************************************************** +CONNECTION PARAMETERS +***************************************************************************************************************/ + +namespace Config { + +// +const int minConnectionInterval = 6; // (1.25 ms units) +const int maxConnectionInterval = 15; // (1.25 ms units) +const int slaveLatency = 0; +const int supervisionTimeout = 500; // (10 ms units) + + + +}
diff -r 000000000000 -r 244f1d0a3810 main.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/main.cpp Tue Aug 09 12:57:23 2016 +0000 @@ -0,0 +1,406 @@ +/* + * Copyright (c) 2016 Samuele Cornell + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + + /* + * Thanks to Matthias Frick and his project blidino https://github.com/sieren/blidino + * which also uses RedBearslab nRF51822 and implements a USB-MIDI to BLE-MIDI bridge. + * His work made this possible and served as the main inspiration for this project . + * The content of BLEMIDI_MIDI_Parser.h file is for the most part taken from his blidino project. + */ + +#include "mbed.h" + +#include "ble/BLE.h" + +#include "BufferedSerial.h" + +#include "config.h" + +BufferedSerial UART(UART_TX_PIN,UART_RX_PIN,BUFSERIAL_LENGHT) ; //UART_RX_PIN,BUFSERIAL_LENGHT + +#if ONLY_MIDI_to_BLEMIDI==0 + +#include "BLEMIDI_MIDI_Parser.h" + +#endif + +#if ONLY_BLEMIDI_to_MIDI==0 + +#include "MIDI_BLEMIDI_Parser.h" + +#endif + + + +#define TXRX_BUF_LEN 20 + +#define RX_BUF_LEN 256 + + +#if LIGHT_SHOW==1 +DigitalOut redled(RX_LED); // sets the two LEDS on the MIDI Shield as outputs, these will be used as a visual feedback for debug +DigitalOut greenled(TX_LED); // +#endif + + +/****************************************************************************************************************************** +*INITIALIZE VARIABLES, BUFFERS and BLE-MIDI Service and Characteristic +******************************************************************************************************************************/ + +Ticker sendBLEMIDI_Ticker ; +Ticker sendData_Ticker ; + +Timer t; // timer used for BLE-MIDI timestamps + +bool isConnected; + + +//////////////////////////////////////////// +bool sendBLEMIDI_flag = false ; + +//////////// + + +BLEDevice ble; // BLE_API + +static Gap::ConnectionParams_t connectionParams; + + +// MIDI BLE Service and Characteristic UUID ( see Apple BLE-MIDI Spec. and Midi manufacter Association BLE-MIDI Spec.) + +static const uint8_t service_uuid[] = {0x03, 0xB8, 0x0E, 0x5A, 0xED, 0xE8, 0x4B, 0x33, 0xA7, 0x51, 0x6C, 0xE3, 0x4E, 0xC4, 0xC7, 0}; +static const uint8_t characteristic_uuid[] = {0x77, 0x72, 0xE5, 0xDB, 0x38, 0x68, 0x41, 0x12, 0xA1, 0xA9, 0xF2, 0x66, 0x9D, 0x10, 0x6B, 0xF3}; + +static const uint8_t service_uuid_rev[] = {0, 0xC7, 0xC4, 0x4E, 0xE3, 0x6C, 0x51, 0xA7, 0x33, 0x4B, 0xE8, 0xED, 0x5A, 0x0E, 0xB8, 0x03}; + +uint8_t txPayload[TXRX_BUF_LEN] = {0,}; + + +GattCharacteristic midiCharacteristic(characteristic_uuid, txPayload, 0, 20,GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_WRITE | + GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_WRITE_WITHOUT_RESPONSE | + GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_NOTIFY | + GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_READ); + +GattCharacteristic *midiChars[] = {&midiCharacteristic}; +GattService BLEMIDIService(service_uuid, midiChars,sizeof(midiChars) / sizeof(GattCharacteristic *)); + + + +/********************************************************************************************************************* +MIDI to BLE-MIDI +*********************************************************************************************************************/ + + +#if ONLY_BLEMIDI_to_MIDI==0 + + +void sendBLEMIDI_Callback (void) +{ + + sendBLEMIDI_flag = true ; + + /**** This callback is called within an ISR with frequency set by the sendBLEMIDI_Ticker, because it is called in an Interrupt context it is preferably + doing the MIDI to BLEMIDI task in the main, the callback only set a flag which would be checked in the main loop. + This is because all the BLEMIDI to MIDI conversion is already performed within Interrupt context. ( thus as a consequence BLEMIDI to MIDI has higher priority ) + *****/ +} + + + +void sendBLEMIDI(void) +{ + + +// MIDI::MIDI_to_BLEMIDI_Parser() parses incoming MIDI events from the UART (see MIDI_BLEMIDI_Parser.h) + if(isConnected == true && MIDI::MIDI_to_BLEMIDI_Parser()==true) { + + // a valid MIDI message has been parsed from the UART buffer + +#if LIGHT_SHOW==1 + greenled = !(greenled); +#endif + + uint8_t BLEmidi_out[SysExMaxSize] ; + uint8_t SysEx_Array_lenght ; + + uint16_t ticks = t.read_us() & 0x1fff; // read timer for timestamps + + if(MIDI::mMessage.sysexArray[0] == MIDI::SystemExclusive) { // message is SysEx + + + SysEx_Array_lenght = MIDI::mMessage.getSysExSize() ; // get SysEx message lenght + + uint8_t position = 0; // position for SysexArray + + // header + + BLEmidi_out[position++] = 0x80 | ((ticks >> 7) & 0x3f); // header & timestampHigh + BLEmidi_out[position++] = 0x80 | (ticks & 0x7f); // timestampLow + + for (int i = 0; i < SysEx_Array_lenght; i++) { + if (i == SysEx_Array_lenght - 1) { + // modify last byte + BLEmidi_out[position++] = 0x80 | (ticks & 0x7f); + + if (position == 20) { + + + ble.updateCharacteristicValue(midiCharacteristic.getValueAttribute().getHandle(), BLEmidi_out, 20); + + position = 0; + // header + BLEmidi_out[position++] = 0x80 | (ticks >> 7) & 0x3f; + } + } + BLEmidi_out[position++] = MIDI::mMessage.sysexArray[i]; + if (position == 20) { + ble.updateCharacteristicValue(midiCharacteristic.getValueAttribute().getHandle(), BLEmidi_out, 20); + + position = 0; + // header + BLEmidi_out[position++] = 0x80 | (ticks >> 7) & 0x3f; + } + + ticks = t.read_us() & 0x1fff; + } + + if (position > 0) { + // send remains + ble.updateCharacteristicValue(midiCharacteristic.getValueAttribute().getHandle(), BLEmidi_out, position); + + } + + + MIDI::mMessage.sysexArray[0]= 0 ; // reset + + } + + + // message is not SysEx + + + else { + + + if(MIDI::mMessage.data1 == 0 ) { // no data1 only status + + BLEmidi_out[0] = 0x80 | ((ticks >> 7) & 0x3f); + BLEmidi_out[1] = 0x80 | (ticks & 0x7f); + BLEmidi_out[2] = MIDI::mMessage.channel+MIDI::mMessage.type; + + ble.updateCharacteristicValue(midiCharacteristic.getValueAttribute().getHandle(), BLEmidi_out , 3); + + } + + if(MIDI::mMessage.data2 == 0 ) { // no data2 + + BLEmidi_out[0] = 0x80 | ((ticks >> 7) & 0x3f); + BLEmidi_out[1] = 0x80 | (ticks & 0x7f); + BLEmidi_out[2] = MIDI::mMessage.channel+MIDI::mMessage.type; + BLEmidi_out[3] = MIDI::mMessage.data1 ; + + ble.updateCharacteristicValue(midiCharacteristic.getValueAttribute().getHandle(), BLEmidi_out , 4); + + } + + if(MIDI::mMessage.data2 != 0 ) { + + BLEmidi_out[0] = 0x80 | ((ticks >> 7) & 0x3f); + BLEmidi_out[1] = 0x80 | (ticks & 0x7f); + BLEmidi_out[2] = MIDI::mMessage.channel+MIDI::mMessage.type; + BLEmidi_out[3] = MIDI::mMessage.data1 ; + BLEmidi_out[4] = MIDI::mMessage.data2 ; + + ble.updateCharacteristicValue(midiCharacteristic.getValueAttribute().getHandle(), BLEmidi_out , 5); + + } + + }// end else + + }// outer if + +// invalid message or not connected + +} + +#endif + + +/****************************************************************************************************************************** +* BLE CALLBACKS +******************************************************************************************************************************/ + + +void disconnectionCallback(const Gap::DisconnectionCallbackParams_t *params) +{ + //device disconnected + + isConnected = false ; + +#if ONLY_BLEMIDI_to_MIDI==0 + sendBLEMIDI_Ticker.detach() ; // stop Ticker to save energy +#endif + + ble.startAdvertising(); // start advertising +} + + +void connectionCallback(const Gap::ConnectionCallbackParams_t* params) { + + +isConnected=true ; + +// try update conn.parameters + +connectionParams.minConnectionInterval = Config::minConnectionInterval; +connectionParams.maxConnectionInterval = Config::maxConnectionInterval; +connectionParams.slaveLatency = Config::slaveLatency; +connectionParams.connectionSupervisionTimeout = Config::supervisionTimeout; + +ble.updateConnectionParams(params->handle ,&connectionParams); + + +//start timers here +#if ONLY_BLEMIDI_to_MIDI==0 +sendBLEMIDI_Ticker.attach( sendBLEMIDI_Callback, SENDBLEMIDI_INTERVAL); // every SENDBLEMIDI_INTERVAL seconds calls sendBLEMIDI_Callback (ISR) + +t.start(); // start the timer used for BLEMIDI timestamps + +#endif + +} + + +/**************************************************************************************************************************** +BLE-MIDI to MIDI +****************************************************************************************************************************/ + + +#if ONLY_MIDI_to_BLEMIDI==0 + +void parseIncoming(uint8_t *buffer, uint16_t bytesRead) { // parse BLE-MIDI Events that have been written on the MIDI Characteristic + for (int i = 1; i < bytesRead; i++) + { + parseMidiEvent(buffer[0], buffer[i]); // parse and send through UART the MIDI Events received through BLE (see BLE_MIDI_Parser.h) + } +} + + + +void onDataWritten(const GattWriteCallbackParams *Handler) // this functions is called within an ISR every time data has been written on nRF51822 GATT Server MIDI Characteristic +{ + +#if LIGHT_SHOW==1 + redled = !(redled) ; +#endif + + uint8_t buf[TXRX_BUF_LEN]; + uint16_t bytesRead; + if (Handler->handle == midiCharacteristic.getValueAttribute().getHandle()) { + ble.readCharacteristicValue(midiCharacteristic.getValueAttribute().getHandle(), + buf, &bytesRead); + parseIncoming(buf, bytesRead); + + } + +} + +#endif + + +/************************************** +MAIN +***************************************/ + +int main(void) +{ + + +#if LIGHT_SHOW==1 + redled = 1; + greenled = 1; +#endif + + + + UART.baud(31250) ; // set UART baud rate to 31250 (MIDI standard) + + ble.init(); + ble.onDisconnection(disconnectionCallback); + ble.onConnection(connectionCallback) ; + +#if ONLY_MIDI_to_BLEMIDI==0 + ble.onDataWritten(onDataWritten); +#endif + + //conn. parameters ( rejected on iOS/OSX see Apple BLE peripheral design guidelines but can work on Android ) + + connectionParams.minConnectionInterval = Config::minConnectionInterval; + connectionParams.maxConnectionInterval = Config::maxConnectionInterval; + connectionParams.slaveLatency = Config::slaveLatency; + connectionParams.connectionSupervisionTimeout = Config::supervisionTimeout; + ble.setPreferredConnectionParams(&connectionParams); + ble.getPreferredConnectionParams(&connectionParams); + + + /* setup advertising */ + ble.accumulateAdvertisingPayload(GapAdvertisingData::BREDR_NOT_SUPPORTED); + ble.accumulateAdvertisingPayload(GapAdvertisingData::SHORTENED_LOCAL_NAME, + (const uint8_t *)"nRF51", sizeof("nRF51") - 1); + ble.accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LIST_128BIT_SERVICE_IDS, + (const uint8_t *)service_uuid_rev, sizeof(service_uuid_rev)); + + ble.accumulateScanResponse(GapAdvertisingData::SHORTENED_LOCAL_NAME, + (const uint8_t *)"nRF51", sizeof("nRF51") - 1); + ble.accumulateScanResponse(GapAdvertisingData::COMPLETE_LIST_128BIT_SERVICE_IDS,(const uint8_t *)service_uuid_rev, sizeof(service_uuid_rev)); + ble.setAdvertisingType(GapAdvertisingParams::ADV_CONNECTABLE_UNDIRECTED); + + /* 100ms; in multiples of 0.625ms. */ + ble.setAdvertisingInterval(160); + + // set adv_timeout, in seconds + ble.setAdvertisingTimeout(0); + ble.addService(BLEMIDIService); + + //Set Device Name + ble.setDeviceName((const uint8_t *)"nRF51"); + + ble.startAdvertising(); + + + while(1) { //main loop + +#if ONLY_BLEMIDI_to_MIDI==0 + if(sendBLEMIDI_flag == true ) { // check if the flag is set + + sendBLEMIDI_flag=false ; + sendBLEMIDI() ; // parse MIDI Events from UART and send them over BLE + + } +#endif + + ble.waitForEvent(); // sleep + + } // end main loop + +} // end main
diff -r 000000000000 -r 244f1d0a3810 mbed.bld --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mbed.bld Tue Aug 09 12:57:23 2016 +0000 @@ -0,0 +1,1 @@ +http://mbed.org/users/mbed_official/code/mbed/builds/4f6c30876dfa \ No newline at end of file
diff -r 000000000000 -r 244f1d0a3810 nRF51822.lib --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/nRF51822.lib Tue Aug 09 12:57:23 2016 +0000 @@ -0,0 +1,1 @@ +http://mbed.org/teams/Nordic-Semiconductor/code/nRF51822/#088f5738bf18