Icon P1-M: a new very interesting small control surface

I currently use an Icon Plaform M+ with its display unit option (DU) with GP. This one seems to have the same small form factor and has also an optional OLED display unit:

It could become my new surface control.

2 Likes

I see thereā€™s an even smaller ā€˜nanoā€™ version as well.

I like what theyā€™ve done with the 4x4 touch button grid. After the holiday Iā€™ll email them and ask if there is a way to change the labels on those via sysex rather than having to go through the IMAP program every time. Would be very cool if they could be changed in real time through GP scripting/extensions.

The display is definitely an upgrade. I never really liked the display on the Platform M+. It works, but itā€™s not great. The OLED display looks much nicer.

Given that the form factor is the same as the M+ Iā€™m not sure itā€™s worth an upgrade for me. I may grab the P1-Nano though, being roughly half the width. I also really like that the nano will work on USB power (assuming your PC is up to it).

From the documentation it looks like everything operates on MCU protocol (like most of Iconā€™s other controllers). Thatā€™s great for compatibility, but also defines (limits) what you can display. Iā€™d love to get more than 7 characters per channel strip, but at least on this one each scribble area is distinct so you can use all 7 rather than having to leave a space at the end (thus 6 chars) so they donā€™t all run together.

I also wanted to contact them, so please if you do so, share your info here. :wink:

Not very useful update, but I emailed Icon to ask if there was a way to re-write the button labels on the touch screen using sysex or any other means.

They got back to me pretty quickly, asking me to elaborate on what I had in mind and what the use case would be.

I wrote a fairly detailed response a week ago and have not heard back. I suppose hearing nothing is better than a ā€œnoā€ but not as good as a ā€œyesā€.

3 Likes

Hi Vindes.

Curious if you ever heard back from Icon?

I too would be interested in this :slight_smile:

Thanks in advance.

No, never heard back after their initial reply.

As the P1-M is set up from a communications perspective, Iā€™m not sure if what I was hoping for is possible. I hoped to do some testing and potentially reverse engineer a protocol if I could snoop on the communications between the PC and the device.

If it was all happening over MIDI then it might be a worthwhile endeavor. That doesnā€™t appear to be the case. It looks like anything that can change what appears on the buttons has to go over USB rather than MIDI (because Iā€™m not seeing it in the midi), and I donā€™t have the time to go down that path if the company isnā€™t interested in going there.

Iā€™m not writing it off as impossible. When I first started communicating with Novation about the protocols to communicate with the SL-MK3 line I also ran into a wall. However, I knew at the time that the possibility existed because others were doing it in Bitwig and Ableton. I decompiled their code and was able to figure out much of the protocol. Shortly after that Novation finally publicly released the communication details. (I assume they were available to others under non-disclosure agreements before that.)

Iā€™m not holding my breath for anything new from Icon on this front, but maybe when they have time theyā€™ll decide itā€™s worth enabling or supporting.

Thanks for your extended answer.

Too bad Icon didnā€™t get back. That controller has so much potential to have those displays being dynamically populated etc and being able to change them through scripts in your daw.

Iā€™m mainly in reaper and doing a lot of scripting, and with the deep integration there you could make something remarkable (already have lots of stuff happening with the Icon Playform M+).

But sounds like you have lots of experience with these approaches and the fact you donā€™t see any data happening on the midi port when editing the iMap software is a downer.

I still think the device will be worth it for me because of the bigger screen estate and the faders seems nicer. Maybe eventually there could be a way to also manipulate the lcd panel.
Iā€™ll also write icon so they know itā€™s not a standalone request :slight_smile:

Thanks again.

Good news, all you wanted is possible!!

So I also wrote them and got this answer:

Which is obviously not true, as you literally write the text on the D4T display with sysex messages when connected to Mackie for instance.
So Iā€™d say they are just not interested in users doing this!

ANYWAY, I got my unit 2 days ago, and have been able to decode both the display (and the extra space available) as wall as the 16 LCD display buttons. And itā€™s possible to write all of it with sysex.
The LCD display is written via port 4, but writes the entire units entire MIDI setup (so quite a big Sysex dump), so itā€™s not the simplest thing, and you canā€™t just overwrite one buttons text.
The extra two lines on the D4T display is a bit weird. They also only support 7 characters pr. line and needs the full text of all 8 channels to work reliably, and is written through the (DAW) port you want to use.

So Iā€™ve slowly started implementing some of the things including the color strip and making a very nice custom setup:

Hereā€™s a picture of it all, and you can see the ā€œautomatedā€ names on the LCD display:

1 Like

The youtube link is ā€œno moreā€ available for me. Could you please share your findings?

Thatā€™s great! The main two lines of text on the D4T display I had working, as thatā€™s just the standard Mackie MCU protocol. All the faders, knobs, and buttons follow the MCU protocol.

The reason I bought the thing (mine is the 1 fader nano version) was for the possibility to control the text on the buttons.

Iā€™d love to see the details of how you write to those.

Sorry for the confusion, thatā€™s from the email screenshot from Icon.
Itā€™s just their product video, so not useful at all. Iā€™m working in reaper, using Lua script, and still decoding things. So kinda hard to share the exact codes.
I used MIDI Monitor: snoize: MIDI Monitor to figure out what messages was sent to the icon mixer.
I might try and do a bigger write up when Iā€™m less busy :slight_smile:

1 Like

So at this moment itā€™s still early stages. Iā€™ve used SoundFlow to ā€œdecodeā€ and test how to write it as Iā€™m more proficient in Javascript, but will later bring it in to Reaper as Lua scripts.
So it might not make any sense to you, but you can have the code.
The 3 first functions deals with trying to make the text look nice and how the iMap is formatting it. Thereā€™s maybe still more work to do on that.

