motorized fader controller teensy 3.6 issue

Status
Not open for further replies.
Code:
thats my master code:
It looks like the gain values are transmitted as NRPN controllers. The faderpanel expects values ranging 0-1023 so you would need to map the incoming data range to 0-1023.

Code:
my midi fader value is the "int Gain" so should it be like this ? :
If you just changed the name it should work. But why change the name at all at this stage? You risk introducing copy/paste or search/replace errors. I like to first get things to work and then tidy up the naming and structure.

The TARGET and FOLLOW modes differ only in the way the PID values are set. You'll have to see what works best for you. It may also be necessary to tune the PID parameters as your setup differs from mine.

Code:
here is the slave code the only change i have made is in (I2C_PULLUP_EXT, 1000000) :
Of course all the pin numbers have to match your PCB layout, this is something you'll have to check yourself.

At what stage are you? Have you tested anything yet?

I would start testing without the motor connected and first check if the I2C communication is OK and the values of the faders can be read. Use the serial monitor on the master to display the fader values. See if you get stable and values ranging 0-1023. You might need to clean up the cabling, I would certainly separate the analog fader signal from the motor supply.
To test the motors fill the Gain[] with 500 and see if the faders move half way. Next check if you have the correct values in the Gain[] if the DAW sends something. Then start testing everything together.
 
Code:
thats my master code:
It looks like the gain values are transmitted as NRPN controllers. The faderpanel expects values ranging 0-1023 so you would need to map the incoming data range to 0-1023.

Code:
my midi fader value is the "int Gain" so should it be like this ? :
If you just changed the name it should work. But why change the name at all at this stage? You risk introducing copy/paste or search/replace errors. I like to first get things to work and then tidy up the naming and structure.

The TARGET and FOLLOW modes differ only in the way the PID values are set. You'll have to see what works best for you. It may also be necessary to tune the PID parameters as your setup differs from mine.

Code:
here is the slave code the only change i have made is in (I2C_PULLUP_EXT, 1000000) :
Of course all the pin numbers have to match your PCB layout, this is something you'll have to check yourself.

At what stage are you? Have you tested anything yet?

I would start testing without the motor connected and first check if the I2C communication is OK and the values of the faders can be read. Use the serial monitor on the master to display the fader values. See if you get stable and values ranging 0-1023. You might need to clean up the cabling, I would certainly separate the analog fader signal from the motor supply.
To test the motors fill the Gain[] with 500 and see if the faders move half way. Next check if you have the correct values in the Gain[] if the DAW sends something. Then start testing everything together.

hi gerrit
i have right now everything soldered and connected to the pcb i checked all the connections and i checked all pins against the schem to be sure im connected ok.
about the 0 to 1024 maping i understand what u saying and ill try to figure out how i map it right.

right now in using: " value = map(value, 0, 127, 127, 0);"
can i just do?: "value = map(value, 0, 127, 1024, 0);"

i map it allready coz my faders r assembled opposite way
i did not understand what you saying about changing names... do you talk about the "int Gain" not to switch it for now with "faderTarget" and 'faderMove" till i do all test?
thos names r used just 1 time in this stage. so its not should be a problem. i hope im getiing you right.

right now i missing 1 hbrige and the TCA i2c mux for 8 displays
so i can test 6 faders and all 31 buttons. ill try later on today to do the serial monitor tests to see if i get the right values in master and slave.
thanks
rota
 
Code:
right now in using: " value = map(value, 0, 127, 127, 0);"
can i just do?: "value = map(value, 0, 127, 1024, 0);"

i map it allready coz my faders r assembled opposite way

Yes (1024 should be 1023).

Code:
i did not understand what you saying about changing names... do you talk about the "int Gain" not to switch it for now with "faderTarget" and 'faderMove" till i do all test?
thos names r used just 1 time in this stage. so its not should be a problem. i hope im getiing you right.
Yes, it's a working function that has been tested, why change it. Like they say 'if it ain't broke, don't fix it'.

To prevent an overflow of data you should remove the fader update from the control event handler and put it in a timer (like the one used for the fader polling). There's no need to send fader update commands as fast as they come in. Just update the values in the array and send the content to the faders every 20ms or so.
 
i tryed to do small test now and i got all values from midi to 0-1023 ok in serial monitor.
then i connected the motors to test it but only fader 5(4) and 7(6) moves all the way up and stay there.
no matter what fader i move in software its allways send fader 5 and 7 to top.
im trying to figure out why? i see in the slave code under setup that "faderTarget=500;"
it should be 500? not 0?
right now i im checking again all the up down pins schematic to see if there is any mistake in the slave pin configuration.

here is the master code i used:
Code:
#include <i2c_t3.h>
#include <MIDIUSB.h>

#define TARGET    0x10              // All faders to target value as fast as possible. PID state is cleared with each command.
#define FOLLOW    0x20
#define MESSAGE_LEN 17 

elapsedMicros sinceFaderRead;                         // timer for fader check
unsigned int  faderReadInterval = 1000;
elapsedMicros faderTouchCount;

uint8_t panelAddress = 0x66;

byte faderValueData[MESSAGE_LEN];    // 16 bytes for values, 1 byte for touch
byte faderTargetData[MESSAGE_LEN];   // 16 for target values, 1 byte for indicating which faders should be moved.

boolean faderTouched[8];                              // boolean indicating if fader is touched
int     faderValue[8];                                // fader value ranging 0 to 1023
int     faderTarget[8];                               // fader target value ranging 0 to 1023
int     faderMove[8];                               // fader target value ranging 0 to 1023


int hi[8];
int lo[8];
int Gain[8];


void setup() {
//**** I2C *****
  // Setup for Master mode, pins 18/19, external pullups, 1MHz, 200ms default timeout
  Wire.begin(I2C_MASTER, 0x00, I2C_PINS_18_19, I2C_PULLUP_EXT, 1000000);
  Wire.setDefaultTimeout(200000); // 200ms
  usbMIDI.setHandleControlChange(myControlChange);
  

}

