rwaldron / johnny-five

JavaScript Robotics and IoT programming framework, developed at Bocoup.
http://johnny-five.io
Other
13.3k stars 1.77k forks source link

Support RC receivers #1071

Closed Resseguie closed 6 years ago

Resseguie commented 8 years ago

I'd like to add a class to support traditional hobby RC receivers to control nodebots. I happen to have a Spektrum radio and compatible receivers, but other popular Tx/Rx (transmitter/receiver - common abbreviations in RC world) combinations should be supported, too.

Details of my exact setup: DX6 Radio: http://amzn.to/1Se2hjd AR610 Rx: http://amzn.to/1UQfqVw (There are Spektrum compatible Tx an Rx made by Orange that are much cheaper.)

It should support individual channels (like the AR610), but also the PPM (Pulse Position Modulation) varieties.

There are a number of examples of using these with Arduino that could get us started. https://www.sparkfun.com/tutorials/348 http://rcarduino.blogspot.com/2012/01/how-to-read-rc-receiver-with.html

How generic should such a component be?

rwaldron commented 8 years ago

I've been casually thinking about this for about a year and the basic sketch I always return to is:

That's about as far as I usually get. It's critically important that these are designed with the controller pattern.

Resseguie commented 8 years ago

I'm not sure we need Transmitter? Seems like that's on "the other end" that we don't care about. At least I can't think of a use case. We just need to know the value the transmitter sends, via the receiver.

I'll need to think through the channels property, to make sure that makes sense for PPM as well. (I think so, but want to think it through.)

rwaldron commented 8 years ago

That may very well be the case, I just figured I'd put it there and we can let the junk shake out :)

rwaldron commented 8 years ago

(There are Spektrum compatible Tx an Rx made by Orange that are much cheaper.)

I just purchased these, should receive them tomorrow

rwaldron commented 8 years ago

The major issue we face is that pulseIn is process blocking. Some things to consider:

I like the idea of defining our own I2C backpacked peripheral, because we can control every aspect. I've come to realize that the design decisions made by many (not all) hardware creators is generally not very compatible with our async goals.

henricavalcante commented 8 years ago

Is there a way to handle PPM signal over firmata?

rwaldron commented 8 years ago

@henricavalcante no, but it can be made available via I2C

rwaldron commented 8 years ago

@Resseguie

I assembled this gizmo as a basic "backpack" starting point. In the photos below, you'll notice that the intention is to plug this thing directly into the receiver unit's channel pins. The fritzing shows everything facing forward, but that's just only for illustrative purposes (and a limitation of fritzing)

G: Ground V: VCC C: Channel

I haven't added a label for channels, because my actual receiver won't arrive until later today, so I'm not sure which direction the channels are in.

Resseguie commented 8 years ago

Nice. I'll try to build something similar.

rwaldron commented 8 years ago

Welllllllll my receiver just arrived and unfortunately the one I ordered has notches

rwaldron commented 8 years ago

I cut them out and made it fit. Then I made a newer, less messy backpack. Pictures later

Resseguie commented 8 years ago

That's the same Rx I have. FYI, the typical method for connecting such a receiver to your RC project (drone, truck, etc) is with a breakout cable, such as this Naze32 one: http://www.readymaderc.com/store/index.php?main_page=product_info&products_id=3062

That also gives you more flexibility in positioning components on your build. I'll probably make my backpack to accept something similar to that.

Note that weight is also a factor for many uses of this, so I'd like to investigate the minimum backpack necessary once we get the design working.

rwaldron commented 8 years ago

The second one I built significantly reduced materials:

FYI, the typical method for connecting such a receiver to your RC project (drone, truck, etc) is with a breakout cable, such as this Naze32 one: http://www.readymaderc.com/store/index.php?main_page=product_info&products_id=3062 That also gives you more flexibility in positioning components on your build. I'll probably make my backpack to accept something similar to that.

But you won't be able to use such a thing if you're trying to read the pulse values from the receiver—right? You're effectively replacing this thing and adding a "middle man" to intercept the pulses to do something else with them.

henricavalcante commented 8 years ago

I'm testing the Pin Change Interrupts to decode pwm signal and it works, but I'm trying to figure out a way to send the information to johnny-five like firmata 'cause I'm using a crius board which uses atmega 2560 and has more sensors and I need them, when I upload firmata to board I cant read pwm signals, when I upload my code with Pin Change Interrupt I only access pwm signals from receiver.