Iā€™m unsure if itā€™ll work if you try dump it on to the Nano, as it has less buttons and all that extra sysex code where Iā€™ve written ā€œ// add settings for deviceā€ is the settings for all the buttons and faders on the device.
But you could try. Also if you need it in another code language, ChatGPT is excellent at re-writing code to another language.


function decimalToHex(decimalNumber, padding = 2) {
    let hex = decimalNumber.toString(16).toUpperCase();
    while (hex.length < padding) {
        hex = '0' + hex;
    }
    return hex;
}

function centerText(text, maxAmount = 8) {
    let spaceAmount = maxAmount - text.length
    let newText = text
    for (let i = 0; i < spaceAmount; i++) {
        if (i < Math.floor(spaceAmount / 2)) {
            newText = " " + newText
        }
        else {
            newText = newText + " "
        }
    }
    return newText
}

function getCenterSpace(str) {
    let indices = []
    for (let i = 0; i < str.length; i++) {
        if (str[i] === ' ') {
            indices.push(i);
        }
    }
    if (indices.length == 1 && indices[0] <= 8 && str.length - (indices[0] + 1) <= 8) {
        return indices[0]
    }
    else if (indices.length > 1) {
        return -2
    }
    else {
        return -1
    }
}

function textToHex(text) {
    let hexValues = [];
    let indexValue;
    let fullText;
    let firstPart;
    let secondPart;
    text = text.trim() // remove space before or after text

    if (text.length < 8) {
        indexValue = text.length % 2 === 0 ? "03" : "01";
        fullText = centerText(text) + centerText("")
        firstPart = centerText(text)
        secondPart = centerText("")
    }
    else if (text.length == 8) {
        indexValue = "02";
        fullText = text + centerText("")
    } else {
        let spaceIndex = getCenterSpace(text)
        if (spaceIndex == -1) {
            firstPart = text.slice(0, 8);
            secondPart = text.slice(8);
            fullText = centerText(firstPart) + centerText(" " + secondPart)
        }
        else if (spaceIndex == -2) {
            firstPart = text.slice(0, 8);
            secondPart = (text.slice(8) + centerText("")).slice(0, 8);
            fullText = centerText(firstPart) + centerText(secondPart)
        }
        else {
            firstPart = text.slice(0, spaceIndex);
            secondPart = text.slice(spaceIndex + 1);
            fullText = centerText(firstPart) + centerText(secondPart)
        }

        // 04 both uneven
        if (firstPart.length % 2 == 1 && secondPart.length % 2 == 1) {
            indexValue = "04";
        }
        // 05 center top row
        else if (firstPart.length % 2 == 1 && secondPart.length % 2 == 0) {
            indexValue = "05";
        }
        // 06 center low row
        else if (firstPart.length % 2 == 0 && secondPart.length % 2 == 1) {
            indexValue = "06";
        }
        // 07 center both rows
        else if (firstPart.length % 2 == 0 && secondPart.length % 2 == 0) {
            indexValue = "07";
        }
        firstPart = centerText(firstPart)
        secondPart = centerText(secondPart)
    }

    // Convert the text to an array of hex values
    let hexText = indexValue + " " + fullText.split('').map(char => char.charCodeAt(0).toString(16).toUpperCase()).join(' ');

    return hexText;
}