void loop() {
  usbMIDI.read();
  //  substract interval to maintain schedule
  // faders
  if (sinceFaderRead >=faderReadInterval) {
    sinceFaderRead = sinceFaderRead - faderReadInterval;
    getFaderData();
    //if (linkMode!=LINK_OFF) handleLinkMode();
   
  }
}
// get fader values and touch status
void getFaderData() {
  size_t idx;
  int incomingFaderValue[16];    // 
  int touchCount=0;
  //ZeusPattern *currentPattern=&sequencerChannel[selectedChannel].pattern[sequencerChannel[selectedChannel].selectedPattern];
  // Read from Slave
  
    Wire.requestFrom(panelAddress, (size_t)MESSAGE_LEN); // Read from Slave
    // Check if error occured
    if(Wire.getError())
        Serial.print("FAIL\n");
    else
    {
      // If no error then read Rx data into buffer
      idx = 0;
      while(Wire.available()) faderValueData[idx++] = Wire.readByte();
      // Process data in buffer
      for (int i=0;i<8;i++){
      // combine bytes back to integers and put in temporary array
      incomingFaderValue[i]= faderValueData[(i*2)];
      incomingFaderValue[i]= incomingFaderValue[i] << 8 | faderValueData[(i*2)+1];
      // fill touch boolean array with last byte
      faderTouched[i*8] = 1 & faderValueData[16] >> i;
      if (faderTouched[i*8]) touchCount++;
      // check if faderValue needs to be updated
      if (incomingFaderValue[i]!=faderValue[i*8] and faderTouched[i*8]){
        faderValue[i*8]=incomingFaderValue[i];
//        currentPattern->step[i+j*8].value[selectedParameter]=map(faderValue[i+j*8],0,1023,parameterLimit[selectedParameter][0],parameterLimit[selectedParameter][1]);
//        updateDisplayData=true;
//        Serial.print("Fader ");
//        Serial.print(i+j*8);
//        Serial.print("\t");
//        Serial.println(faderValue[i+j*8]);     
    }
  }
  faderTouchCount=touchCount;
  }
}

// Send command with faderTarget array values to fader, TARGET or FOLLOW mode
void sendFaderCommand(byte mode){
  size_t idx;
  faderTargetData[16]=0;
    for (int i=0;i<8;i++){
      faderTargetData[(i*2)]=(Gain[i*8] >> 8) & 0xFF;                // split Target integers over two bytes in array
      faderTargetData[(i*2)+1]=Gain[i*8] & 0xFF;
      faderTargetData[16] |= Gain[i*8] << i;                           // fill last byte in array with faderMove boolean array
    }
    Wire.beginTransmission(panelAddress);                                  // Transmit to Slave
    Wire.write(mode);                                                         // Send mode (TARGET or FOLLOW)
    for(idx = 0; idx < MESSAGE_LEN; idx++) Wire.write(faderTargetData[idx]);  // Write data to I2C Tx buffer
    Wire.endTransmission();                                                   // Transmit to Slave
}
void myControlChange(byte channel, byte control, byte value) {               // blink the LED a number of times

  value = map(value, 0, 127, 1023, 0);
  //INT FADERS GAINS
  if (control == 32) {
    hi[0] = value;
  }
  if (control == 0) {
    lo[0] = value;
    Gain[0] = (hi[0] >> 7) + lo[0];
  }

  if (control == 33) {
    hi[1] = value;
  }
  if (control == 1) {
    lo[1] = value;
    Gain[1] = (hi[1] >> 7) + lo[1];
  }
  if (control == 34) {
    hi[2] = value;
  }
  if (control == 2) {
    lo[2] = value;
    Gain[2] = (hi[2] >> 7) + lo[2];
  }

  if (control == 35) {
    hi[3] = value;
  }
  if (control == 3) {
    lo[3] = value;
    Gain[3] = (hi[3] >> 7) + lo[3];
  }

  if (control == 36) {
    hi[4] = value;
  }
  if (control == 4) {
    lo[4] = value;
    Gain[4] = (hi[4] >> 7) + lo[4];
  }

  if (control == 37) {
    hi[5] = value;
  }
  if (control == 5) {
    lo[5] = value;
    Gain[5] = (hi[5] >> 7) + lo[5];
  }

  if (control == 38) {
    hi[6] = value;
  }
  if (control == 6) {
    lo[6] = value;
    Gain[6] = (hi[6] >> 7) + lo[6];
  }

  if (control == 39) {
    hi[7] = value;
  }
  if (control == 7) {
    lo[7] = value;
    Gain[7] = (hi[7] >> 7) + lo[7];
  }
sendFaderCommand(TARGET);
}

about the timer u talk about im not sure i know how to write it can u give me example please?
did u mean to add this row : Wire.setDefaultTimeout(2000); // 20ms
to the slave in this way?

Wire.begin(I2C_SLAVE, PANEL_ADDR, I2C_PINS_33_34, I2C_PULLUP_EXT, 1000000);
Wire.setDefaultTimeout(2000); // 20ms
 
Last edited:
i tryed to do small test now and i got all values from midi to 0-1023 ok in serial monitor.


about the timer u talk about im not sure i know how to write it can u give me example please?

What do you mean by values from midi? The first thing is to ensure that all the faders send correct values to the master and that the number matches (fader one on the panel matches the index 0 in the array).

The timer is just like the sinceFaderRead timer, you can make a copy of it (variables and code in the loop) and name it sinceFaderWrite with a corresponding sinceFaderWriteInterval. It's the standard way of using elapsedMillis for timing events.

This declaration is wrong:
Code:
elapsedMicros faderTouchCount;
This should be an int

Faders not moving or getting stuck at one end is probably due to wiring errors.
 
ok i think i got ur point

Code:
#include <i2c_t3.h>
#include <MIDIUSB.h>

#define TARGET    0x10              // All faders to target value as fast as possible. PID state is cleared with each command.
#define FOLLOW    0x20
#define MESSAGE_LEN 17 

elapsedMicros sinceFaderRead;                         // timer for fader check
unsigned int  faderReadInterval = 1000;

elapsedMicros sinceFaderWrite;                         // timer for fader check
unsigned int   faderWriteInterval = 20000;
int faderTouchCount;

uint8_t panelAddress = 0x66;

byte faderValueData[MESSAGE_LEN];    // 16 bytes for values, 1 byte for touch
byte faderTargetData[MESSAGE_LEN];   // 16 for target values, 1 byte for indicating which faders should be moved.

boolean faderTouched[8];                              // boolean indicating if fader is touched
int     faderValue[8];                                // fader value ranging 0 to 1023
int     faderTarget[8];                               // fader target value ranging 0 to 1023
int     faderMove[8];                               // fader target value ranging 0 to 1023


int hi[8];
int lo[8];
int Gain[8];


