Skip to content

Commit

Permalink
Alpha support for Milestag2 (#1380)
Browse files Browse the repository at this point in the history
I've implemented MilesTag2 Lasertag protocol described here:
http://hosting.cmalton.me.uk/chrism/lasertag/MT2Proto.pdf

decoding  actually meaning of bit's in protocol is on users side, they could just macro it  for example:
```cpp
#define MT_SHOT(player, team, damage) (unsigned long)(((player & 127) << 6) | ((team   & 3  ) << 4) | (damage & 15 ))
#define MT_PLAYER_VALUE(shot_data) (unsigned int)((shot_data >> 6) & 127)
#define MT_TEAM_VALUE(shot_data)   (unsigned short)((shot_data >> 4) & 3)
#define MT_DAMAGE_VALUE(shot_data) (unsigned short)(shot_data & 15)
```

Tested it on ESP8266 and ESP32 and it works fine for me. But I don't have actual commercial Lasertag taggers and receivers with MilesTag2, to test against real world conditions. But I think this should be a good start.

* Extend `matchData()`, `matchBytes()`, & `matchGeneric()` to handle no trailing space.
* Add basic unit tests
* Unit tests
  - Housekeeping
  - Sending Shot & Msg sizes.
  - Self-decoding
  - Failure when packets are not formatted correctly.

Note: No real-world test data as yet.

Co-authored-by: crankyoldgit <[email protected]>


X-Ref #1360
  • Loading branch information
vitos1k authored Jan 16, 2021
1 parent 2b508b3 commit 667ed87
Show file tree
Hide file tree
Showing 10 changed files with 401 additions and 23 deletions.
72 changes: 52 additions & 20 deletions src/IRrecv.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,14 @@ bool IRrecv::decode(decode_results *results, irparams_t *save,
DPRINTLN("Attempting NEC decode");
if (decodeNEC(results, offset)) return true;
#endif
#if DECODE_MILESTAG2
DPRINTLN("Attempting MilesTag2 decode");
// Try decodeMilestag2() before decodeSony() because the protocols are
// similar in timings & structure, but the Miles one differs in nbits
// so this one should be tried first to try to reduce false detection
if (decodeMilestag2(results, offset, kMilesTag2MsgBits) ||
decodeMilestag2(results, offset, kMilesTag2ShotBits)) return true;
#endif
#if DECODE_SONY
DPRINTLN("Attempting Sony decode");
if (decodeSony(results, offset)) return true;
Expand Down Expand Up @@ -1202,30 +1210,49 @@ bool IRrecv::decodeHash(decode_results *results) {
/// @param[in] excess Nr. of uSeconds. (Def: kMarkExcess)
/// @param[in] MSBfirst Bit order to save the data in. (Def: true)
/// true is Most Significant Bit First Order, false is Least Significant First
/// @param[in] expectlastspace Do we expect a space at the end of the message?
/// @return A match_result_t structure containing the success (or not), the
/// data value, and how many buffer entries were used.
match_result_t IRrecv::matchData(
volatile uint16_t *data_ptr, const uint16_t nbits, const uint16_t onemark,
const uint32_t onespace, const uint16_t zeromark, const uint32_t zerospace,
const uint8_t tolerance, const int16_t excess, const bool MSBfirst) {
const uint8_t tolerance, const int16_t excess, const bool MSBfirst,
const bool expectlastspace) {
match_result_t result;
result.success = false; // Fail by default.
result.data = 0;
for (result.used = 0; result.used < nbits * 2;
result.used += 2, data_ptr += 2) {
// Is the bit a '1'?
if (matchMark(*data_ptr, onemark, tolerance, excess) &&
matchSpace(*(data_ptr + 1), onespace, tolerance, excess)) {
result.data = (result.data << 1) | 1;
} else if (matchMark(*data_ptr, zeromark, tolerance, excess) &&
matchSpace(*(data_ptr + 1), zerospace, tolerance, excess)) {
result.data <<= 1; // The bit is a '0'.
} else {
if (!MSBfirst) result.data = reverseBits(result.data, result.used / 2);
return result; // It's neither, so fail.
if (expectlastspace) { // We are expecting data with a final space.
for (result.used = 0; result.used < nbits * 2;
result.used += 2, data_ptr += 2) {
// Is the bit a '1'?
if (matchMark(*data_ptr, onemark, tolerance, excess) &&
matchSpace(*(data_ptr + 1), onespace, tolerance, excess)) {
result.data = (result.data << 1) | 1;
} else if (matchMark(*data_ptr, zeromark, tolerance, excess) &&
matchSpace(*(data_ptr + 1), zerospace, tolerance, excess)) {
result.data <<= 1; // The bit is a '0'.
} else {
if (!MSBfirst) result.data = reverseBits(result.data, result.used / 2);
return result; // It's neither, so fail.
}
}
result.success = true;
} else { // We are expecting data without a final space.
// Match all but the last bit, as it may not match easily.
result = matchData(data_ptr, nbits ? nbits - 1 : 0, onemark, onespace,
zeromark, zerospace, tolerance, excess, true, true);
if (result.success) {
// Is the bit a '1'?
if (matchMark(*(data_ptr + result.used), onemark, tolerance, excess))
result.data = (result.data << 1) | 1;
else if (matchMark(*(data_ptr + result.used), zeromark, tolerance,
excess))
result.data <<= 1; // The bit is a '0'.
else
result.success = false;
if (result.success) result.used++;
}
}
result.success = true;
if (!MSBfirst) result.data = reverseBits(result.data, nbits);
return result;
}
Expand All @@ -1245,20 +1272,23 @@ match_result_t IRrecv::matchData(
/// @param[in] excess Nr. of uSeconds. (Def: kMarkExcess)
/// @param[in] MSBfirst Bit order to save the data in. (Def: true)
/// true is Most Significant Bit First Order, false is Least Significant First
/// @param[in] expectlastspace Do we expect a space at the end of the message?
/// @return If successful, how many buffer entries were used. Otherwise 0.
uint16_t IRrecv::matchBytes(volatile uint16_t *data_ptr, uint8_t *result_ptr,
const uint16_t remaining, const uint16_t nbytes,
const uint16_t onemark, const uint32_t onespace,
const uint16_t zeromark, const uint32_t zerospace,
const uint8_t tolerance, const int16_t excess,
const bool MSBfirst) {
const bool MSBfirst, const bool expectlastspace) {
// Check if there is enough capture buffer to possibly have the desired bytes.
if (remaining < nbytes * 8 * 2) return 0; // Nope, so abort.
if (remaining + expectlastspace < (nbytes * 8 * 2) + 1)
return 0; // Nope, so abort.
uint16_t offset = 0;
for (uint16_t byte_pos = 0; byte_pos < nbytes; byte_pos++) {
bool lastspace = (byte_pos + 1 == nbytes) ? expectlastspace : true;
match_result_t result = matchData(data_ptr + offset, 8, onemark, onespace,
zeromark, zerospace, tolerance, excess,
MSBfirst);
MSBfirst, lastspace);
if (result.success == false) return 0; // Fail
result_ptr[byte_pos] = (uint8_t)result.data;
offset += result.used;
Expand Down Expand Up @@ -1316,8 +1346,10 @@ uint16_t IRrecv::_matchGeneric(volatile uint16_t *data_ptr,
const bool MSBfirst) {
// If we are expecting byte sizes, check it's a factor of 8 or fail.
if (!use_bits && nbits % 8 != 0) return 0;
// Calculate if we expect a trailing space in the data section.
const bool kexpectspace = footermark || (onespace != zerospace);
// Calculate how much remaining buffer is required.
uint16_t min_remaining = nbits * 2;
uint16_t min_remaining = nbits * 2 - (kexpectspace ? 0 : 1);

if (hdrmark) min_remaining++;
if (hdrspace) min_remaining++;
Expand All @@ -1340,7 +1372,7 @@ uint16_t IRrecv::_matchGeneric(volatile uint16_t *data_ptr,
match_result_t result = IRrecv::matchData(data_ptr + offset, nbits,
onemark, onespace,
zeromark, zerospace, tolerance,
excess, MSBfirst);
excess, MSBfirst, kexpectspace);
if (!result.success) return 0;
*result_bits_ptr = result.data;
offset += result.used;
Expand All @@ -1349,7 +1381,7 @@ uint16_t IRrecv::_matchGeneric(volatile uint16_t *data_ptr,
remaining - offset, nbits / 8,
onemark, onespace,
zeromark, zerospace, tolerance,
excess, MSBfirst);
excess, MSBfirst, kexpectspace);
if (!data_used) return 0;
offset += data_used;
}
Expand Down
11 changes: 9 additions & 2 deletions src/IRrecv.h
Original file line number Diff line number Diff line change
Expand Up @@ -192,14 +192,16 @@ class IRrecv {
const uint16_t zeromark, const uint32_t zerospace,
const uint8_t tolerance = kUseDefTol,
const int16_t excess = kMarkExcess,
const bool MSBfirst = true);
const bool MSBfirst = true,
const bool expectlastspace = true);
uint16_t matchBytes(volatile uint16_t *data_ptr, uint8_t *result_ptr,
const uint16_t remaining, const uint16_t nbytes,
const uint16_t onemark, const uint32_t onespace,
const uint16_t zeromark, const uint32_t zerospace,
const uint8_t tolerance = kUseDefTol,
const int16_t excess = kMarkExcess,
const bool MSBfirst = true);
const bool MSBfirst = true,
const bool expectlastspace = true);
uint16_t matchGeneric(volatile uint16_t *data_ptr,
uint64_t *result_ptr,
const uint16_t remaining, const uint16_t nbits,
Expand Down Expand Up @@ -509,6 +511,11 @@ class IRrecv {
const uint16_t nbits = kLasertagBits,
const bool strict = true);
#endif
#if DECODE_MILESTAG2
bool decodeMilestag2(decode_results *results, uint16_t offset = kStartOffset,
const uint16_t nbits = kMilesTag2ShotBits,
const bool strict = true);
#endif
#if DECODE_CARRIER_AC
bool decodeCarrierAC(decode_results *results, uint16_t offset = kStartOffset,
const uint16_t nbits = kCarrierAcBits,
Expand Down
13 changes: 12 additions & 1 deletion src/IRremoteESP8266.h
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,13 @@
#define SEND_ELITESCREENS _IR_ENABLE_DEFAULT_
#endif // SEND_ELITESCREENS

#ifndef DECODE_MILESTAG2
#define DECODE_MILESTAG2 _IR_ENABLE_DEFAULT_
#endif // DECODE_MILESTAG2
#ifndef SEND_MILESTAG2
#define SEND_MILESTAG2 _IR_ENABLE_DEFAULT_
#endif // SEND_MILESTAG2

#if (DECODE_ARGO || DECODE_DAIKIN || DECODE_FUJITSU_AC || DECODE_GREE || \
DECODE_KELVINATOR || DECODE_MITSUBISHI_AC || DECODE_TOSHIBA_AC || \
DECODE_TROTEC || DECODE_HAIER_AC || DECODE_HITACHI_AC || \
Expand Down Expand Up @@ -867,8 +874,9 @@ enum decode_type_t {
MIRAGE,
ELITESCREENS, // 95
PANASONIC_AC32,
MILESTAG2,
// Add new entries before this one, and update it to point to the last entry.
kLastDecodeType = PANASONIC_AC32,
kLastDecodeType = MILESTAG2,
};

// Message lengths & required repeat values
Expand Down Expand Up @@ -1094,6 +1102,9 @@ const uint16_t kZepealBits = 16;
const uint16_t kZepealMinRepeat = 4;
const uint16_t kVoltasBits = 80;
const uint16_t kVoltasStateLength = 10;
const uint16_t kMilesTag2ShotBits = 14;
const uint16_t kMilesTag2MsgBits = 24;
const uint16_t kMilesMinRepeat = 0;


// Legacy defines. (Deprecated)
Expand Down
7 changes: 7 additions & 0 deletions src/IRsend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,8 @@ uint16_t IRsend::defaultBits(const decode_type_t protocol) {
return kHitachiAc424Bits;
case KELVINATOR:
return kKelvinatorBits;
case MILESTAG2:
return kMilesTag2ShotBits;
case MIRAGE:
return kMirageBits;
case MITSUBISHI_AC:
Expand Down Expand Up @@ -897,6 +899,11 @@ bool IRsend::send(const decode_type_t type, const uint64_t data,
sendMidea24(data, nbits, min_repeat);
break;
#endif // SEND_MIDEA24
#if SEND_MILESTAG2
case MILESTAG2:
sendMilestag2(data, nbits, min_repeat);
break;
#endif // SEND_MILESTAG2
#if SEND_MITSUBISHI
case MITSUBISHI:
sendMitsubishi(data, nbits, min_repeat);
Expand Down
7 changes: 7 additions & 0 deletions src/IRsend.h
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,13 @@ class IRsend {
const uint16_t nbits = kEliteScreensBits,
const uint16_t repeat = kEliteScreensDefaultRepeat);
#endif // SEND_ELITESCREENS
#if SEND_MILESTAG2
// Since There 2 types of transmissions
// (14bits for Shooting by default, you can set 24 bit for msg delivery)
void sendMilestag2(const uint64_t data,
const uint16_t nbits = kMilesTag2ShotBits,
const uint16_t repeat = kMilesMinRepeat);
#endif // SEND_MILESTAG2

protected:
#ifdef UNIT_TEST
Expand Down
1 change: 1 addition & 0 deletions src/IRtext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -278,5 +278,6 @@ const PROGMEM char *kAllProtocolNamesStr =
D_STR_MIRAGE "\x0"
D_STR_ELITESCREENS "\x0"
D_STR_PANASONIC_AC32 "\x0"
D_STR_MILESTAG2 "\x0"
///< New protocol strings should be added just above this line.
"\x0"; ///< This string requires double null termination.
113 changes: 113 additions & 0 deletions src/ir_MilesTag2.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright 2021 Victor Mukayev (vitos1k)
// Copyright 2021 David Conran (crankyoldgit)

/// @file
/// @brief Support for the MilesTag2 IR protocol for LaserTag gaming
/// @see http://hosting.cmalton.me.uk/chrism/lasertag/MT2Proto.pdf
/// @see https://github.com/crankyoldgit/IRremoteESP8266/issues/1360

// Supports:
// Brand: Milestag2, Model: Various

// TODO(vitos1k): This implementation would support only
// short SHOT packets(14bits) and MSGs = 24bits. Support
// for long MSGs > 24bits is TODO

#include <algorithm>
#include "IRrecv.h"
#include "IRsend.h"
#include "IRutils.h"

// Constants
// Shot packets have this bit as `0`
const uint16_t kMilesTag2ShotMask = 1 << (kMilesTag2ShotBits - 1);
// Msg packets have this bit as `1`
const uint32_t kMilesTag2MsgMask = 1 << (kMilesTag2MsgBits - 1);
const uint8_t kMilesTag2MsgTerminator = 0xE8;
const uint16_t kMilesTag2HdrMark = 2400; /// uSeconds.
const uint16_t kMilesTag2Space = 600; /// uSeconds.
const uint16_t kMilesTag2OneMark = 1200; /// uSeconds.
const uint16_t kMilesTag2ZeroMark = 600; /// uSeconds.
const uint16_t kMilesTag2RptLength = 32000; /// uSeconds.
const uint16_t kMilesTag2StdFreq = 38000; /// Hz.
const uint16_t kMilesTag2StdDuty = 25; /// Percentage.

#if SEND_MILESTAG2
/// Send a MilesTag2 formatted Shot/Msg packet.
/// Status: ALPHA / Probably works but needs testing with a real device.
/// @param[in] data The message to be sent.
/// @param[in] nbits The number of bits of message to be sent.
/// @param[in] repeat The number of times the command is to be repeated.
void IRsend::sendMilestag2(const uint64_t data, const uint16_t nbits,
const uint16_t repeat) {
sendGeneric(
kMilesTag2HdrMark, kMilesTag2Space, // Header
kMilesTag2OneMark, kMilesTag2Space, // 1 bit
kMilesTag2ZeroMark, kMilesTag2Space, // 0 bit
0, // No footer mark
kMilesTag2RptLength, data, nbits, kMilesTag2StdFreq, true, // MSB First
repeat, kMilesTag2StdDuty);
}
#endif // SEND_MILESTAG2

#if DECODE_MILESTAG2
/// Decode the supplied MilesTag2 message.
/// Status: ALPHA / Probably works but needs testing with a real device.
/// @param[in,out] results Ptr to the data to decode & where to store the result
/// @param[in] offset The starting index to use when attempting to decode the
/// raw data. Typically/Defaults to kStartOffset.
/// @param[in] nbits The number of data bits to expect.
/// @param[in] strict Flag indicating if we should perform strict matching.
/// @return True if it can decode it, false if it can't.
/// @see https://github.com/crankyoldgit/IRremoteESP8266/issues/1360
bool IRrecv::decodeMilestag2(decode_results *results, uint16_t offset,
const uint16_t nbits, const bool strict) {
uint64_t data = 0;
// Header + Data + Optional Footer
if (!matchGeneric(results->rawbuf + offset, &data,
results->rawlen - offset, nbits,
kMilesTag2HdrMark, kMilesTag2Space,
kMilesTag2OneMark, kMilesTag2Space,
kMilesTag2ZeroMark, kMilesTag2Space,
0, kMilesTag2RptLength, true)) return false;

// Compliance
if (strict) {
switch (nbits) {
case kMilesTag2ShotBits:
// Is it a valid shot packet?
if (data & kMilesTag2ShotMask) return false;
break;
case kMilesTag2MsgBits:
// Is it a valid msg packet? i.e. Msg bit set & Terminator present.
if (!(data & kMilesTag2MsgMask) ||
((data & 0xFF) != kMilesTag2MsgTerminator))
return false;
break;
default:
DPRINT("incorrect nbits:");
DPRINTLN(nbits);
return false; // The request doesn't strictly match the protocol defn.
}
}

// Success
results->bits = nbits;
results->value = data;
results->decode_type = decode_type_t::MILESTAG2;
switch (nbits) {
case kMilesTag2ShotBits:
results->command = data & 0x3F; // Team & Damage
results->address = data >> 6; // Player ID.
break;
case kMilesTag2MsgBits:
results->command = (data >> 8) & 0xFF; // Message data
results->address = (data >> 16) & 0x7F; // Message ID
break;
default:
results->command = 0;
results->address = 0;
}
return true;
}
#endif // DECODE_MILESTAG2
3 changes: 3 additions & 0 deletions src/locale/defaults.h
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,9 @@
#ifndef D_STR_MIDEA24
#define D_STR_MIDEA24 "MIDEA24"
#endif // D_STR_MIDEA24
#ifndef D_STR_MILESTAG2
#define D_STR_MILESTAG2 "MILESTAG2"
#endif // D_STR_MILESTAG2
#ifndef D_STR_MIRAGE
#define D_STR_MIRAGE "MIRAGE"
#endif // D_STR_MIRAGE
Expand Down
Loading

0 comments on commit 667ed87

Please sign in to comment.