function setNames() {
    // after 21, the next 3 values seems to be like a timestamp. end of string might be different for different daw modes
    let introLine = "F0 1D 03 10 07 21 0E 58 32 6D 65 58 12 7A 03 F7\n"
    let outroLines = "EF 7F 7F\n" + "EC 22 06\n" + "F0 1D 03 10 07 24 0E 58 32 6D 65 58 12 7A 03 F7"

    // construct a string to set all fields to note on channel 2, starting from 0
    let constructSendString = ""
    let constructNameString = ""
    for (let i = 0; i < 80; i++) {
        if (i % 28 == 0) {
            if (i > 0) { constructSendString = constructSendString + "F7\n" }
            constructSendString = constructSendString + "F0 1D 03 10 07 25 06 0" + ((i / 28) + 1) + " 1C " + "09 09 00 " + decimalToHex(i+40) + " 7F 00 00 00 "
        }
        else {
            constructSendString = constructSendString + "09 09 00 " + decimalToHex(i+40) + " 7F 00 00 00 "
        }

        let name = textToHex("test " + (i + 1))
        
        if (i % 10 == 0) {
            if (i > 0) { constructNameString = constructNameString + "F7\n" }
            constructNameString = constructNameString + "F0 1D 03 10 07 26 06 0" + ((i / 10) + 1) + " 0A " + name + " "
        }
        else {
            constructNameString = constructNameString + name + " "
        }
    }

    // add settings for device
    constructSendString = constructSendString +
        "09 09 00 30 7F 00 00 00 09 09 00 31 7F 00 00 00 09 09 00 2E 7F 00 00 00 09 09 00 2F 7F 00 00 00 F7\n" +
        "F0 1D 03 10 07 25 06 04 1C 09 09 00 32 7F 00 00 00 00 00 00 00 00 00 00 00 09 09 00 4F 7F 00 00 00 09 09 00 4A 7F 00 00 00 09 09 00 4C 7F 00 00 00 09 09 00 4B 7F 00 00 00 09 09 00 4E 7F 00 00 00 09 09 00 4D 7F 00 00 00 09 09 00 5B 7F 00 00 01 09 09 00 5C 7F 00 00 01 09 09 00 56 7F 00 00 01 09 09 00 5D 7F 00 00 01 09 09 00 5E 7F 00 00 01 09 09 00 5F 7F 00 00 01 09 09 00 00 7F 00 00 00 09 09 00 01 7F 00 00 00 09 09 00 02 7F 00 00 00 09 09 00 03 7F 00 00 00 09 09 00 04 7F 00 00 00 09 09 00 05 7F 00 00 00 09 09 00 06 7F 00 00 00 09 09 00 07 7F 00 00 00 09 09 00 20 7F 00 00 00 09 09 00 21 7F 00 00 00 09 09 00 22 7F 00 00 00 09 09 00 23 7F 00 00 00 09 09 00 24 7F 00 00 00 09 09 00 25 7F 00 00 00 F7\n" +
        "F0 1D 03 10 07 25 06 05 1C 09 09 00 26 7F 00 00 00 09 09 00 27 7F 00 00 00 09 09 00 65 7F 00 00 00 0B 0B 00 10 40 00 00 00 0B 0B 00 11 40 00 00 00 0B 0B 00 12 40 00 00 00 0B 0B 00 13 40 00 00 00 0B 0B 00 14 40 00 00 00 0B 0B 00 15 40 00 00 00 0B 0B 00 16 40 00 00 00 0B 0B 00 17 40 00 00 00 0B 0B 00 3C 40 00 00 00 09 09 00 66 7F 00 00 00 09 09 00 67 7F 00 00 00 09 09 00 68 7F 00 00 00 09 09 00 69 7F 00 00 00 09 09 00 6A 7F 00 00 00 09 09 00 6B 7F 00 00 00 09 09 00 6C 7F 00 00 00 09 09 00 6D 7F 00 00 00 09 09 00 6E 7F 00 00 00 09 09 00 6F 7F 00 00 00 09 09 00 70 7F 00 00 00 0E 0E 00 00 7F 00 00 00 0E 0E 01 00 7F 00 00 00 0E 0E 02 00 7F 00 00 00 0E 0E 03 00 7F 00 00 00 0E 0E 04 00 7F 00 00 00 F7\n" +
        "F0 1D 03 10 07 25 06 06 1C 0E 0E 05 00 7F 00 00 00 0E 0E 06 00 7F 00 00 00 0E 0E 07 00 7F 00 00 00 0E 0E 08 00 7F 00 00 00 09 09 00 60 7F 00 00 00 09 09 00 61 7F 00 00 00 09 09 00 62 7F 00 00 00 09 09 00 63 7F 00 00 00 40 05 00 00 00 00 00 00 40 04 0F 00 00 00 00 00 50 00 00 00 02 00 00 00 50 00 00 00 01 00 00 00 09 09 00 52 7F 00 00 00 09 09 00 53 7F 00 00 00 09 09 00 54 7F 00 00 00 09 09 00 55 7F 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 09 09 00 64 7F 00 00 00 09 09 00 64 7F 00 00 00 09 09 00 08 7F 00 00 00 09 09 00 09 7F 00 00 00 09 09 00 0A 7F 00 00 00 09 09 00 0B 7F 00 00 00 F7\n" +
        "F0 1D 03 10 07 25 06 07 1C 09 09 00 0C 7F 00 00 00 09 09 00 0D 7F 00 00 00 09 09 00 0E 7F 00 00 00 09 09 00 0F 7F 00 00 00 09 09 00 10 7F 00 00 00 09 09 00 11 7F 00 00 00 09 09 00 12 7F 00 00 00 09 09 00 13 7F 00 00 00 09 09 00 14 7F 00 00 00 09 09 00 15 7F 00 00 00 09 09 00 16 7F 00 00 00 09 09 00 17 7F 00 00 00 09 09 00 18 7F 00 00 00 09 09 00 19 7F 00 00 00 09 09 00 1A 7F 00 00 00 09 09 00 1B 7F 00 00 00 09 09 00 1C 7F 00 00 00 09 09 00 1D 7F 00 00 00 09 09 00 1E 7F 00 00 00 09 09 00 1F 7F 00 00 00 09 09 00 20 7F 00 00 00 09 09 00 21 7F 00 00 00 09 09 00 22 7F 00 00 00 09 09 00 23 7F 00 00 00 09 09 00 24 7F 00 00 00 09 09 00 25 7F 00 00 00 09 09 00 26 7F 00 00 00 09 09 00 27 7F 00 00 00 F7\n" +
        "F0 1D 03 10 07 25 06 08 1C 0B 0B 00 10 40 00 00 00 0B 0B 00 11 40 00 00 00 0B 0B 00 12 40 00 00 00 0B 0B 00 13 40 00 00 00 0B 0B 00 14 40 00 00 00 0B 0B 00 15 40 00 00 00 0B 0B 00 16 40 00 00 00 0B 0B 00 17 40 00 00 00 09 09 00 20 7F 00 00 00 09 09 00 21 7F 00 00 00 09 09 00 22 7F 00 00 00 09 09 00 23 7F 00 00 00 09 09 00 24 7F 00 00 00 09 09 00 25 7F 00 00 00 09 09 00 26 7F 00 00 00 09 09 00 27 7F 00 00 00 0B 0B 00 10 40 00 00 00 0B 0B 00 11 40 00 00 00 0B 0B 00 12 40 00 00 00 0B 0B 00 13 40 00 00 00 0B 0B 00 14 40 00 00 00 0B 0B 00 15 40 00 00 00 0B 0B 00 16 40 00 00 00 0B 0B 00 17 40 00 00 00 09 09 00 20 7F 00 00 00 09 09 00 21 7F 00 00 00 09 09 00 22 7F 00 00 00 09 09 00 23 7F 00 00 00 F7\n" +
        "F0 1D 03 10 07 25 06 09 1C 09 09 00 24 7F 00 00 00 09 09 00 25 7F 00 00 00 09 09 00 26 7F 00 00 00 09 09 00 27 7F 00 00 00 0B 0B 00 10 40 00 00 00 0B 0B 00 11 40 00 00 00 0B 0B 00 12 40 00 00 00 0B 0B 00 13 40 00 00 00 0B 0B 00 14 40 00 00 00 0B 0B 00 15 40 00 00 00 0B 0B 00 16 40 00 00 00 0B 0B 00 17 40 00 00 00 09 09 00 20 7F 00 00 00 09 09 00 21 7F 00 00 00 09 09 00 22 7F 00 00 00 09 09 00 23 7F 00 00 00 09 09 00 24 7F 00 00 00 09 09 00 25 7F 00 00 00 09 09 00 26 7F 00 00 00 09 09 00 27 7F 00 00 00 0B 0B 00 10 40 00 00 00 0B 0B 00 11 40 00 00 00 0B 0B 00 12 40 00 00 00 0B 0B 00 13 40 00 00 00 0B 0B 00 14 40 00 00 00 0B 0B 00 15 40 00 00 00 0B 0B 00 16 40 00 00 00 0B 0B 00 17 40 00 00 00 F7\n"

    constructNameString = constructNameString + "F7\n"

    let text = introLine + constructSendString + constructNameString + outroLines

    let multipleStringsArray = text.split("\n")

    let send = true
    for (let i = 0; i < multipleStringsArray.length; i++) {
        let string = multipleStringsArray[i]

        let stringArray = string.split(" ").map(x => Number("0x" + x))

        if (stringArray.length > 2 && send) {
            sf.midi.send({
                midiBytes: stringArray,
                externalMidiPort: "iCON P1-M V1.07  Port 4"
            })
        }
    }
}
setNames()
2 Likes