void setup() {
//**** I2C *****
  // Setup for Master mode, pins 18/19, external pullups, 1MHz, 200ms default timeout
  Wire.begin(I2C_MASTER, 0x00, I2C_PINS_18_19, I2C_PULLUP_EXT, 1000000);
  Wire.setDefaultTimeout(200000); // 200ms
  usbMIDI.setHandleControlChange(myControlChange);
  

}

void loop() {
  usbMIDI.read();
  //  substract interval to maintain schedule
  // faders
  if (sinceFaderRead >=faderReadInterval) {
    sinceFaderRead = sinceFaderRead - faderReadInterval;
    getFaderData();
  }
  if (sinceFaderWrite >=faderWriteInterval) {
    sinceFaderWrite = sinceFaderWrite - faderWriteInterval;
    sendFaderCommand(TARGET);
    //if (linkMode!=LINK_OFF) handleLinkMode();  
  }
}
// get fader values and touch status
void getFaderData() {
  size_t idx;
  int incomingFaderValue[16];    // 
  int touchCount=0;
  //ZeusPattern *currentPattern=&sequencerChannel[selectedChannel].pattern[sequencerChannel[selectedChannel].selectedPattern];
  // Read from Slave
  
    Wire.requestFrom(panelAddress, (size_t)MESSAGE_LEN); // Read from Slave
    // Check if error occured
    if(Wire.getError())
        Serial.print("FAIL\n");
    else
    {
      // If no error then read Rx data into buffer
      idx = 0;
      while(Wire.available()) faderValueData[idx++] = Wire.readByte();
      // Process data in buffer
      for (int i=0;i<8;i++){
      // combine bytes back to integers and put in temporary array
      incomingFaderValue[i]= faderValueData[(i*2)];
      incomingFaderValue[i]= incomingFaderValue[i] << 8 | faderValueData[(i*2)+1];
      // fill touch boolean array with last byte
      faderTouched[i*8] = 1 & faderValueData[16] >> i;
      if (faderTouched[i*8]) touchCount++;
      // check if faderValue needs to be updated
      if (incomingFaderValue[i]!=faderValue[i*8] and faderTouched[i*8]){
        faderValue[i*8]=incomingFaderValue[i];
//        currentPattern->step[i+j*8].value[selectedParameter]=map(faderValue[i+j*8],0,1023,parameterLimit[selectedParameter][0],parameterLimit[selectedParameter][1]);
//        updateDisplayData=true;
//        Serial.print("Fader ");
//        Serial.print(i+j*8);
//        Serial.print("\t");
//        Serial.println(faderValue[i+j*8]);     
    }
  }
  faderTouchCount=touchCount;
  }
}

// Send command with faderTarget array values to fader, TARGET or FOLLOW mode
void sendFaderCommand(byte mode){
  size_t idx;
  faderTargetData[16]=0;
    for (int i=0;i<8;i++){
      faderTargetData[(i*2)]=(Gain[i*8] >> 8) & 0xFF;                // split Target integers over two bytes in array
      faderTargetData[(i*2)+1]=Gain[i*8] & 0xFF;
      faderTargetData[16] |= Gain[i*8] << i;                           // fill last byte in array with faderMove boolean array
    }
    Wire.beginTransmission(panelAddress);                                  // Transmit to Slave
    Wire.write(mode);                                                         // Send mode (TARGET or FOLLOW)
    for(idx = 0; idx < MESSAGE_LEN; idx++) Wire.write(faderTargetData[idx]);  // Write data to I2C Tx buffer
    Wire.endTransmission();                                                   // Transmit to Slave
}
void myControlChange(byte channel, byte control, byte value) {               // blink the LED a number of times

  value = map(value, 0, 127, 1023, 0);
  //INT FADERS GAINS
  if (control == 32) {
    hi[0] = value;
  }
  if (control == 0) {
    lo[0] = value;
    Gain[0] = (hi[0] >> 7) + lo[0];
  }

  if (control == 33) {
    hi[1] = value;
  }
  if (control == 1) {
    lo[1] = value;
    Gain[1] = (hi[1] >> 7) + lo[1];
  }
  if (control == 34) {
    hi[2] = value;
  }
  if (control == 2) {
    lo[2] = value;
    Gain[2] = (hi[2] >> 7) + lo[2];
  }

  if (control == 35) {
    hi[3] = value;
  }
  if (control == 3) {
    lo[3] = value;
    Gain[3] = (hi[3] >> 7) + lo[3];
  }

  if (control == 36) {
    hi[4] = value;
  }
  if (control == 4) {
    lo[4] = value;
    Gain[4] = (hi[4] >> 7) + lo[4];
  }

  if (control == 37) {
    hi[5] = value;
  }
  if (control == 5) {
    lo[5] = value;
    Gain[5] = (hi[5] >> 7) + lo[5];
  }

  if (control == 38) {
    hi[6] = value;
  }
  if (control == 6) {
    lo[6] = value;
    Gain[6] = (hi[6] >> 7) + lo[6];
  }

  if (control == 39) {
    hi[7] = value;
  }
  if (control == 7) {
    lo[7] = value;
    Gain[7] = (hi[7] >> 7) + lo[7];
  }
}

thats the way u meant to use the timer?
 
What do you mean by values from midi? The first thing is to ensure that all the faders send correct values to the master and that the number matches (fader one on the panel matches the index 0 in the array).

The timer is just like the sinceFaderRead timer, you can make a copy of it (variables and code in the loop) and name it sinceFaderWrite with a corresponding sinceFaderWriteInterval. It's the standard way of using elapsedMillis for timing events.

This declaration is wrong:
Code:
elapsedMicros faderTouchCount;
This should be an int

Faders not moving or getting stuck at one end is probably due to wiring errors.

im trying to test now only faders 1 2 3 4 (only 2 hbriges) and its not acting well
i print in the slave (faderTarget); tahts what i get:
Screen Shot 2019-03-03 at 10.59.05 PM.jpg

i send data for test in that way:
Screen Shot 2019-03-03 at 11.02.13 PM.jpg

fader 1 not move at all fader 2 moves somtimes in strang way only if i touch it and fader 3-4 moves if i touch them to buttom and not stop.
i checked all connections and pin in the schematic everithing is connect to the right place i tryed allso to swip the up down pins in the slave code but it dident solve the prob. i allso sweep Between the hbrige modules and i checked resistance to be sure its not dammeged hbrige but its till act the same so i know its code issue .
do i send the data test in the right way? what im missing here?
 