This is the board: crius-layout

This is the receiver: turnigy-9x-reciever-9x8cv2

I have a quadcopter working well with this firmware and I'm trying to make a car controlled by johnny-five.

There is a way to rewrite firmata with Pin Change Interrupts in some pins? I want still firmata and read pwm signals in same board.

something like:

I'm crazy or there is a way to make it possible?

rwaldron commented 8 years ago

The reader code will go on a different microprocessor. I have some super basic firmware written that gets us 6 channels of PPM values. How are you using the pin change interrupts? I couldn't get them to do what I wanted. Can you share your work so far? Tomorrow I will push out what I have so far

henricavalcante commented 8 years ago

It's my setup: img_20160405_245759492 img_20160405_245810435 img_20160405_245818821

It's my code:

#include <PinChangeInt.h> //https://github.com/GreyGnome/PinChangeInt

#define FIRST_PIN 62

volatile int pwm[] = {0,0,0,0,0,0,0,0,0,0};
volatile int last_up[] = {0,0,0,0,0,0,0,0,0,0};

void up()
{
  uint8_t pin=PCintPort::arduinoPin;
  PCintPort::attachInterrupt(pin, &down, FALLING);
  last_up[pin - FIRST_PIN] = micros();
}

void down() {
  uint8_t pin=PCintPort::arduinoPin;
  PCintPort::attachInterrupt(pin, &up, RISING);
  pwm[pin - FIRST_PIN] = micros()-last_up[pin - FIRST_PIN];
  Serial.print(pin);
  Serial.print(" - ");
  Serial.println(pwm[pin - FIRST_PIN]);
}

void setup() {
  pinMode(A8, INPUT); digitalWrite(A8, HIGH);
  pinMode(A9, INPUT); digitalWrite(A9, HIGH);
  pinMode(A10, INPUT); digitalWrite(A10, HIGH);
  pinMode(A11, INPUT); digitalWrite(A11, HIGH);
  pinMode(A12, INPUT); digitalWrite(A12, HIGH);
  pinMode(A13, INPUT); digitalWrite(A13, HIGH);
  pinMode(A14, INPUT); digitalWrite(A14, HIGH);
  pinMode(A15, INPUT); digitalWrite(A15, HIGH);
  Serial.begin(115200);

  //start interrupts
  PCintPort::attachInterrupt(A8, &up, RISING);
  PCintPort::attachInterrupt(A9, &up, RISING);
  PCintPort::attachInterrupt(A10, &up, RISING);
  PCintPort::attachInterrupt(A11, &up, RISING);
  PCintPort::attachInterrupt(A12, &up, RISING);
  PCintPort::attachInterrupt(A13, &up, RISING);
  PCintPort::attachInterrupt(A14, &up, RISING);
  PCintPort::attachInterrupt(A15, &up, RISING);
}

void loop() { }

And this is some serial output:

66 - 1456
62 - 1472
64 - 680
65 - 1156
63 - 1476
68 - 1464
67 - 1084
66 - 1448
62 - 1476
64 - 684
65 - 1156
63 - 1480
68 - 1468
67 - 1080
66 - 1452
62 - 1476
64 - 680
65 - 1156
63 - 1476
68 - 1464
67 - 1080
66 - 1452
62 - 1472
64 - 680
65 - 1156
63 - 1476
68 - 1464

When I move the sticks on controller, values on serial output change between ~600 and ~1500

henricavalcante commented 8 years ago

Are you getting receiver PPM signal by channels output? how? If you get signal from each channel should be a PWM signal. Have you checked it on oscilloscope?

rwaldron commented 8 years ago

If you get signal from each channel should be a PWM signal

Yes, I was mistaken.

Here is the crude firmware I wrote:

#include <Wire.h>

#define DEBUG_MODE 1

// Address Pins
#define AD0 8
#define AD1 9

// I2C Defaults
#define I2C_DEFAULT_ADDRESS 0x0A
#define I2C_BUFFER_SIZE 12

byte buffer[I2C_BUFFER_SIZE];

int addressPins[] = { AD0, AD1 };
int address = I2C_DEFAULT_ADDRESS;
int first = 2;
int channels = 6;

void resetState() {
  for (int i = first; i < first + channels; i++) {
    pinMode(i, INPUT);
  }
}