Great work!

Your sysex doesnā€™t work on my P1-Nano, but the fact that you found it on midi made me go take a look for the data again.

Using Wireshark on windows, with the USBPcap extension installed, allows us to snoop on the USB communications at the interface level, and once we filter out all the noise (which there is a lot of) and narrow it down to sysex packets I see what appears to be similar to what you see.

At a minimum the device ID of the P1-Nano is different. The ā€œF0 1D 03 10 07ā€ at the start of all your sysex data has an 06 for the Nano instead of your 07. Not too surprising I suppose.

The iMap Nano program also groups the sysex strings differently. It looks like about the same amount of total information, but with the Nano the sysex for the display text on the LCD is more logically grouped, with each full page of text (16 buttons) all grouped into one sysex line. (i.e., yours is in 8 sysex groups of 10 softkeys, mine is in 5 groups of 16 softkeys).

The button data in yours is 9 groups of 28 (8 byte chunks) while mine is 5 groups of 40 plus one group of 24.

So far I havenā€™t gotten the Nano to accept a partial dump of the sysex info. Like you said, it seem to want all of it every time. That would be unfortunate if changing the text on a single button would require transmitting the whole block every time.

I also donā€™t see anywhere in the sysex to control the name of selected DAW, like Bitwig, Cubase, etc. I see where the sysex (at least for the Nano) refers to them by their numbers (as enumerated in iMap) but Iā€™m guessing the lookup table is in the firmware.

Some work left to do, but Iā€™m excited that you found this stuff!

1 Like

Thanks for the info, I didnā€™t know about this one. :+1:

Thatā€™s the unfortunate thing about these findings. Itā€™s not the best news, I hope thereā€™ll be a solution so that only useful information has to be sent via SysEx.

I wonder if when you connect an Icon Platform M+ with a DU display unit to a DAW, these are also this kind of data packets that are sent? If so, as we already know how to send useful information only to the DU, perhaps we can find the logic behind it, so that only useful information is sent?.. :nerd_face:

Some key observations (and limitations) after decoding the whole yesterday:

  • You can omit the part of the dump that sets the device mappings, so you could have fixed mappings and only dump the sysex that sets the lcd display.
  • you can only set the background of the lcd display if the mapping is set to a button and channel 1. (I believe itā€™s the same with all buttons on the device)
  • When sending the many sysex lines to the device, the device actually ā€œanswersā€ with a note on message sending from the port 4. If the sysex messages comes to fast, Iā€™ve experienced that thereā€™s some wrong pixels/scrambled data on the lcd display (and probably also on the mappings). You can of course wait for the ā€œanswerā€ messages before sending the next sysex message, but that would require that you have some code listening for this. I seem to have reliably made it work by adding a couple of milliseconds between sending each line. Anyway hereā€™s the note on numbers:
    // 94 21 03 - answer after first line
    // 94 25 01 - answer after each message line
    // 94 26 01 - answer after each name line
    // 94 21 00 - answer after last line
  • It seems like you can omit the two pitchbend message that the imap software is sending as in itā€™s own dump.
  • I had hoped to also be able to get information when you press the function layer keys, but they donā€™t seem to be mapped.
  • I canā€™t seem to map the Navi and Focus jog wheel function to other things, as it seems hardcoded that they will be sending key command / scroll wheel.
1 Like

For anyone else who might wade into this or not be intimately familiar with the details:

  • All of the ICON controllers are built around the Mackie MCU protocol, which is well known documented. That covers just about everything youā€™ll see on a Platform M+ and most other MCU compatible devices out there.
  • A few manufacturers (e.g. Icon, Behringer) added an extra line of ā€œscribble stripsā€ and color bars to newer MCU ā€œclonesā€. The protocols for these are ā€œknownā€ but not publicly documented (to my knowledge). They are implemented in the Moss extensions for Bitwig & Reaper, among other places. It sounds like saxmand has reverse engineered the Icon versions as well through sniffing. The V1-M puts the scribble strips in the middle of the unit, while the P1-M puts them up on the separate display. The P1-Nano doesnā€™t have either the scribble strips or the color bars (as far as i know).
  • Icon broke the cool new ground by adding touchscreens to the P1-M, P1-Nano, and V1-M. They are set up as 4x4 grids on the P1ā€™s and 4x6 on the V1-M. Iā€™m not aware at this point of anyone other than the people in this thread decoding the protocols for those yet.

I noticed similar ā€œanswerā€ messages like you mentioned on the P1-Nano, but they are different. They can also be ignored on the Nano, as can the pitch bend messages. The ā€œintroā€ and ā€œoutroā€ sysex messages also appear unnecessary on the Nano.