ok i think i got ur point

thats the way u meant to use the timer?

Yes, exactly.

im trying to test now only faders 1 2 3 4 (only 2 hbriges) and its not acting well
i print in the slave (faderTarget); tahts what i get:
View attachment 16047

i send data for test in that way:
View attachment 16048

fader 1 not move at all fader 2 moves somtimes in strang way only if i touch it and fader 3-4 moves if i touch them to buttom and not stop.
i checked all connections and pin in the schematic everithing is connect to the right place i tryed allso to swip the up down pins in the slave code but it dident solve the prob. i allso sweep Between the hbrige modules and i checked resistance to be sure its not dammeged hbrige but its till act the same so i know its code issue .
do i send the data test in the right way? what im missing here?


faderMove is a boolean, it should contain 0 for faders that don't need to move and 1 for faders that need to move (it should work though because 500 is >0).

You really have to start with reading the faders, not with sending something to them. Uncomment the serial.print lines in the getFaderData function on the master and see if you get values from the slave, only proceed if this test is successful.
 
Yes, exactly.



faderMove is a boolean, it should contain 0 for faders that don't need to move and 1 for faders that need to move (it should work though because 500 is >0).

You really have to start with reading the faders, not with sending something to them. Uncomment the serial.print lines in the getFaderData function on the master and see if you get values from the slave, only proceed if this test is successful.


hoo ok i didnt know that... ill change faderMove to boolean form int and ill put the commend to from each midi handle fader to send 1 every time the fader move.
ill do it later and ill try to do the test now for the getfaderdata to see if its all good.

in the last test i got success to move fader no 1 from software it moves ok (but not smooth) and allso maped ok and move in the right direction. but only fader 1 coz i didnt configure it right (now when i know how getfaderdata works here).