void setup() {

  int offset = 0;

  for (int i = 0; i < 2; i++) {
    pinMode(addressPins[i], INPUT);
    if (digitalRead(addressPins[i])) {
      offset |= 1 << i;
    }
  }

  address += offset;

  #if DEBUG_MODE
    Serial.begin(9600);
  #endif

  resetState();

  Wire.begin(address);
  Wire.onRequest(onRequest);
}

void loop() {

  uint16_t pulses[channels];

  for (int i = first; i < first + channels; i++) {
    pulses[i - first] = pulseIn(i, HIGH, 35000);
  }

  #if DEBUG_MODE
    for (int i = 0; i < channels; i++) {
      Serial.print(i);
      Serial.print(": ");
      Serial.println(pulses[i]);
    }
  #endif

  buffer[0] = pulses[0] >> 8;
  buffer[1] = pulses[0] & 0xFF;
  buffer[2] = pulses[1] >> 8;
  buffer[3] = pulses[1] & 0xFF;
  buffer[4] = pulses[2] >> 8;
  buffer[5] = pulses[2] & 0xFF;
  buffer[6] = pulses[3] >> 8;
  buffer[7] = pulses[3] & 0xFF;
  buffer[8] = pulses[4] >> 8;
  buffer[9] = pulses[4] & 0xFF;
  buffer[10] = pulses[5] >> 8;
  buffer[11] = pulses[5] & 0xFF;
}

void onRequest() {
  Wire.write(buffer, I2C_BUFFER_SIZE);
}

I don't want to put this inside any version of Firmata, because that means it's not available on any other platform that Johnny-Five supports. The benefit of using the I2C slave backpack approach is that all we need is I2C support on other platforms and we're good to go. I'm going to try to combine your interrupt approach with my slave firmware (I will obviously cite you as co-author)

rwaldron commented 8 years ago

I'm not sure we should use this: https://github.com/GreyGnome/PinChangeInt it's not available via the Arduino IDE "Manage Libraries" interface, which means people have to go through the hassle of adding it via the "Add .ZIP Library". Both are terrible.

Also, there is a notice on the repo:

NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE This library is deprecated as of April 3, 2015. This means that I will be providing bug fixes for some time, but users are encouraged to migrate to the EnableInterrupt library, at https://github.com/GreyGnome/EnableInterrupt . It is faster and easier to use. Thank you.

The version referenced does exist in the Arduino IDE's Library Manager.

rwaldron commented 8 years ago

The interrupt version is giving me wildly inaccurate results and appears to be slower than the version with pulseIn... which is strange.

henricavalcante commented 8 years ago

What if we use EnableInterrupt instead?

rwaldron commented 8 years ago

We'll have to either way.

rwaldron commented 8 years ago

So... this works awesome when channels is set to 1. Once it I set back to 6 or 8, the values are garbage.

#define EI_ARDUINO_INTERRUPTED_PIN
#include <EnableInterrupt.h>
#include <Wire.h>

#define DEBUG_MODE 1

// Address Pins
#define AD0 11
#define AD1 12

// I2C Defaults
#define I2C_DEFAULT_ADDRESS 0x0A
#define I2C_BUFFER_SIZE 16

byte buffer[I2C_BUFFER_SIZE];

int addressPins[] = { AD0, AD1 };
int address = I2C_DEFAULT_ADDRESS;
int first = 2;
int channels = 1;

volatile int pulses[8];
volatile int last_up[8];

void up() {
  uint8_t pin = arduinoInterruptedPin;
  enableInterrupt(pin, &down, FALLING);

  #if DEBUG_MODE
    Serial.print("pin up: ");
    Serial.println(pin);
  #endif

  last_up[pin - first] = micros();
}

void down() {
  uint8_t pin = arduinoInterruptedPin;
  enableInterrupt(pin, &up, RISING);

//  #if DEBUG_MODE
//    Serial.print("pin down: ");
//    Serial.println(pin);
//  #endif

  pulses[pin - first] = micros() - last_up[pin - first];

  #if DEBUG_MODE
    Serial.print(pin);
    Serial.print(": ");
    Serial.println(pulses[pin - first]);
  #endif
}

void resetState() {
  for (int i = first; i < first + channels; i++) {
    pulses[i - first] = 0;
    last_up[i - first] = 0;

    pinMode(i, INPUT_PULLUP);
    enableInterrupt(i, &up, RISING);
  }
}