Iā€™m not surprised about not being able to remap what the jog wheel sends. Thatā€™s part of the MCU protocol, and I would imagine that allowing that to change could break MCU compatibility with DAWs and users saying ā€œwhy doesnā€™t this work anymore?ā€ The P1-Nano iMap wonā€™t let me change many of the standard MCU physical control assignments, including the jog wheel, the transport controls, or the mute, solo, rec buttons.

Yeah, I had that myself. Mine actually seemed to come from buffering problems on the PC side rather than the Nano, or at least some strange interaction between them. My program for sending (Midi-Ox) defaults to 256 byte buffers, and with sysexā€™s that would run past 256 bytes thatā€™s where the garbage would show up. The bottom right few soft-buttons on mine.

2 Likes

Great write up.

Nice we donā€™t need the the intro outro message, didnā€™t think of that. Wonder what that does, maybe some general setting on the device.

Forgot to response regarding the DAW name, indeed thereā€™s no way to edit that as of now it seems. Itā€™s within the device it sets it, based on the sysex that was send. Itā€™s set on the 7th byte. For example:

  • 06 is for reaper on port/daw layer 1. 26 would be on port 2 and 46 would be on port 3

Iā€™m able to change the message of all keys as well as jog wheel function except the combination of Focus+Jog Wheel, as it seems the Icon device is sending some mouse message outside the MIDI protocol.

Also figured out that you can set different midi message for each of the encoders, depending on what Function Layer is selected, at least on the P1-M, as I have to set those controllers 4 times at the end of setting up the device.

Indeed the display for the Nano does not seem to have the track color strip. Kinda weird, as itā€™s just a color display, and they have the channel metering on there instead. It could easily have been there I thinkā€¦

Below is my ā€œfinalā€ javascript code structure, which has some more clear decoding, if you understand how to read javascript. I can now define two variables, one for the lcd display and one for the device in general. Iā€™ve been able to port it to lua via ChatGPT and have started implementing it in my mixer setup in Reaper.

One thing I could dream for, was that Icon would open up the device for further editing, as the display could easily show more characters etc. Sort of an open sandbox mode, that is not trying to fit in to the MCU box. But that would probably require a lot more as the on processor device has a lot of predefined stuff, such as the DAW name, how the master knob function etc.
But for now this has been better than what I had hoped to find, and already improving my setup. Cheers.


function decimalToHex(decimalNumber, padding = 2) {
    let hex = decimalNumber.toString(16).toUpperCase();
    while (hex.length < padding) {
        hex = '0' + hex;
    }
    return hex;
}

function centerText(text, maxAmount = 8) {
    let spaceAmount = maxAmount - text.length
    let newText = text
    for (let i = 0; i < spaceAmount; i++) {
        if (i < Math.floor(spaceAmount / 2)) {
            newText = " " + newText
        }
        else {
            newText = newText + " "
        }
    }
    return newText
}

function getCenterSpace(str) {
    let indices = []
    for (let i = 0; i < str.length; i++) {
        if (str[i] === ' ') {
            indices.push(i);
        }
    }
    if (indices.length == 1 && indices[0] <= 8 && str.length - (indices[0] + 1) <= 8) {
        return indices[0]
    }
    else if (indices.length > 1) {
        return -2
    }
    else {
        return -1
    }
}


function textToHex(text) {
    let hexValues = [];
    let indexValue;
    let fullText;
    let firstPart;
    let secondPart;
    text = text.trim() // remove space before or after text

    if (text.length < 8) {
        indexValue = text.length % 2 === 0 ? "03" : "01";
        fullText = centerText(text) + centerText("")
        firstPart = centerText(text)
        secondPart = centerText("")
    }
    else if (text.length == 8) {
        indexValue = "02";
        fullText = text + centerText("")
    } else {
        let spaceIndex = getCenterSpace(text)
        if (spaceIndex == -1) {
            firstPart = text.slice(0, 8);
            secondPart = text.slice(8);
            fullText = centerText(firstPart) + centerText(" " + secondPart)
        }
        else if (spaceIndex == -2) {
            firstPart = text.slice(0, 8);
            secondPart = (text.slice(8) + centerText("")).slice(0, 8);
            fullText = centerText(firstPart) + centerText(secondPart)
        }
        else {
            firstPart = text.slice(0, spaceIndex);
            secondPart = text.slice(spaceIndex + 1);
            fullText = centerText(firstPart) + centerText(secondPart)
        }

        // 04 both uneven
        if (firstPart.length % 2 == 1 && secondPart.length % 2 == 1) {
            indexValue = "04";
        }
        // 05 center top row
        else if (firstPart.length % 2 == 1 && secondPart.length % 2 == 0) {
            indexValue = "05";
        }
        // 06 center low row
        else if (firstPart.length % 2 == 0 && secondPart.length % 2 == 1) {
            indexValue = "06";
        }
        // 07 center both rows
        else if (firstPart.length % 2 == 0 && secondPart.length % 2 == 0) {
            indexValue = "07";
        }
        firstPart = centerText(firstPart)
        secondPart = centerText(secondPart)
    }

    // Convert the text to an array of hex values
    let hexText = indexValue + " " + fullText.split('').map(char => char.charCodeAt(0).toString(16).toUpperCase()).join(' ');

    return hexText;
}

