This blog is now retired. Please visit our new blog here.

Every so often, I try to do some short projects that don't take too long, but let me learn something new. When the new Nerf Laser Ops range came out in September 2018, I had a pair of blasters on the first day — because they seemed to be using targeted infra-red signals and that looked very hackable.

The first thing to when you get a new device is take it apart!

I had an Adafruit Circuit Playground Express from some teaching classes that we'd been doing recently, and the onboard infra-red (IR) receiver and sender, plus the onboard RGB LEDs, looked like a nice way of duplicating the foam dart sensors but this time, for the new IR blasters.

Adafruit Circuit Playground Express in Makecode

While I'd been meaning to write this up for a while, Les Pounder's article in issue 14 of Hackspace Magazine (page 120) reminded me that I'd never actually published it! [Thanks Les!]

TL;DR

Without having to know the exact infra-red sending pattern that Nerf use for the Laser Ops series, the following hash values are enough to use IRLib2 on Arduino with the Hash Decoder functions to identify which colour team last "hit" the IR receiver with an Alphapoint blaster. It's not known if this works for the Deltaburst, as I don't have one to try at the time of writing.

/* IR hashes for Nerf LaserOps standard game */
static const long PURPLE = 0x67228B44;
static const long RED = 0x78653B0E;
static const long BLUE = 0x2FFEA610;

Receiving IR

While it would be entirely possible that Nerf/Hasbro would use a completely custom IR sending arrangement, most IR signals are sent by modulating the signal using a common frequency to prevent interference from other devices (eg. incandescent bulbs). The receiver then minuses the known frequency from the received signal, and the result comes out as a set of pulses of specific length.

A quick check by firing the blaster at a TV IR receiver I have that always lights up as long as it's receiving any signal, confirmed to me that the blaster uses the same IR hardware as every other common remote or IR device.

Then it was on to finding out the exact signal that the blasters use.

IRLib2

A quick google found Chris Young's IR library for Arduino, and it is especially suitable because it has example Arduino sketches to show the raw timing of incoming signals — useful in this case because (spoiler alert) the Nerf Laser Ops signal doesn't use TV manufacturer codes for the blasters.

The first order of business then is to see what the incoming signals actually look like.

By using a raw receiver example from the library that's lightly modified to match the pin assignments for the Circuit Playground Express (rawRecv) and starting the Alphapoint blaster as the purple team and firing at the IR sensor; the results look something like this:

#define RAW_DATA_LEN 36
uint16_t rawData[RAW_DATA_LEN]={
	2895, 5992, 2897, 2053, 841, 2059, 846, 2054,
	851, 2061, 824, 2076, 1817, 2108, 787, 2112,
	844, 2056, 838, 2062, 1863, 2062, 842, 2057,
	848, 2052, 843, 2057, 848, 2051, 843, 2070,
	825, 2074, 790, 1000};

In the raw results from the library, each number in the array indicates either the number of microseconds of a mark (signal high) or a space (signal low).

The stream starts with the length of the first mark (2895µs), followed by the length of the next space (5992µs). The sequence continues with alternating marks and spaces until the final recorded mark (790µs) and the sketch adds a trailing space of a set length (1000µs) to allow it to be used in another program without needing to worry about running into the start of the next sent signal.

Now, these results would be fine to just copy and paste into a sketch if we just wanted to resend these to another blaster, but with a little guess work, I figured it might be reasonable to make an educated guess to the structure of the message.

Decoding

The numbers from rawRecv are alternating mark & space times, in microseconds, so assuming some variation on the exact timings of the received signals, they are likely to actually be sent as a round number of microseconds. Eg.

uint16_t rawData[RAW_DATA_LEN]={
	3000, 6000, 3000, 2000, 800, 2000, 800, 2000,
	800, 2000, 800, 2000, 1800, 2000, 800, 2000,
	800, 2000, 800, 2000, 1800, 2000, 800, 2000,
	800, 2000, 800, 2000, 800, 2000, 800, 2000,
	800, 2000, 800, 1000};

And knowing that lots of signals start with a preamble to allow the receiver to identify the start of the signal, the rest starts to look a lot like two possible length marks that each follow a fixed length space.

If the preamble is:

  • (mark)3000, (space)6000, (mark)3000

The rest of the message looks a lot more regular. And I'm guessing that the rest of the mark lengths will indicate either a binary 0 or 1. Assuming that zeros make up more of the message, that would suggest that after the preamble:

  • (space)2000, (mark)800 is a 0, and
  • (space)2000, (mark)1800 is a 1.

So this could be translated as:

uint16_t rawData[RAW_DATA_LEN]={
  3000, 6000, 3000, // preamble (3ms mark, 6ms space, 3ms mark)
  2000, 800, 2000, 800, 2000, 800, 2000, 800,  // 0, 0, 0, 0
  2000, 1800, 2000, 800, 2000, 800, 2000, 800, // 1, 0, 0, 0
  2000, 1800, 2000, 800, 2000, 800, 2000, 800, // 1, 0, 0, 0
  2000, 800, 2000, 800, 2000, 800, 2000, 800,  // 0, 0, 0, 0
  1000 // added by the sketch
};

I've broken these up in to 4 bits lines (a nybble), on a hunch. The position of those 1 bits suggests that there might be 4 fields of 4 bytes each, encoded with the least significant bit to the earliest bit (least significant bit to the left).

Running each of the colour in the same mode gives us some more information:

// PURPLE
#define RAW_DATA_LEN 36
uint16_t rawData[RAW_DATA_LEN]={
  2888, 5987, 2893,
  2058, 848, 2052, 843, 2069, 795, 2104, 791,  // 0, 0, 0, 0
  2109, 1816, 2109, 837, 2063, 842, 2057, 848, // 1, 0, 0, 0
  2052, 1873, 2055, 840, 2059, 846, 2054, 841, // 1, 0, 0, 0
  2071, 794, 2106, 789, 2111, 795, 2105, 841,  // 0, 0, 0, 0
  1000
};
// RED
#define RAW_DATA_LEN 36
uint16_t rawData[RAW_DATA_LEN]={
  2899, 5977, 2897,
  2065, 842, 2058, 849, 2051, 845, 2055, 842,  // 0, 0, 0, 0
  2058, 1869, 2056, 851, 2061, 826, 2074, 792, // 1, 0, 0, 0
  2108, 789, 2111, 836, 2064, 843, 2057, 840,  // 0, 0, 0, 0
  2060, 846, 2054, 843, 2057, 850, 2050, 847,  // 0, 0, 0, 0
  1000
};
// BLUE
#define RAW_DATA_LEN 36
uint16_t rawData[RAW_DATA_LEN]={
  2896, 5979, 2902,
  2060, 836, 2064, 843, 2057, 848, 2052, 844,  // 0, 0, 0, 0
  2055, 1870, 2055, 851, 2052, 844, 2065, 820, // 1, 0, 0, 0
  2080, 795, 2105, 1821, 2104, 792, 2111, 836, // 0, 1, 0, 0
  2063, 843, 2057, 838, 2062, 845, 2055, 840,  // 0, 0, 0, 0
  1000
};

While this doesn't give all the answers, it strongly suggests that the number of the ID for the team that sent the pulse is encoded in the third data line.

If I assume that it is nybbles with the least significant bit sent first:

Nybble Identifier Values
0 Unknown 0
1 Unknown
  • Game type?
  • Number of hits?
  • Blaster type?
1
2 Team
  • Red = 0
  • Purple = 1
  • Blue = 2
3 Unknown 0

Hash

While having this information is good, and sending identical codes back out of the Circuit Playground Express IR sender will "hit" a blaster of the appropriate team, there's not enough information yet to fully decode the signal, and because it's not a known encoding in the library, it would need specific decoder writing for the library to use.

But because, the library can only identify a limited set of "standard" IR patterns, it also includes a way of identifying any signal, even an unknown one by generating a 32 bit "hash" of the sequence. I've not looked to closely at the hash algorithm used, but it's designed to allow for the intrinsic timing jitter in the received IR signals and it's expected to be robust enough that repeated sends of the same message will always give the same hash.

Using the hashDecode sketch, the hash code will be printed on the serial monitor.

The values for each team I got are:

/* IR hashes for Nerf LaserOps standard game */
static const long PURPLE = 0x67228B44;
static const long RED    = 0x78653B0E;
static const long BLUE   = 0x2FFEA610;

And as we know the hashes, checking for these values specifically will allow identification of the team who's signal has been received:

if (myDecoder.value == PURPLE) {
  // indicate Purple on the LEDs...
}

I've already created an example sketch that uses these hashes, and lights up the LEDs on the Circuit Playground, depending on the incoming signal. As with the other sketches, I've included a UF2 file, that can just be downloaded, dragged and dropped onto the CPLAYBOOT drive to flash onto the Circuit Playground Express:

Team hit colour on the Circuit Playground Express

TODO

There's enough here to be able to identify incoming signals, and send replica signals out, so it's possible to create alternate game components that interact with the blasters.

Still outstanding questions include:

  • What do the other fields in the IR signal relate to?
  • Does the message change for other game types? Especially, does an ID for each blaster get included in games using the app for scoring?
  • Does the Deltaburst signal change to indicate its "double shot" mode?
  • What's the best approach to write an IRLib2 library file for decoding these signals?