void setup() {

  // First check if the address was set via pins 11 or 12
  int offset = 0;

  for (int i = 0; i < 2; i++) {
    pinMode(addressPins[i], INPUT);
    if (digitalRead(addressPins[i])) {
      offset |= 1 << i;
    }
  }

  address += offset;

  resetState();

  #if DEBUG_MODE
    Serial.begin(9600);
  #endif

  // Initialize Slave
  Wire.begin(address);
  Wire.onRequest(onRequest);
  Wire.onReceive(onReceive);
}

void loop() {
  for (int i = 0; i < channels; i++) {
    buffer[i * 2] = pulses[i] >> 8;
    buffer[i * 2 + 1] = pulses[i] & 0xFF;
  }  
}

void onRequest() {
  Wire.write(buffer, I2C_BUFFER_SIZE);
}

void onReceive(int count) {
  while (Wire.available()) {
    // Command 0x01 => Reset. 
    if (Wire.read() == 0x01) {
      Serial.println("RESET");
      resetState();  
    }
  }
}

............

Can we discuss the pros and cons of pulseIn vs interrupts, given the context (which is: 100% dedicated microprocessor that must only write to I2C bus)

henricavalcante commented 8 years ago

Im going home now and I will make some tests about differences, interrupts should be better because is non blocking, but I'm thinking, if you want to make a hardware exclusive to convert signals why not convert pwm to analog signal with capacitors and use arduino ADC ports?

henricavalcante commented 8 years ago

Are you building a tessel 2 module?

henricavalcante commented 8 years ago

I'm getting better results using atmega 2560 microprocessor than using atmega 328p, trying to figure out the problem. img_20160405_201640392

henricavalcante commented 8 years ago

Check using without Serial output inside interrupt delegates like this:

#define EI_ARDUINO_INTERRUPTED_PIN
#include <EnableInterrupt.h>
#include <Wire.h>

#define DEBUG_MODE 1

// Address Pins
#define AD0 11
#define AD1 12

// I2C Defaults
#define I2C_DEFAULT_ADDRESS 0x0A
#define I2C_BUFFER_SIZE 16

byte buffer[I2C_BUFFER_SIZE];

int addressPins[] = { AD0, AD1 };
int address = I2C_DEFAULT_ADDRESS;
int first = 2;
int channels = 8;

volatile int pulses[8];
volatile int last_up[8];

void up() {
  volatile uint8_t pin = arduinoInterruptedPin;
  enableInterrupt(pin, &down, FALLING);
  last_up[pin - first] = micros(); 
}

void down() {
  volatile uint8_t pin = arduinoInterruptedPin;
  enableInterrupt(pin, &up, RISING);
  pulses[pin - first] = micros() - last_up[pin - first];
}

void resetState() {
  for (int i = first; i < first + channels; i++) {
    pulses[i - first] = 0;
    last_up[i - first] = 0;

    pinMode(i, INPUT_PULLUP);
    enableInterrupt(i, &up, RISING);
  }
}

void setup() {

  // First check if the address was set via pins 11 or 12
  int offset = 0;

  for (int i = 0; i < 2; i++) {
    pinMode(addressPins[i], INPUT);
    if (digitalRead(addressPins[i])) {
      offset |= 1 << i;
    }
  }

  address += offset;

  resetState();

  #if DEBUG_MODE
    Serial.begin(9600);
  #endif

  // Initialize Slave
  Wire.begin(address);
  Wire.onRequest(onRequest);
  Wire.onReceive(onReceive);
}

void loop() {
  #if DEBUG_MODE
    for (int i = first; i < first + channels - 1; i++) {
      Serial.print(pulses[i - first]);
      Serial.print("  - ");
    }
    Serial.println(pulses[channels - 1]);
  #endif
  for (int i = 0; i < channels; i++) {
    buffer[i * 2] = pulses[i] >> 8;
    buffer[i * 2 + 1] = pulses[i] & 0xFF;
  } 
}

void onRequest() {
  Wire.write(buffer, I2C_BUFFER_SIZE);
}

void onReceive(int count) {
  while (Wire.available()) {
    // Command 0x01 => Reset. 
    if (Wire.read() == 0x01) {
      Serial.println("RESET");
      resetState();  
    }
  }
}