function setNames() {

    // 06 is for reaper on port 1. 26 would be port 2 and 46 would be port 3
    function openSysexMapping(line) {
        return "F0 1D 03 10 07 25 06 " + decimalToHex(line) + " 1C"
    }
    function openSysexName(line) {
        return "F0 1D 03 10 07 26 06 " + decimalToHex(line) + " 0A"
    }

    /* 
    28, 29, 2A, 2B, 2C, 2D, 
    33, 34, 35, 36, 37, 38, // used
    39, 3A, 3B,
    3D, 3E, 3F, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 51, 57,
    58, 59, 5A, 70, 71, 72,
    73, 74, 75, 76, 77, 78, 79, 7A, 7B, 7C, 7D, 7E, 7F
     */

    function createSysexString(info) {
        if (typeof info === "string") {
            return info
        }
        else {
            let pattern = info.pattern ? info.pattern : "00"
            return [info.type, info.type, info.channel, info.number, info.value, "00","02",pattern].join(" ")
        }
    }

    const emptyMapping = { name: "", number: "00", type: "00", value: "7F", channel: "00" }
    let randomNumber = Math.floor(Math.random() * 9)
    
    // for encoders pattern 00 is incremental and 10 is numerical
    const lcdSettings = {
        1: { name: "Volume", number: "33", type: "09", value: "7F", channel: "00" },
        2: { name: "Pan", number: "34", type: "09", value: "7F", channel: "00" },
        3: { name: "Pan Width", number: "35", type: "09", value: "7F", channel: "00" },
        5: { name: "Stereo Balance", number: "37", type: "09", value: "7F", channel: "00" },
        6: { name: "Stereo Pan", number: "38", type: "09", value: "7F", channel: "00" },
        7: { name: "Dual Pan", number: "39", type: "09", value: "7F", channel: "00" },
        14: { name: Date.now() + "", number: "51", type: "09", value: "7F", channel: "00" },
        15: { name: randomNumber + "", number: "51", type: "0B", value: "7F", channel: "0" + randomNumber },
    }

    const deviceSettings = {
        // PLATFORM SETTINGS
        leftButton: { number: "30", type: "09", value: "7F", channel: "00" }, // 48
        rightButton: { number: "31", type: "09", value: "7F", channel: "00" }, // 49
        bankLeftButton: { number: "2E", type: "09", value: "7F", channel: "00" }, // 46
        bankRightButton: { number: "2F", type: "09", value: "7F", channel: "00" }, // 47
        flipButton: { number: "32", type: "09", value: "7F", channel: "00" }, // 50
        lockButton: { number: "00", type: "00", value: "7F", channel: "00" },

        offButton: { number: "4F", type: "09", value: "7F", channel: "00" }, // 73
        readButton: { number: "4A", type: "09", value: "7F", channel: "00" }, // 74
        writeButton: { number: "4B", type: "09", value: "7F", channel: "00" }, // 75
        trimButton: { number: "4C", type: "09", value: "7F", channel: "00" }, // 76
        latchButton: { number: "4E", type: "09", value: "7F", channel: "00" }, // 78
        touchButton: { number: "4D", type: "09", value: "7F", channel: "00" }, // 77

        rewindButton: { number: "5B", type: "09", value: "7F", channel: "00" }, // 91
        forwardButton: { number: "5C", type: "09", value: "7F", channel: "00" }, // 92
        loopButton: { number: "56", type: "09", value: "7F", channel: "00" }, // 86
        stopButton: { number: "5D", type: "09", value: "7F", channel: "00" }, // 93
        playButton: { number: "5E", type: "09", value: "7F", channel: "00" }, // 94
        recordButton: { number: "5F", type: "09", value: "7F", channel: "00" }, // 95

        armButton1: { number: "00", type: "09", value: "7F", channel: "00" }, // 0
        armButton2: { number: "01", type: "09", value: "7F", channel: "00" }, // 1
        armButton3: { number: "02", type: "09", value: "7F", channel: "00" }, // 2
        armButton4: { number: "03", type: "09", value: "7F", channel: "00" }, // 3
        armButton5: { number: "04", type: "09", value: "7F", channel: "00" }, // 4
        armButton6: { number: "05", type: "09", value: "7F", channel: "00" }, // 5
        armButton7: { number: "06", type: "09", value: "7F", channel: "00" }, // 6
        armButton8: { number: "07", type: "09", value: "7F", channel: "00" }, // 7

        pushKnob1: { number: "20", type: "09", value: "7F", channel: "00" }, // 32
        pushKnob2: { number: "21", type: "09", value: "7F", channel: "00" }, // 33
        pushKnob3: { number: "22", type: "09", value: "7F", channel: "00" }, // 34
        pushKnob4: { number: "23", type: "09", value: "7F", channel: "00" }, // 35
        pushKnob5: { number: "24", type: "09", value: "7F", channel: "00" }, // 36
        pushKnob6: { number: "25", type: "09", value: "7F", channel: "00" }, // 37
        pushKnob7: { number: "26", type: "09", value: "7F", channel: "00" }, // 38
        pushKnob8: { number: "27", type: "09", value: "7F", channel: "00" }, // 39

        pushJog: { number: "65", type: "09", value: "7F", channel: "00" }, // 101

        encoderKnob1: { number: "10", type: "0B", value: "40", channel: "00" }, // 16
        encoderKnob2: { number: "11", type: "0B", value: "40", channel: "00" }, // 17
        encoderKnob3: { number: "12", type: "0B", value: "40", channel: "00" }, // 18
        encoderKnob4: { number: "13", type: "0B", value: "40", channel: "00" }, // 19
        encoderKnob5: { number: "14", type: "0B", value: "40", channel: "00" }, // 20
        encoderKnob6: { number: "15", type: "0B", value: "40", channel: "00" }, // 21
        encoderKnob7: { number: "16", type: "0B", value: "40", channel: "00" }, // 22
        encoderKnob8: { number: "17", type: "0B", value: "40", channel: "00" }, // 23

        jogWheel: { number: "3C", type: "0B", value: "40", channel: "00" }, // 60

        unknown1: { number: "66", type: "09", value: "7F", channel: "00" },
        unknown2: { number: "67", type: "09", value: "7F", channel: "00" },

        touchFader1: { number: "68", type: "09", value: "7F", channel: "00" }, // 104
        touchFader2: { number: "69", type: "09", value: "7F", channel: "00" }, // 105
        touchFader3: { number: "6A", type: "09", value: "7F", channel: "00" }, // 106
        touchFader4: { number: "6B", type: "09", value: "7F", channel: "00" }, // 107
        touchFader5: { number: "6C", type: "09", value: "7F", channel: "00" }, // 108
        touchFader6: { number: "6D", type: "09", value: "7F", channel: "00" }, // 109
        touchFader7: { number: "6E", type: "09", value: "7F", channel: "00" }, // 110
        touchFader8: { number: "6F", type: "09", value: "7F", channel: "00" }, // 111
        touchFader9: { number: "70", type: "09", value: "7F", channel: "00" }, // 112

        fader1: { number: "00", type: "0E", value: "7F", channel: "00" }, // 
        fader2: { number: "00", type: "0E", value: "7F", channel: "01" }, // 
        fader3: { number: "00", type: "0E", value: "7F", channel: "02" }, // 
        fader4: { number: "00", type: "0E", value: "7F", channel: "03" }, // 
        fader5: { number: "00", type: "0E", value: "7F", channel: "04" }, // 
        fader6: { number: "00", type: "0E", value: "7F", channel: "05" }, // 
        fader7: { number: "00", type: "0E", value: "7F", channel: "06" }, // 
        fader8: { number: "00", type: "0E", value: "7F", channel: "07" }, // 
        fader9: { number: "00", type: "0E", value: "7F", channel: "08" }, // 

        jogMoveHorLeft: { number: "62", type: "09", value: "7F", channel: "00" },
        jogMoveHorRight: { number: "63", type: "09", value: "7F", channel: "00" },
        jogMoveVerLeft: { number: "60", type: "09", value: "7F", channel: "00" },
        jogMoveVerRight: { number: "61", type: "09", value: "7F", channel: "00" },

        navigationLeft: { number: "62", type: "09", value: "7F", channel: "00" },//"40 05 00 00 00 00 00 00", // sends arrows
        navigationRight: { number: "62", type: "09", value: "7F", channel: "00" },//"40 04 0F 00 00 00 00 00", // sends arrows
        focusLeft: "50 00 00 00 02 00 00 00",
        focusRight: "50 00 00 00 01 00 00 00",

        jogZoomVerLeft: { number: "64", type: "09", value: "7F", channel: "00" }, // 82
        jogZoomVerRight: { number: "65", type: "09", value: "7F", channel: "00" }, // 83
        jogZoomHorLeft: { number: "66", type: "09", value: "7F", channel: "00" }, // 84
        jogZoomHorRight: { number: "67", type: "09", value: "7F", channel: "00" }, // 85

        masterButton: { number: "00", type: "00", value: "7F", channel: "00" },
        shuffleButton: { number: "00", type: "00", value: "7F", channel: "00" },
        moveVerButton: { number: "00", type: "00", value: "7F", channel: "00" },
        moveHorButton: { number: "00", type: "00", value: "7F", channel: "00" },
        navigationButton: { number: "00", type: "00", value: "7F", channel: "00" },
        focusButton: { number: "00", type: "00", value: "7F", channel: "00" },
        zoomHorButton: { number: "00", type: "00", value: "7F", channel: "00" },
        zoomVerButton: { number: "00", type: "00", value: "7F", channel: "00" },

        soloButton1: { number: "08", type: "09", value: "7F", channel: "00" }, // 8
        soloButton2: { number: "09", type: "09", value: "7F", channel: "00" }, // 9
        soloButton3: { number: "0A", type: "09", value: "7F", channel: "00" }, // 10
        soloButton4: { number: "0B", type: "09", value: "7F", channel: "00" }, // 11
        soloButton5: { number: "0C", type: "09", value: "7F", channel: "00" }, // 12
        soloButton6: { number: "0D", type: "09", value: "7F", channel: "00" }, // 13
        soloButton7: { number: "0E", type: "09", value: "7F", channel: "00" }, // 14
        soloButton8: { number: "0F", type: "09", value: "7F", channel: "00" }, // 15

        muteButton1: { number: "10", type: "09", value: "7F", channel: "00" }, // 16
        muteButton2: { number: "11", type: "09", value: "7F", channel: "00" }, // 17
        muteButton3: { number: "12", type: "09", value: "7F", channel: "00" }, // 18
        muteButton4: { number: "13", type: "09", value: "7F", channel: "00" }, // 19
        muteButton5: { number: "14", type: "09", value: "7F", channel: "00" }, // 20
        muteButton6: { number: "15", type: "09", value: "7F", channel: "00" }, // 21
        muteButton7: { number: "16", type: "09", value: "7F", channel: "00" }, // 22
        muteButton8: { number: "17", type: "09", value: "7F", channel: "00" }, // 23

        selButton1: { number: "18", type: "09", value: "7F", channel: "00" }, // 24
        selButton2: { number: "19", type: "09", value: "7F", channel: "00" }, // 25
        selButton3: { number: "1A", type: "09", value: "7F", channel: "00" }, // 26
        selButton4: { number: "1B", type: "09", value: "7F", channel: "00" }, // 27
        selButton5: { number: "1C", type: "09", value: "7F", channel: "00" }, // 28
        selButton6: { number: "1D", type: "09", value: "7F", channel: "00" }, // 29
        selButton7: { number: "1E", type: "09", value: "7F", channel: "00" }, // 30
        selButton8: { number: "1F", type: "09", value: "7F", channel: "00" } // 31
    };
    let keyOrder = ["leftButton", "rightButton", "bankLeftButton", "bankRightButton", "flipButton", "lockButton", "offButton", "readButton", "writeButton", "trimButton", "latchButton", "touchButton", "rewindButton", "forwardButton", "loopButton", "stopButton", "playButton", "recordButton", "armButton1", "armButton2", "armButton3", "armButton4", "armButton5", "armButton6", "armButton7", "armButton8", "pushKnob1", "pushKnob2", "pushKnob3", "pushKnob4", "pushKnob5", "pushKnob6", "pushKnob7", "pushKnob8", "pushJog", "encoderKnob1", "encoderKnob2", "encoderKnob3", "encoderKnob4", "encoderKnob5", "encoderKnob6", "encoderKnob7", "encoderKnob8", "jogWheel", "unknown1", "unknown2", "touchFader1", "touchFader2", "touchFader3", "touchFader4", "touchFader5", "touchFader6", "touchFader7", "touchFader8", "touchFader9", "fader1", "fader2", "fader3", "fader4", "fader5", "fader6", "fader7", "fader8", "fader9", "jogMoveHorLeft", "jogMoveHorRight", "jogMoveVerLeft", "jogMoveVerRight", "navigationLeft", "navigationRight", "focusLeft", "focusRight", "jogZoomVerLeft", "jogZoomVerRight", "jogZoomHorLeft", "jogZoomHorRight", "masterButton", "shuffleButton", "moveVerButton", "moveHorButton", "navigationButton", "focusButton", "zoomHorButton", "zoomVerButton", "soloButton1", "soloButton2", "soloButton3", "soloButton4", "soloButton5", "soloButton6", "soloButton7", "soloButton8", "muteButton1", "muteButton2", "muteButton3", "muteButton4", "muteButton5", "muteButton6", "muteButton7", "muteButton8", "selButton1", "selButton2", "selButton3", "selButton4", "selButton5", "selButton6", "selButton7", "selButton8"]

    
    let sysexTextArray = []//["F0 1D 03 10 07 21 0E 58 32 6D 65 58 12 7A 03 F7\n"]
    
    let pushMapping = true
    let sysexLine = 1
    let mappingsAdded = 0
    if (pushMapping) {
        for (let i = 1; i <= 80; i++) {
            if (mappingsAdded % 28 == 0) {
                sysexTextArray.push(openSysexMapping(sysexLine))
                sysexLine++
            }
            if (lcdSettings[i]) {
                sysexTextArray.push(createSysexString(lcdSettings[i]))
            }
            else {
                sysexTextArray.push(createSysexString(emptyMapping))
            }
            if (mappingsAdded % 28 == 27) {
                sysexTextArray.push("F7\n")
            }
            mappingsAdded++
        }

        for (let k = 0; k < keyOrder.length; k++) {
            let key = keyOrder[k]
            if (mappingsAdded % 28 == 0) {
                sysexTextArray.push(openSysexMapping(sysexLine))
                sysexLine++
            }
            sysexTextArray.push(createSysexString(deviceSettings[key]))

            if (mappingsAdded % 28 == 27) {
                sysexTextArray.push("F7\n")
            }
            mappingsAdded++
        }

        // we add the encoder push knob buttons 4 more times for each of the function layers          
        keyOrder = ["pushKnob1", "pushKnob2", "pushKnob3", "pushKnob4", "pushKnob5", "pushKnob6", "pushKnob7", "pushKnob8", "encoderKnob1", "encoderKnob2", "encoderKnob3", "encoderKnob4", "encoderKnob5", "encoderKnob6", "encoderKnob7", "encoderKnob8"]
        for (let p = 0; p < 4; p++) {
            for (let k = 0; k < keyOrder.length; k++) {
                let key = keyOrder[k]
                let modifiedSysex = { number: deviceSettings[key].number, type: deviceSettings[key].type, value: deviceSettings[key].value, channel: decimalToHex(p+1) }
                if (mappingsAdded % 28 == 0) {
                    sysexTextArray.push(openSysexMapping(sysexLine))
                    sysexLine++
                }
                sysexTextArray.push(createSysexString(modifiedSysex))

                if (mappingsAdded % 28 == 27) {
                    sysexTextArray.push("F7\n")
                }
                mappingsAdded++
            }
        }
    }


    sysexLine = 1
    mappingsAdded = 0
    for (let i = 1; i <= 80; i++) {
        if (mappingsAdded % 10 == 0) {
            sysexTextArray.push(openSysexName(sysexLine))
            sysexLine++
        }
        if (lcdSettings[i]) {
            sysexTextArray.push(textToHex(lcdSettings[i].name))
        }
        else {
            sysexTextArray.push(textToHex(emptyMapping.name))
        }

        if (mappingsAdded % 10 == 9) {
            sysexTextArray.push("F7\n")
        }
        mappingsAdded++
    }
    //sysexTextArray.push("EF 7F 7F\n")
    //sysexTextArray.push("EC 22 06\n")
    //sysexTextArray.push("F0 1D 03 10 07 24 0E 58 32 6D 65 58 12 7A 03 F7")
    
    let fullSysexText = sysexTextArray.join(' ').replace(/\n /g, '\n')

    sf.clipboard.setText({ text: sysexTextArray.join(' ').replace(/\n /g, '\n') })

    let multipleStringsArray = fullSysexText.split("\n")


    let send = true
    for (let i = 0; i < multipleStringsArray.length; i++) {
        let string = multipleStringsArray[i]

        let stringArray = string.split(" ").map(x => Number("0x" + x))

        if (stringArray.length > 2 && send) {
            sf.midi.send({
                midiBytes: stringArray,
                externalMidiPort: "iCON P1-M V1.07  Port 4"
            })
            sf.wait({ intervalMs: 30 })
        }
    }
}
setNames()
2 Likes

Thatā€™s fantastic. I started mapping where all the various controls are in that sysex stream as well. Iā€™ll compare your P1-M to my P1-Nano and see how different they are.

One quick question - I donā€™t see anything in your code above (on first glance) for controlling the color bars or the bottom parts of the scribble strips. I know those donā€™t run on the ā€œport 4ā€ with all this iMap stuff, but do you happen to have that detail handy somewhere?