all the 8 faders responding ok from 1023 to 0 in when i print faderTarget in serial monitor
Code:
  for (int i=0; i<8; i++) {   
    faderTarget[i] = Gain[i];
    faderTarget[i] = map(faderTarget[i], 0, 1023, 1023, 0);
 
hoo ok i didnt know that... ill change faderMove to boolean form int and ill put the commend to from each midi handle fader to send 1 every time the fader move.
ill do it later and ill try to do the test now for the getfaderdata to see if its all good.

in the last test i got success to move fader no 1 from software it moves ok (but not smooth) and allso maped ok and move in the right direction. but only fader 1 coz i didnt configure it right (now when i know how getfaderdata works here).

all the 8 faders responding ok from 1023 to 0 in when i print faderTarget in serial monitor
Code:
  for (int i=0; i<8; i++) {   
    faderTarget[i] = Gain[i];
    faderTarget[i] = map(faderTarget[i], 0, 1023, 1023, 0);

Of course it does, if Gain=0 then after the map all values will be 1023.
Forget about faderTarget for now, important is if the values in faderValue are correct and if the touch signals are correct because if they're not the PID control will not work correctly.
 
Of course it does, if Gain=0 then after the map all values will be 1023.
Forget about faderTarget for now, important is if the values in faderValue are correct and if the touch signals are correct because if they're not the PID control will not work correctly.


ok i test the getFaderData in master i moved faders 1 2 3 4 and i get the 0-1023 value of each fader just fine.
how do i check the incoming touch value in master?
 
ok i test the getFaderData in master i moved faders 1 2 3 4 and i get the 0-1023 value of each fader just fine.
how do i check the incoming touch value in master?

So far, so good but why not check the other 4 faders? Reading the faders is not dependent on whether the H-bridges are connected.
just add Serial.print(faderTouched) to the print statements.
Is the value stable, if the fader is touched do the numbers change? You should be able to set the value to the last digit, e.g. move the fader to exactly 500 (or any other value between 0 and 1023).

You also need to remove all the '*8' from the code in getFaderData() and sendFaderCommand(), you removed the j iterator (for multiple panels) but forgot to remove the associated *8 (8 faders on a panel). The way it is now the code will try to address array indexes that aren't there and this can cause the weirdest of errors.

Also from
Code:
if (incomingFaderValue[i]!=faderValue[i*8] and faderTouched[i*8])
remove 'and faderTouched' so any change will be displayed regardless of the touch status of the faders. This will help to show if values are actually stable. It should look like this (the '*8' removed):
Code:
if (incomingFaderValue[i]!=faderValue[i])
You do need to put 'and faderTouched' back later if you control the faders from the DAW to prevent feedback of the value.
 
i got success in sending all 8 fader values from fader to DAW all the faders moving good in the software.
and yes i figure it out that i need to move all *8 now in using this code to send midi to the DAW i know i can short it but i dont know how to do it so i leave it like that for now
Code:
      // check if faderValue needs to be updated
      if (incomingFaderValue[i]!=faderValue[i] and faderTouched[i]){
        faderValue[i]=incomingFaderValue[i];
        faderValue[i] = map(faderValue[i], 0, 1023, 127, 0);

        usbMIDI.sendControlChange(32, faderValue[0] , 1);
        usbMIDI.sendControlChange(0, faderValue[0] , 1);
        
        usbMIDI.sendControlChange(33, faderValue[1] , 1);
        usbMIDI.sendControlChange(1, faderValue[1] , 1);
        
        usbMIDI.sendControlChange(34, faderValue[2] , 1);
        usbMIDI.sendControlChange(2, faderValue[2] , 1);

        usbMIDI.sendControlChange(35, faderValue[3] , 1);
        usbMIDI.sendControlChange(3, faderValue[3] , 1);
       
        usbMIDI.sendControlChange(36, faderValue[4] , 1);
        usbMIDI.sendControlChange(4, faderValue[4] , 1);
        
        usbMIDI.sendControlChange(37, faderValue[5] , 1);
        usbMIDI.sendControlChange(5, faderValue[5] , 1);
        
        usbMIDI.sendControlChange(38, faderValue[6] , 1);
        usbMIDI.sendControlChange(6, faderValue[6] , 1);

        usbMIDI.sendControlChange(39, faderValue[7] , 1);
        usbMIDI.sendControlChange(7, faderValue[7] , 1);

ill try now to understtand the touch commend and to see how do i send it as sysex to the DAW.

the master code now looks like that

Code:
#include <i2c_t3.h>
#include <MIDIUSB.h>

#define TARGET    0x10              // All faders to target value as fast as possible. PID state is cleared with each command.
#define FOLLOW    0x20
#define MESSAGE_LEN 17 

elapsedMicros sinceFaderRead;                         // timer for fader check
unsigned int  faderReadInterval = 1000;

elapsedMicros sinceFaderWrite;                         // timer for fader check
unsigned int   faderWriteInterval = 20000;
int faderTouchCount;

uint8_t panelAddress = 0x66;

byte faderValueData[MESSAGE_LEN];    // 16 bytes for values, 1 byte for touch
byte faderTargetData[MESSAGE_LEN];   // 16 for target values, 1 byte for indicating which faders should be moved.

boolean faderTouched[8];                              // boolean indicating if fader is touched
int     faderValue[8];                                // fader value ranging 0 to 1023
int     faderTarget[8];                               // fader target value ranging 0 to 1023
int faderMove[8];                               // fader target value ranging 0 to 1023


int hi[8];
int lo[8];
int Gain[8];


void setup() {
//**** I2C *****
  // Setup for Master mode, pins 18/19, external pullups, 1MHz, 200ms default timeout
  Wire.begin(I2C_MASTER, 0x00, I2C_PINS_18_19, I2C_PULLUP_EXT, 1000000);
  Wire.setDefaultTimeout(200000); // 200ms
  usbMIDI.setHandleControlChange(myControlChange);
  

}

void loop() {
  usbMIDI.read();
  //  substract interval to maintain schedule
  // faders
  if (sinceFaderRead >=faderReadInterval) {
    sinceFaderRead = sinceFaderRead - faderReadInterval;
    getFaderData();
  }
  if (sinceFaderWrite >=faderWriteInterval) {
    sinceFaderWrite = sinceFaderWrite - faderWriteInterval;
    sendFaderCommand(TARGET);
    //if (linkMode!=LINK_OFF) handleLinkMode();  
  }
}
// get fader values and touch status
void getFaderData() {
  size_t idx;
  int incomingFaderValue[16];    // 
  int touchCount=0;
  //ZeusPattern *currentPattern=&sequencerChannel[selectedChannel].pattern[sequencerChannel[selectedChannel].selectedPattern];
  // Read from Slave
  
    Wire.requestFrom(panelAddress, (size_t)MESSAGE_LEN); // Read from Slave
    // Check if error occured
    if(Wire.getError())
        Serial.print("FAIL\n");
    else
    {
      // If no error then read Rx data into buffer
      idx = 0;
      while(Wire.available()) faderValueData[idx++] = Wire.readByte();
      // Process data in buffer
      for (int i=0;i<8;i++){
      // combine bytes back to integers and put in temporary array
      incomingFaderValue[i]= faderValueData[(i*2)];
      incomingFaderValue[i]= incomingFaderValue[i] << 8 | faderValueData[(i*2)+1];
      // fill touch boolean array with last byte
      faderTouched[i] = 1 & faderValueData[16] >> i;
      if (faderTouched[i]) touchCount++;
      //Serial.println(faderTouched[i*8]);     
       

      // check if faderValue needs to be updated
      if (incomingFaderValue[i]!=faderValue[i] and faderTouched[i]){
        faderValue[i]=incomingFaderValue[i];
        faderValue[i] = map(faderValue[i], 0, 1023, 127, 0);

        usbMIDI.sendControlChange(32, faderValue[0] , 1);
        usbMIDI.sendControlChange(0, faderValue[0] , 1);
        
        usbMIDI.sendControlChange(33, faderValue[1] , 1);
        usbMIDI.sendControlChange(1, faderValue[1] , 1);
        
        usbMIDI.sendControlChange(34, faderValue[2] , 1);
        usbMIDI.sendControlChange(2, faderValue[2] , 1);

        usbMIDI.sendControlChange(35, faderValue[3] , 1);
        usbMIDI.sendControlChange(3, faderValue[3] , 1);
       
        usbMIDI.sendControlChange(36, faderValue[4] , 1);
        usbMIDI.sendControlChange(4, faderValue[4] , 1);
        
        usbMIDI.sendControlChange(37, faderValue[5] , 1);
        usbMIDI.sendControlChange(5, faderValue[5] , 1);
        
        usbMIDI.sendControlChange(38, faderValue[6] , 1);
        usbMIDI.sendControlChange(6, faderValue[6] , 1);

        usbMIDI.sendControlChange(39, faderValue[7] , 1);
        usbMIDI.sendControlChange(7, faderValue[7] , 1);
       // currentPattern->step[i+j*8].value[selectedParameter]=map(faderValue[i+j*8],0,1023,parameterLimit[selectedParameter][0],parameterLimit[selectedParameter][1]);
       // updateDisplayData=true;
        Serial.print("Fader ");
        Serial.print(i);
        Serial.print("\t");
        Serial.println(faderValue[i]);     
    }
  }
  faderTouchCount=touchCount;
  }
}

// Send command with faderTarget array values to fader, TARGET or FOLLOW mode
void sendFaderCommand(byte mode){
  size_t idx;
  faderTargetData[16]=0;
    for (int i=0;i<8;i++){
      faderTargetData[(i*2)]=(faderTarget[i] >> 8) & 0xFF;                // split Target integers over two bytes in array
      faderTargetData[(i*2)+1]=faderTarget[i] & 0xFF;
      faderTargetData[16] |= faderMove[i] << i;                           // fill last byte in array with faderMove boolean array
    }
    Wire.beginTransmission(panelAddress);                                  // Transmit to Slave
    Wire.write(mode);                                                         // Send mode (TARGET or FOLLOW)
    for(idx = 0; idx < MESSAGE_LEN; idx++) Wire.write(faderTargetData[idx]);  // Write data to I2C Tx buffer
    Wire.endTransmission();                                                   // Transmit to Slave
}
void myControlChange(byte channel, byte control, byte value) {               // blink the LED a number of times

  //value = map(value, 0, 127, 1023, 0);
  //INT FADERS GAINS
  if (control == 32) {
    hi[0] = value;
  }
  if (control == 0) {
    lo[0] = value;
    Gain[0] = (hi[0] >> 7) + lo[0]*8.05;
  }

  if (control == 33) {
    hi[1] = value;
  }
  if (control == 1) {
    lo[1] = value;
    Gain[1] = (hi[1] >> 7) + lo[1]*8.05;
  }
  if (control == 34) {
    hi[2] = value;
  }
  if (control == 2) {
    lo[2] = value;
    Gain[2] = (hi[2] >> 7) + lo[2]*8.05;
  }

  if (control == 35) {
    hi[3] = value;
  }
  if (control == 3) {
    lo[3] = value;
    Gain[3] = (hi[3] >> 7) + lo[3]*8.05;
  }

  if (control == 36) {
    hi[4] = value;
  }
  if (control == 4) {
    lo[4] = value;
    Gain[4] = (hi[4] >> 7) + lo[4]*8.05;
  }

  if (control == 37) {
    hi[5] = value;
  }
  if (control == 5) {
    lo[5] = value;
    Gain[5] = (hi[5] >> 7) + lo[5]*8.05;
  }

  if (control == 38) {
    hi[6] = value;
  }
  if (control == 6) {
    lo[6] = value;
    Gain[6] = (hi[6] >> 7) + lo[6]*8.05;
  }

  if (control == 39) {
    hi[7] = value;
  }
  if (control == 7) {
    lo[7] = value;
    Gain[7] = (hi[7] >> 7) + lo[7]*8.05;
  }
  for (int i=0; i<8; i++) {       
  //  faderTarget[i] = Gain[i];
  //  faderTarget[i] = map(faderTarget[i], 0, 1023, 1023, 0);
   // Serial.println(faderTarget[i]);
   // Serial.println(faderMove[i]);
  }
}
 
i tested the touch sensor in all faders its looks ok its send me 1 when i touch the fader and move it
Screen Shot 2019-03-04 at 3.02.51 PM.jpg
Screen Shot 2019-03-04 at 3.04.06 PM.jpg

i have now issue becose i put map in this void the fader touch read got mass so i move the map for now and the test looks ok

this is test with map and 'and faderTouched) its looks ok and all fader in DAW moving
Screen Shot 2019-03-04 at 3.20.11 PM.jpg
Screen Shot 2019-03-04 at 3.17.02 PM.jpg


i tested allso some writing automations to the DAW and its looks good and act ok for now without configoring any touch massage to to software.
i know HUI protocol need to send touch and release commend to DAW ill check it out later if i need it at all.
 
Last edited:
Good to see you're making progress!

yap with many thanks to you its start to get life:)
you think its ok to go for sending data to faders now?
i print serial print to check if the data comes for midi is ok and all 8 fader data is in range of 0 - 1023.
whats the best way to send it with the combination of faderMove?
 
yap with many thanks to you its start to get life:)
you think its ok to go for sending data to faders now?
i print serial print to check if the data comes for midi is ok and all 8 fader data is in range of 0 - 1023.
whats the best way to send it with the combination of faderMove?

Yes, if the values are stable and the touch is OK it is time to test the motor control.

I would suggest to set the faderMove for a particular fader to 1 (true) if there's incoming data from the DAW for that fader. All faderMove values can be reset if the data had been sent to the panel. You should also add a boolean 'updateFaders' which is set to true if there is incoming data and set to false if the data has been sent. In the timer for the fader command add an if statement to only send the command if updateFaders is true, this prevents sending unnecessary commands.
 
so i did some tests with this code
Code:
#include <i2c_t3.h>
#include <MIDIUSB.h>

#define TARGET    0x10              // All faders to target value as fast as possible. PID state is cleared with each command.
#define FOLLOW    0x20
#define MESSAGE_LEN 17 

elapsedMicros sinceFaderRead;                         // timer for fader check
unsigned int  faderReadInterval = 1000;

elapsedMicros sinceFaderWrite;                         // timer for fader check
unsigned int   faderWriteInterval = 20000;
int faderTouchCount;

uint8_t panelAddress = 0x66;

byte faderValueData[MESSAGE_LEN];    // 16 bytes for values, 1 byte for touch
byte faderTargetData[MESSAGE_LEN];   // 16 for target values, 1 byte for indicating which faders should be moved.

boolean faderTouched[8];                              // boolean indicating if fader is touched
int     faderValue[8];                                // fader value ranging 0 to 1023
int     faderTarget[8];                               // fader target value ranging 0 to 1023
boolean faderMove[8];                               // fader target value ranging 0 to 1023


int hi[8];
int lo[8];
int Gain[8];


void setup() {
//**** I2C *****
  // Setup for Master mode, pins 18/19, external pullups, 1MHz, 200ms default timeout
  Wire.begin(I2C_MASTER, 0x00, I2C_PINS_18_19, I2C_PULLUP_EXT, 1000000);
  Wire.setDefaultTimeout(200000); // 200ms
  usbMIDI.setHandleControlChange(myControlChange);
  
}

void loop() {
  usbMIDI.read();
  //  substract interval to maintain schedule
  // faders
  if (sinceFaderRead >=faderReadInterval) {
    sinceFaderRead = sinceFaderRead - faderReadInterval;
    getFaderData();
  }
  if (sinceFaderWrite >=faderWriteInterval) {
    sinceFaderWrite = sinceFaderWrite - faderWriteInterval;
    sendFaderCommand(TARGET);
  }
}
// get fader values and touch status
void getFaderData() {
  size_t idx;
  int incomingFaderValue[16];    // 
  int touchCount=0;
  // Read from Slave
  
    Wire.requestFrom(panelAddress, (size_t)MESSAGE_LEN); // Read from Slave
    // Check if error occured
    if(Wire.getError())
        Serial.print("FAIL\n");
    else
    {
      // If no error then read Rx data into buffer
      idx = 0;
      while(Wire.available()) faderValueData[idx++] = Wire.readByte();
      // Process data in buffer
      for (int i=0;i<8;i++){
    // combine bytes back to integers and put in temporary array
    incomingFaderValue[i]= faderValueData[(i*2)];
    incomingFaderValue[i]= incomingFaderValue[i] << 8 | faderValueData[(i*2)+1];
    // fill touch boolean array with last byte
    faderTouched[i] = 1 & faderValueData[16] >> i;
    if (faderTouched[i]) touchCount++;
       

      // check if faderValue needs to be updated
      if (incomingFaderValue[i]!=faderValue[i] and faderTouched[i]){
      faderValue[i]=incomingFaderValue[i];
      faderValue[i] = map(faderValue[i], 0, 1023, 127, 0);
      for (int j=0; j<8;j++){
        usbMIDI.sendControlChange(j+32, faderValue[j] , 1);
        usbMIDI.sendControlChange(j, faderValue[j] , 1);
      } 
      Serial.print("Fader ");
      Serial.print(i);
      Serial.print("\t");
      Serial.println(faderValue[i]);     
      Serial.print(faderTouched[i]);
    }
  }
  faderTouchCount=touchCount;
  }
}
// Send command with faderTarget array values to fader, TARGET or FOLLOW mode
void sendFaderCommand(byte mode){
  size_t idx;
  faderTargetData[16]=0;
    for (int i=0;i<8;i++){
      faderTargetData[(i*2)]=(faderTarget[i] >> 8) & 0xFF;                // split Target integers over two bytes in array
      faderTargetData[(i*2)+1]=faderTarget[i] & 0xFF;
      faderTargetData[16] |= faderMove[i] << i;                           // fill last byte in array with faderMove boolean array
      Serial.println(faderMove[i]);
    }
    Wire.beginTransmission(panelAddress);                                  // Transmit to Slave
    Wire.write(mode);                                                         // Send mode (TARGET or FOLLOW)
    for(idx = 0; idx < MESSAGE_LEN; idx++) Wire.write(faderTargetData[idx]);  // Write data to I2C Tx buffer
    Wire.endTransmission();                                                   // Transmit to Slave
}
void myControlChange(byte channel, byte control, byte value) {               // blink the LED a number of times

   
   value = map(value, 0, 127, 127, 0);
   
  //INT FADERS GAINS
  for (int i=0; i<8;i++){
  if (control == i+32) {
    hi[i] = value;
    faderMove[i] = 1;
  }
  if (control == i) {
    lo[i] = value;
    Gain[i] = (hi[i] >> 7) + lo[i]*8.055;
    faderMove[i] = 1;
  } 
  }
   for (int i=0;i<8;i++){
    faderTarget[i] = Gain[i];   
   // Serial.println(faderTarget[i]);
   }
}

something here is not right i get in the Gain (the values that comes from the midi faders) 0- 1204
i commper it to faderTarget and i turn faderMove of the fader to 1 then the fader should move.
in serial print faderTarget and faderMove values r ok but in serial print of faderTargetData its print values from 0-255( i print the only first 8 bytes of faderTargetData)
i test this code and its moves the faders but with not enaf power and after few sec of palying its stack's the fader to one end of the rail trying reach somewhere.
i allso shorten the code abit.
do u have any suggestion why this is happening?
 
Last edited:
ok thats is not simple for me but i hope i understand you i follow your instruction and here is the code now
its move the fader now a bit better its not tacks on one side of the rail but now its not gets to the and of the rail (i guess its about the pid configuration of power and distance but its moves fader 1 and 2
3 and 4 for some reason not moving i need to check up down pins in slave otherwise i can think about something else

Code:
#include <i2c_t3.h>
#include <MIDIUSB.h>

#define TARGET    0x10              // All faders to target value as fast as possible. PID state is cleared with each command.
#define FOLLOW    0x20
#define MESSAGE_LEN 17 

elapsedMicros sinceFaderRead;                         // timer for fader check
unsigned int  faderReadInterval = 1000;

elapsedMicros sinceFaderWrite;                         // timer for fader check
unsigned int   faderWriteInterval = 20000;
int faderTouchCount;

uint8_t panelAddress = 0x66;

byte faderValueData[MESSAGE_LEN];    // 16 bytes for values, 1 byte for touch
byte faderTargetData[MESSAGE_LEN];   // 16 for target values, 1 byte for indicating which faders should be moved.

boolean faderTouched[8];                              // boolean indicating if fader is touched
int     faderValue[8];                                // fader value ranging 0 to 1023
int     faderTarget[8];                               // fader target value ranging 0 to 1023
boolean faderMove[8];                               // fader target value ranging 0 to 1023
boolean updateFaders[8];
int hi[8];
int lo[8];
int Gain[8];


void setup() {
//**** I2C *****
  // Setup for Master mode, pins 18/19, external pullups, 1MHz, 200ms default timeout
  Wire.begin(I2C_MASTER, 0x00, I2C_PINS_18_19, I2C_PULLUP_EXT, 1000000);
  Wire.setDefaultTimeout(200000); // 200ms
  usbMIDI.setHandleControlChange(myControlChange);
  
}

void loop() {
  usbMIDI.read();
  //  substract interval to maintain schedule
  // faders
  if (sinceFaderRead >=faderReadInterval) {
    sinceFaderRead = sinceFaderRead - faderReadInterval;
    getFaderData();
  }
  if (sinceFaderWrite >=faderWriteInterval) {
    sinceFaderWrite = sinceFaderWrite - faderWriteInterval;
  for (int i=0;i<8;i++){
  if(updateFaders[i] == true ){
    sendFaderCommand(TARGET);
  }
  }
  }
}
// get fader values and touch status
void getFaderData() {
  size_t idx;
  int incomingFaderValue[16];    // 
  int touchCount=0;
  // Read from Slave
    Wire.requestFrom(panelAddress, (size_t)MESSAGE_LEN); // Read from Slave
    // Check if error occured
    if(Wire.getError())
        Serial.print("FAIL\n");
    else
    {
      // If no error then read Rx data into buffer
      idx = 0;
      while(Wire.available()) faderValueData[idx++] = Wire.readByte();
      // Process data in buffer
      for (int i=0;i<8;i++){
      updateFaders[i] = true;
    // combine bytes back to integers and put in temporary array
    incomingFaderValue[i]= faderValueData[(i*2)];
    incomingFaderValue[i]= incomingFaderValue[i] << 8 | faderValueData[(i*2)+1];
    // fill touch boolean array with last byte
    faderTouched[i] = 1 & faderValueData[16] >> i;
    if (faderTouched[i]) touchCount++;
       

      // check if faderValue needs to be updated
      if (incomingFaderValue[i]!=faderValue[i] and faderTouched[i]){
      faderValue[i]=incomingFaderValue[i];
      faderValue[i] = map(faderValue[i], 0, 1023, 127, 0);
      for (int j=0; j<8;j++){
        usbMIDI.sendControlChange(j+32, faderValue[j] , 1);
        usbMIDI.sendControlChange(j, faderValue[j] , 1);
      } 
      Serial.print("Fader ");
      Serial.print(i);
      Serial.print("\t");
      Serial.println(faderValue[i]);     
      Serial.print(faderTouched[i]);
    }
  }
  faderTouchCount=touchCount;
  }
}
// Send command with faderTarget array values to fader, TARGET or FOLLOW mode
void sendFaderCommand(byte mode){
  size_t idx;
  faderTargetData[16]=0;
    for (int i=0;i<8;i++){
      updateFaders[i] = false;
      faderTargetData[(i*2)]=(faderTarget[i] >> 8) & 0xFF;                // split Target integers over two bytes in array
      faderTargetData[(i*2)+1]=faderTarget[i] & 0xFF;
      faderTargetData[16] |= faderMove[i] << i;                           // fill last byte in array with faderMove boolean array
      Serial.println(faderTarget[i]);
    }
    Wire.beginTransmission(panelAddress);                                  // Transmit to Slave
    Wire.write(mode);                                                         // Send mode (TARGET or FOLLOW)
    for(idx = 0; idx < MESSAGE_LEN; idx++) Wire.write(faderTargetData[idx]);  // Write data to I2C Tx buffer
    Wire.endTransmission();                                                   // Transmit to Slave
    for (int j=0;j<8;j++){
    faderMove[j] = 0;
    }
}
void myControlChange(byte channel, byte control, byte value) {               // blink the LED a number of times

   
   value = map(value, 0, 127, 127, 0);
   
  //INT FADERS GAINS
  for (int i=0; i<8;i++){
  if (control == i+32) {
    hi[i] = value;
    faderMove[i] = 1;
  }
  if (control == i) {
    lo[i] = value;
    Gain[i] = (hi[i] >> 7) + lo[i]*8.055;
    faderMove[i] = 1;
  } 
  }
   for (int j=0;j<8;j++){
    faderTarget[j] = Gain[j];   
   // Serial.println(faderTarget[i]);
   }
}
 
update:
its turns out that i kill half of one hbrige with all my tests i guess i put in some case to much pressure (high resistance) on the chip with fader (motor) that stacks in the end of the rail trying reach point he cant.
but i play littel bit with the pid tuning to be a bit more agresive and i got now 3 fader working very well reaching to top and buttom and every point on the rail and not getting stacked .
i allso run sequence of automation from DAW to see the results and faders got very nice fast time response movment with no delay or any overflow symptoms :)
 
Yes, if the values are stable and the touch is OK it is time to test the motor control.

I would suggest to set the faderMove for a particular fader to 1 (true) if there's incoming data from the DAW for that fader. All faderMove values can be reset if the data had been sent to the panel. You should also add a boolean 'updateFaders' which is set to true if there is incoming data and set to false if the data has been sent. In the timer for the fader command add an if statement to only send the command if updateFaders is true, this prevents sending unnecessary commands.

hi gerrit
i have very strange problem all the test i make was on StudioOne DAW set to HUI Protocol and the faders in software moves good when i move the physical fader.
but i do test now on ProTools DAW allso with HUI protocol (my studio software) and the fader in protools not moves. but still the physical fader is moving when i send data from fader in protools.
i sniff midi from protools and studioOne and i find that is send the data different a bit so it might be the problem.

this is the test from studio one (software fader0 move)
Screen Shot 2019-03-06 at 1.09.57 AM.jpg
this is the test from protools (software fader0 move)
Screen Shot 2019-03-06 at 1.11.04 AM.jpg

thats how i send the data
Screen Shot 2019-03-06 at 1.17.54 AM.jpg

maybe you have any idea why its happening in protools?
 

Attachments

  • Screen Shot 2019-03-06 at 1.17.54 AM.jpg
    Screen Shot 2019-03-06 at 1.17.54 AM.jpg
    173.2 KB · Views: 50
Last edited:
Sorry, I don't know anything about Protools or Studio One as I use Logic and Reaper.

Why do you send the same data to two controllers? Isn't that supposed to a NRPN controller where the value is split over two MIDI bytes? But like I said, I don't know how Protools is controlled. Logic uses the Mackie MCU protocol and this uses pitch bend to send the fader values.
 
Sorry, I don't know anything about Protools or Studio One as I use Logic and Reaper.

Why do you send the same data to two controllers? Isn't that supposed to a NRPN controller where the value is split over two MIDI bytes? But like I said, I don't know how Protools is controlled. Logic uses the Mackie MCU protocol and this uses pitch bend to send the fader values.

Yes its NRPN but I don't know how to split the 2 bytes over the midi and send them. Maybe you can give example how to do it?
 
Yes its NRPN but I don't know how to split the 2 bytes over the midi and send them. Maybe you can give example how to do it?

It's actually in the Teensy usbMIDI library but it's not documented. I found a description is this thread.

I use something I copied of the net:
Code:
void sendNRPN(uint16_t parameter, uint16_t value,uint8_t channel) {
  usbMIDI.sendControlChange( 99, (parameter >> 7) & 0x7F, channel);
  usbMIDI.sendControlChange( 98, (parameter & 0x7F), channel);
  usbMIDI.sendControlChange( 6, (value >> 7) & 0x7F, channel);
  usbMIDI.sendControlChange( 38, (value & 0x7F), channel);
}

I don't know whether one is better than the other but this one works for me (for controlling synths).
 
It's actually in the Teensy usbMIDI library but it's not documented. I found a description is this thread.

I use something I copied of the net:
Code:
void sendNRPN(uint16_t parameter, uint16_t value,uint8_t channel) {
  usbMIDI.sendControlChange( 99, (parameter >> 7) & 0x7F, channel);
  usbMIDI.sendControlChange( 98, (parameter & 0x7F), channel);
  usbMIDI.sendControlChange( 6, (value >> 7) & 0x7F, channel);
  usbMIDI.sendControlChange( 38, (value & 0x7F), channel);
}

I don't know whether one is better than the other but this one works for me (for controlling synths).

ok i figure it out and i guess thats the answer to every one that wanna move ProTools Fader,
just like the HUI pdf documentation says, faders in HUI protocol act in this way: TOUCH >> MOVE>>RELEASE
so i just added the commend of touch fader with Sysex massage before the move commend and i added release Sysex commend after the move commend.
i used the same move commend like before.
now the fader in Protools moves all i need is to multiply it for all faders


here is the HUI pdf documentation:
https://github.com/willsmillie/HUI-...er/blob/master/HUI Protocol Documentation.pdf


i guess i reach the goal of this post and solved most of the faders issues.
the next stage is the buttons and screens action.
again many thanks gerrit for all the help and patient i learn a lot, hope this thread will help anyone :)
rota
 
Last edited:
Status
Not open for further replies.
Back
Top