Virtual and real sensors on 1CH-gateway (ESP)?

Just as an alternative: without any (fake) sensor you can define a payload function in a (dummy) application in TTN Console, and test that using the input field in TTN Console as well:

Click to see example decoder used above

This is discussed in Nemeus NIS-UL – Ultrasonic sensor Not the easiest example… See also Payload Formats [HowTo] - #3 by arjanvanb

function Decoder(bytes, port) {
  
  // See http://wiki.nemeus.fr/index.php?title=NIS-UL_UltraSonic_Sensor#Nemeus_uplink_protocol
  // Test with:
  //   8B 02 0097 0098 1f 19: 151, 152cm; 31, 25°C
  //   8B 02 0097 0098 fe 03: 151, 152cm; -2, 3°C
  //   AF 03 0097 0098 0090 0fb9 0e91 0fff fe 03 00 0a: 151, 152, 144cm; 4025, 3729, 4095mV, -2, 3, 0°C
  //   82 00a0: 160cm
  //   88 1e: 30°C
  var usonic_dist = [], voltage = [], internal_temp = [];
  var i = 0, m = 0;

  var mask = bytes[i++];

  // If bit 0 set: 1 byte indicating the number of measurements, else 1
  var nb_meas = mask & 1<<0 ? bytes[i++] : 1;

  // If bit 1 set: distances in centimeters, each 2 bytes unsigned MSB, 1 to 300cm
  if (mask & 1<<1) {
    for (m = 0; m < nb_meas; m++) {
      usonic_dist.push(bytes[i++]<<8 | bytes[i++]);
    }
  }

  // If bit 2 set: voltages in millivolts, each 2 bytes unsigned MSB
  if (mask & 1<<2) {
    for (m = 0; m < nb_meas; m++) {
      voltage.push((bytes[i++]<<8 | bytes[i++]) / 1000);
    }
  }

  // If bit 3 set: internal temperatures, each 1 byte signed integer [-128..+127]
  if (mask & 1<<3) {
    for (m = 0; m < nb_meas; m++) {
      // Sign-extend to 32 bits to support negative values, by shifting 24 bits
      // (too far) to the left, followed by a sign-propagating right shift:
      internal_temp.push(bytes[i++]<<24>>24);
    }
  }

  // Bit 4: reserved

  // If bit 5 set: cause for the measurement, else "periodic measurement"
  var cause = mask & 1<<5 ? bytes[i++] : 1;

  // We should have consumed all bytes, if not then just return the payload as hex
  if (i !== bytes.length) {
    return {
      'error': 'failed to parse payload',
      'payload': bytes.map(function(b) {
         return ('0' + b.toString(16).toUpperCase()).substr(-2);
       }).join(' ')
    }
  }
    // Format the results
  var result = {};
  // Show all 8 bits, ensuring leading zeroes
  result.mask = '0b' + ('00000000' + mask.toString(2)).substr(-8);

  result.numberOfMeasurements = nb_meas;
  result.distances = usonic_dist;
  result.voltages = voltage;
  result.temperatures = internal_temp;

  // Same data, grouped by measurement
  result.measurements = [];
  for (m = 0; m < nb_meas; m++) {
    result.measurements.push({
      distance: usonic_dist[m],
      voltage: voltage[m],
      temperature: internal_temp[m]
    });
  }

  result.cause = cause;
  result.causeMask = '0b' + ('00000000' + cause.toString(2)).substr(-8);
  result.causes = [];
  if (cause & 1<<0) {
    result.causes.push('periodic');
  }
  if (cause & 1<<1) {
    result.causes.push('usonic_dist > high threshold');
  }
  if (cause & 1<<2) {
    result.causes.push('usonic_dist < high threshold - high hysteresis');
  }
  if (cause & 1<<3) {
    result.causes.push('usonic_dist < low threshold');
  }
  if (cause & 1<<4) {
    result.causes.push('usonic_dist > low threshold + low hysteresis');
  }
  if (cause & 1<<5) {
    result.causes.push('forced manually');
  }

  return result;
}