works like a charm here with arduino pro mini (atmega 328p 16MHZ) ;)

rwaldron commented 8 years ago

Are you building a tessel 2 module?

Not specifically, but I plan to use this with Tessel 2 :D

rwaldron commented 8 years ago

Check using without Serial output inside interrupt delegates like this

Brilliant! I guess I didn't realize that Serial output inside the interrupt would cause such issues—great work :D

works like a charm here with arduino pro mini (atmega 328p 16MHZ) ;)

Can't wait to try this!

rwaldron commented 8 years ago

@henricavalcante :clap: this works perfectly

soundanalogous commented 8 years ago

I guess I didn't realize that Serial output inside the interrupt would cause such issues

Yeah typically you want to do as little as possible inside the interrupt service handler (ISR) - usually just toggle a variable (marked as "volatile") and then based on the state of that variable, do any actual reporting or heavy calculations in the main loop. Some architectures limit the size of the stack for the ISR to just a few bytes so you can't do much and really want to return as quickly as possible.

rwaldron commented 8 years ago

Some architectures limit the size of the stack for the ISR to just a few bytes so you can't do much and really want to return as quickly as possible.

I feel like I learned a lot today—thanks for providing that additional insight :)

soundanalogous commented 8 years ago

There are also scenarios where you want to disable the interrupts globally while executing a piece of code. You have to approach from the perspective of what happens if an interrupt is triggered while some block of code is executing and if so disable interrupts globally before that block and reenable after that block. You will see a lot of this if you dig into the code in Adafruit libraries and other well-written Arduino libraries that use interrupts. You'll see this in PJRC libs as well.

soundanalogous commented 8 years ago

here's an example where disabling and reenabling global interrupts is useful: https://github.com/PaulStoffregen/CapacitiveSensor/blob/master/CapacitiveSensor.cpp#L154-L183

fivdi commented 8 years ago

One scenario where it's necessary to disable interrupts in the above code is when the pulses array is accessed in the loop function.

void loop() {
  for (int i = 0; i < channels; i++) {
    buffer[i * 2] = pulses[i] >> 8;
    buffer[i * 2 + 1] = pulses[i] & 0xFF;
  }  
}

When the following code is executed

    buffer[i * 2] = pulses[i] >> 8;

there is no guarantee that loading the values of the two bytes of the 16 bit integer pulses[i] into registers is an atomic operation that can't be interrupted. There may be an interrupt after the first byte is loaded and before the second byte is loaded. This can potentially mess things things up.

The same applies to the following code

    buffer[i * 2 + 1] = pulses[i] & 0xFF;

There is a similar issue with the following code

    buffer[i * 2] = pulses[i] >> 8;
    buffer[i * 2 + 1] = pulses[i] & 0xFF;

There may be an interrupt between the first and second line of code that changes the value of pulses[i].

rwaldron commented 8 years ago

@fivdi

This should address that?

int pulse = pulses[i];

buffer[i * 2] = pulse >> 8;
buffer[i * 2 + 1] = pulse & 0xFF;
rwaldron commented 8 years ago

@soundanalogous

Per your suggestion, I tried using the noInterrupt()/interrupt() calls (in a few different places), but everywhere I tried just crashed the board :\

fivdi commented 8 years ago

This should address that?

int pulse = pulses[i];

buffer[i * 2] = pulse >> 8; buffer[i * 2 + 1] = pulse & 0xFF;

@rwaldron

I'm afraid not. The AVR 8-bit instructions set doesn't have an instruction for loading 16 bit values. This forces to the compiler to generate two instructions each of which loads a single byte. Because there are two instructions there can be an interrupt between the execution of the first and second.

rwaldron commented 8 years ago

Well, then I guess it's a deal breaker.

Can someone tell me why we need to use interrupts here?

henricavalcante commented 8 years ago

I think because reading pwm values with pulseIn will block everything, with interrupts the microprocessor will be able to work between pwm edges. I will try to figure out a way to make data atomic while i2c buffering.

rwaldron commented 8 years ago

I think because reading pwm values with pulseIn will block everything, with interrupts the microprocessor will be able to work between pwm edges.

Thanks, I know why we might want to use interrupts in general, but that strategy is generally used in scenarios where the entire program, including all user application code is running on the same processor. We're only using this to read a maximum of 8 channels (maybe someone will want to use a 10 or 12 channel unit, but I'll worry about that when the day actually comes), and write the values to the I2C bus...

rwaldron commented 8 years ago

Whoops!

henricavalcante commented 8 years ago

@rwaldron have you tried like this:

for (int i = 0; i < channels; i++) {
    noInterrupts();
    buffer[i * 2] = pulses[i] >> 8;
    buffer[i * 2 + 1] = pulses[i] & 0xFF;
    interrupts();
  } 
rwaldron commented 8 years ago

I thought I did and I thought I saw it crashing. It's not crashing now.

rwaldron commented 8 years ago

I'm signing off for a bit.

rwaldron commented 8 years ago

Presently, this is not crashing:

#define EI_ARDUINO_INTERRUPTED_PIN
#include <EnableInterrupt.h>
#include <Wire.h>

#define DEBUG_MODE 1

// Address Pins
#define AD0 11
#define AD1 12

// I2C Defaults
#define I2C_DEFAULT_ADDRESS 0x0A
#define I2C_BUFFER_SIZE 16

byte buffer[I2C_BUFFER_SIZE];

int addressPins[] = { AD0, AD1 };
int address = I2C_DEFAULT_ADDRESS;
int first = 2;
int channels = 8;

volatile int pulses[8];
volatile int last_up[8];

void up() {
  volatile uint8_t pin = arduinoInterruptedPin;
  enableInterrupt(pin, &down, FALLING);
  last_up[pin - first] = micros();
}

void down() {
  volatile uint8_t pin = arduinoInterruptedPin;
  enableInterrupt(pin, &up, RISING);
  pulses[pin - first] = micros() - last_up[pin - first];
}

void resetState() {
  for (int i = first; i < first + channels; i++) {
    pulses[i - first] = 0;
    last_up[i - first] = 0;

    pinMode(i, INPUT_PULLUP);
    enableInterrupt(i, &up, RISING);
  }
}

void setup() {

  // First check if the address was set via pins 11 or 12
  int offset = 0;

  for (int i = 0; i < 2; i++) {
    pinMode(addressPins[i], INPUT);
    if (digitalRead(addressPins[i])) {
      offset |= 1 << i;
    }
  }

  address += offset;

  resetState();

  #if DEBUG_MODE
    Serial.begin(9600);
  #endif

  // Initialize Slave
  Wire.begin(address);
  Wire.onRequest(onRequest);
  Wire.onReceive(onReceive);
}

void loop() {
  #if DEBUG_MODE
    for (int i = first; i < first + channels - 1; i++) {
      Serial.print(pulses[i - first]);
      Serial.print("  - ");
    }
    Serial.println(pulses[channels - 1]);
  #endif
  for (int i = 0; i < channels; i++) {
    noInterrupts();
    buffer[i * 2] = pulses[i] >> 8;
    buffer[i * 2 + 1] = pulses[i] & 0xFF;
    interrupts();
  }
}

void onRequest() {
  Wire.write(buffer, I2C_BUFFER_SIZE);
}

void onReceive(int count) {
  while (Wire.available()) {
    // Command 0x01 => Reset.
    if (Wire.read() == 0x01) {
      Serial.println("RESET");
      resetState();
    }
  }
}
henricavalcante commented 8 years ago

It's not crashing here too, which node code are you using in tessel 2 to get values from I2C?

rwaldron commented 8 years ago

I've been working on a plugin component Receiver class all day, I will push it out tomorrow. I have to leave my computer for the rest of the day.

henricavalcante commented 8 years ago

ok I'll wait until tomorrow ;) I was trying something like this without success:

const five = require('johnny-five');
const board = new five.Board();

board.on('ready', function(){
  const opts = {
    address: 0x0A
  };

  this.io.i2cConfig(opts);

  this.io.i2cRead(opts.address, 16, function(data) {
    console.log(data);
  })
});
Resseguie commented 8 years ago

Looks like I was away for a few days and missed some fun discussion.

Regarding that Naze cable:

But you won't be able to use such a thing if you're trying to read the pulse values from the receiver—right? You're effectively replacing this thing and adding a "middle man" to intercept the pulses to do something else with them.

I really only meant that I'll probably have male pins on my backpack so I could use such a cable since you can get something similar for most receivers. Then you don't have to worry about having female headers shaped a specific way to fit a particular receiver onto a backpack.