Initial public release of SystemK.

This commit is contained in:
Joe Kearney 2025-01-25 13:45:14 -06:00
parent 387f57cdda
commit 6f51f5b006
129 changed files with 11654 additions and 2 deletions

325
Protocols/Dubuque.c Normal file
View file

@ -0,0 +1,325 @@
/*
* This program source code file is part of SystemK, a library in the KTag project.
*
* 🛡 <https://ktag.clubk.club> 🃞
*
* Copyright © 2024-2025 Joseph P. Kearney and the KTag developers.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* There should be a copy of the GNU Affero General Public License in the LICENSE
* file in the root of this repository. If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdbool.h>
#include <stdio.h>
#include "SystemK.h"
#define DBQ_PURPLE_TEAM 0b00
#define DBQ_RED_TEAM 0b01
#define DBQ_BLUE_TEAM 0b10
#define DBQ_WHITE_TEAM 0b11
#define DBQ_ZERO_MARK_DURATION_IN_us 400
#define DBQ_ZERO_SPACE_DURATION_IN_us 800
#define DBQ_ONE_MARK_DURATION_IN_us 800
#define DBQ_ONE_SPACE_DURATION_IN_us 400
#define DBQ_TOLERANCE_IN_us 199
#define DBQ_PURPLE_TX_GAP_IN_us 2600
#define DBQ_RED_TX_GAP_IN_us 5000
#define DBQ_BLUE_TX_GAP_IN_us 7400
#define DBQ_WHITE_TX_GAP_IN_us 9800
#define MIN_ZERO_MARK_IN_us (DBQ_ZERO_MARK_DURATION_IN_us - DBQ_TOLERANCE_IN_us)
#define MAX_ZERO_MARK_IN_us (DBQ_ZERO_MARK_DURATION_IN_us + DBQ_TOLERANCE_IN_us)
#define MIN_ZERO_SPACE_IN_us (DBQ_ZERO_SPACE_DURATION_IN_us - DBQ_TOLERANCE_IN_us)
#define MAX_ZERO_SPACE_IN_us (DBQ_ZERO_SPACE_DURATION_IN_us + DBQ_TOLERANCE_IN_us)
#define MIN_ONE_MARK_IN_us (DBQ_ONE_MARK_DURATION_IN_us - DBQ_TOLERANCE_IN_us)
#define MAX_ONE_MARK_IN_us (DBQ_ONE_MARK_DURATION_IN_us + DBQ_TOLERANCE_IN_us)
#define MIN_ONE_SPACE_IN_us (DBQ_ONE_SPACE_DURATION_IN_us - DBQ_TOLERANCE_IN_us)
#define MAX_ONE_SPACE_IN_us (DBQ_ONE_SPACE_DURATION_IN_us + DBQ_TOLERANCE_IN_us)
static const TimedBit_T DBQ_ZERO_MARK = {.symbol = MARK, .duration = DBQ_ZERO_MARK_DURATION_IN_us};
static const TimedBit_T DBQ_ONE_MARK = {.symbol = MARK, .duration = DBQ_ONE_MARK_DURATION_IN_us};
static const TimedBit_T DBQ_ZERO_SPACE = {.symbol = SPACE, .duration = DBQ_ZERO_SPACE_DURATION_IN_us};
static const TimedBit_T DBQ_ONE_SPACE = {.symbol = SPACE, .duration = DBQ_ONE_SPACE_DURATION_IN_us};
static const TimedBit_T DBQ_PURPLE_GAP = {.symbol = SPACE, .duration = DBQ_ZERO_SPACE_DURATION_IN_us + DBQ_PURPLE_TX_GAP_IN_us};
static const TimedBit_T DBQ_RED_GAP = {.symbol = SPACE, .duration = DBQ_ONE_SPACE_DURATION_IN_us + DBQ_RED_TX_GAP_IN_us};
static const TimedBit_T DBQ_BLUE_GAP = {.symbol = SPACE, .duration = DBQ_ZERO_SPACE_DURATION_IN_us + DBQ_BLUE_TX_GAP_IN_us};
static const TimedBit_T DBQ_WHITE_GAP = {.symbol = SPACE, .duration = DBQ_ONE_SPACE_DURATION_IN_us + DBQ_WHITE_TX_GAP_IN_us};
static const TimedBit_T DBQ_END = {.symbol = SPACE, .duration = LAST_PULSE};
static TimedPulseTrain_T Tag_Send_Buffer;
static DecodedPacket_T DBQ_Tag_Received_Buffers[3] =
{
{.Tag.type = DECODED_PACKET_TYPE_BUFFER_FREE},
{.Tag.type = DECODED_PACKET_TYPE_BUFFER_FREE},
{.Tag.type = DECODED_PACKET_TYPE_BUFFER_FREE}
};
static uint_fast16_t Team_To_Ignore = DBQ_WHITE_TEAM;
static inline DecodedPacket_T *Get_Tag_Packet_Buffer(void)
{
if (DBQ_Tag_Received_Buffers[0].Tag.type == DECODED_PACKET_TYPE_BUFFER_FREE)
{
return &DBQ_Tag_Received_Buffers[0];
}
else if (DBQ_Tag_Received_Buffers[1].Tag.type == DECODED_PACKET_TYPE_BUFFER_FREE)
{
return &DBQ_Tag_Received_Buffers[1];
}
else
{
// Just use it.
return &DBQ_Tag_Received_Buffers[2];
}
}
TimedPulseTrain_T *DBQ_EncodePacket(TagPacket_T *packet)
{
if (packet->team_ID == DBQ_PURPLE_TEAM)
{
Tag_Send_Buffer.bitstream[0] = DBQ_ZERO_MARK;
Tag_Send_Buffer.bitstream[1] = DBQ_ZERO_SPACE;
Tag_Send_Buffer.bitstream[2] = DBQ_ZERO_MARK;
Tag_Send_Buffer.bitstream[3] = DBQ_PURPLE_GAP;
Tag_Send_Buffer.bitstream[4] = DBQ_ZERO_MARK;
Tag_Send_Buffer.bitstream[5] = DBQ_ZERO_SPACE;
Tag_Send_Buffer.bitstream[6] = DBQ_ZERO_MARK;
Tag_Send_Buffer.bitstream[7] = DBQ_ZERO_SPACE;
Tag_Send_Buffer.bitstream[8] = DBQ_END;
}
else if (packet->team_ID == DBQ_RED_TEAM)
{
Team_To_Ignore = DBQ_RED_TEAM;
Tag_Send_Buffer.bitstream[0] = DBQ_ZERO_MARK;
Tag_Send_Buffer.bitstream[1] = DBQ_ZERO_SPACE;
Tag_Send_Buffer.bitstream[2] = DBQ_ONE_MARK;
Tag_Send_Buffer.bitstream[3] = DBQ_RED_GAP;
Tag_Send_Buffer.bitstream[4] = DBQ_ZERO_MARK;
Tag_Send_Buffer.bitstream[5] = DBQ_ZERO_SPACE;
Tag_Send_Buffer.bitstream[6] = DBQ_ONE_MARK;
Tag_Send_Buffer.bitstream[7] = DBQ_ONE_SPACE;
Tag_Send_Buffer.bitstream[8] = DBQ_END;
}
else if (packet->team_ID == DBQ_BLUE_TEAM)
{
Team_To_Ignore = DBQ_BLUE_TEAM;
Tag_Send_Buffer.bitstream[0] = DBQ_ONE_MARK;
Tag_Send_Buffer.bitstream[1] = DBQ_ONE_SPACE;
Tag_Send_Buffer.bitstream[2] = DBQ_ZERO_MARK;
Tag_Send_Buffer.bitstream[3] = DBQ_BLUE_GAP;
Tag_Send_Buffer.bitstream[4] = DBQ_ONE_MARK;
Tag_Send_Buffer.bitstream[5] = DBQ_ONE_SPACE;
Tag_Send_Buffer.bitstream[6] = DBQ_ZERO_MARK;
Tag_Send_Buffer.bitstream[7] = DBQ_ZERO_SPACE;
Tag_Send_Buffer.bitstream[8] = DBQ_END;
}
else // if (packet->team_ID == DBQ_WHITE_TEAM)
{
Tag_Send_Buffer.bitstream[0] = DBQ_ONE_MARK;
Tag_Send_Buffer.bitstream[1] = DBQ_ONE_SPACE;
Tag_Send_Buffer.bitstream[2] = DBQ_ONE_MARK;
Tag_Send_Buffer.bitstream[3] = DBQ_WHITE_GAP;
Tag_Send_Buffer.bitstream[4] = DBQ_ONE_MARK;
Tag_Send_Buffer.bitstream[5] = DBQ_ONE_SPACE;
Tag_Send_Buffer.bitstream[6] = DBQ_ONE_MARK;
Tag_Send_Buffer.bitstream[7] = DBQ_ONE_SPACE;
Tag_Send_Buffer.bitstream[8] = DBQ_END;
}
Tag_Send_Buffer.count = 8;
return &Tag_Send_Buffer;
}
DecodedPacket_T *DBQ_MaybeDecodePacket(TimedPulseTrain_T *packet)
{
uint_fast8_t decoded_data = UINT_FAST8_MAX;
uint_fast8_t consecutive_bits = 0;
uint_fast8_t ignored_data = UINT_FAST8_MAX;
// Special handling for the purple team.
uint_fast16_t collisions = 0;
uint_fast16_t purple_completions = 0;
// Loop through all the pulses, looking for a match.
for (uint8_t n = 0; n < packet->count; n += 2)
{
// Even pulses are marks; odd pulses are spaces.
if (consecutive_bits == 0)
{
// Perform full checking on the first bit.
if ((packet->bitstream[n].duration > MIN_ONE_MARK_IN_us) && (packet->bitstream[n].duration < MAX_ONE_MARK_IN_us) &&
(packet->bitstream[n + 1].duration > MIN_ONE_SPACE_IN_us) && (packet->bitstream[n + 1].duration < MAX_ONE_SPACE_IN_us))
{
// This is a "one".
decoded_data = 2;
consecutive_bits = 1;
}
else if ((packet->bitstream[n].duration > MIN_ZERO_MARK_IN_us) && (packet->bitstream[n].duration < MAX_ZERO_MARK_IN_us) &&
(packet->bitstream[n + 1].duration > MIN_ZERO_SPACE_IN_us) && (packet->bitstream[n + 1].duration < MAX_ZERO_SPACE_IN_us))
{
// This is a "zero".
decoded_data = 0;
consecutive_bits = 1;
}
else
{
// This mark/space combination is neither a zero or a one; continue.
collisions++;
}
}
else // consecutive_bits > 0
{
// One the second bit, full check the mark, and make sure the space is long enough.
if ((packet->bitstream[n].duration > MIN_ONE_MARK_IN_us) && (packet->bitstream[n].duration < MAX_ONE_MARK_IN_us) &&
((packet->bitstream[n + 1].duration > MIN_ONE_SPACE_IN_us) || (packet->bitstream[n + 1].duration == LAST_PULSE)))
{
// This is a "one".
decoded_data += 1;
if (decoded_data == Team_To_Ignore)
{
// We received a packet we should ignore. Remember this, and keep looking.
ignored_data = decoded_data;
decoded_data = UINT_FAST8_MAX;
consecutive_bits = 0;
}
else if (decoded_data == DBQ_PURPLE_TEAM)
{
// We received a purple packet. Remember this, and keep looking.
purple_completions++;
decoded_data = UINT_FAST8_MAX;
consecutive_bits = 0;
}
else
{
break;
}
}
else if ((packet->bitstream[n].duration > MIN_ZERO_MARK_IN_us) && (packet->bitstream[n].duration < MAX_ZERO_MARK_IN_us) &&
((packet->bitstream[n + 1].duration > MIN_ZERO_SPACE_IN_us) || (packet->bitstream[n + 1].duration == LAST_PULSE)))
{
// This is a "zero".
decoded_data += 0;
if (decoded_data == Team_To_Ignore)
{
// We received a packet we should ignore. Remember this, and keep looking.
ignored_data = decoded_data;
decoded_data = UINT_FAST8_MAX;
consecutive_bits = 0;
}
else if (decoded_data == DBQ_PURPLE_TEAM)
{
// We received a purple packet. Remember this, and keep looking.
purple_completions++;
decoded_data = UINT_FAST8_MAX;
consecutive_bits = 0;
}
else
{
break;
}
}
else
{
// This mark/space combination is neither a zero or a one; reset.
collisions++;
decoded_data = UINT_FAST8_MAX;
consecutive_bits = 0;
}
}
}
// If we ignored the only data we received, restore it here.
if ((ignored_data != UINT_FAST8_MAX) && (decoded_data == UINT_FAST8_MAX))
{
decoded_data = ignored_data;
}
if (purple_completions > 0)
{
DecodedPacket_T *Tag_Rx_Buffer = Get_Tag_Packet_Buffer();
Tag_Rx_Buffer->Tag.type = DECODED_PACKET_TYPE_TAG_RECEIVED;
Tag_Rx_Buffer->Tag.protocol = DUBUQUE_PROTOCOL;
Tag_Rx_Buffer->Tag.player_ID = 0;
Tag_Rx_Buffer->Tag.team_ID = DBQ_PURPLE_TEAM;
Tag_Rx_Buffer->Tag.damage = 10 * (purple_completions + collisions);
Tag_Rx_Buffer->Tag.color = COLOR_PURPLE;
return Tag_Rx_Buffer;
}
else if (decoded_data == DBQ_RED_TEAM)
{
DecodedPacket_T *Tag_Rx_Buffer = Get_Tag_Packet_Buffer();
Tag_Rx_Buffer->Tag.type = DECODED_PACKET_TYPE_TAG_RECEIVED;
Tag_Rx_Buffer->Tag.protocol = DUBUQUE_PROTOCOL;
Tag_Rx_Buffer->Tag.player_ID = 0;
Tag_Rx_Buffer->Tag.team_ID = DBQ_RED_TEAM;
Tag_Rx_Buffer->Tag.damage = 10;
Tag_Rx_Buffer->Tag.color = COLOR_RED;
return Tag_Rx_Buffer;
}
else if (decoded_data == DBQ_BLUE_TEAM)
{
DecodedPacket_T *Tag_Rx_Buffer = Get_Tag_Packet_Buffer();
Tag_Rx_Buffer->Tag.type = DECODED_PACKET_TYPE_TAG_RECEIVED;
Tag_Rx_Buffer->Tag.protocol = DUBUQUE_PROTOCOL;
Tag_Rx_Buffer->Tag.player_ID = 0;
Tag_Rx_Buffer->Tag.team_ID = DBQ_BLUE_TEAM;
Tag_Rx_Buffer->Tag.damage = 10;
Tag_Rx_Buffer->Tag.color = COLOR_BLUE;
return Tag_Rx_Buffer;
}
else if (decoded_data == DBQ_WHITE_TEAM)
{
DecodedPacket_T *Tag_Rx_Buffer = Get_Tag_Packet_Buffer();
Tag_Rx_Buffer->Tag.type = DECODED_PACKET_TYPE_TAG_RECEIVED;
Tag_Rx_Buffer->Tag.protocol = DUBUQUE_PROTOCOL;
Tag_Rx_Buffer->Tag.player_ID = 0;
Tag_Rx_Buffer->Tag.team_ID = DBQ_WHITE_TEAM;
Tag_Rx_Buffer->Tag.damage = 10;
Tag_Rx_Buffer->Tag.color = COLOR_WHITE;
return Tag_Rx_Buffer;
}
return NULL;
}
color_t DBQ_GetTeamColor(uint8_t team_ID)
{
color_t result = COLOR_PURPLE;
if (team_ID == DBQ_RED_TEAM)
{
result = COLOR_RED;
}
else if (team_ID == DBQ_BLUE_TEAM)
{
result = COLOR_BLUE;
}
else if (team_ID == DBQ_WHITE_TEAM)
{
result = COLOR_WHITE;
}
else
{
// Nothing to do.
}
return result;
}

39
Protocols/Dubuque.h Normal file
View file

@ -0,0 +1,39 @@
/*
* This program source code file is part of SystemK, a library in the KTag project.
*
* 🛡 <https://ktag.clubk.club> 🃞
*
* Copyright © 2024-2025 Joseph P. Kearney and the KTag developers.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* There should be a copy of the GNU Affero General Public License in the LICENSE
* file in the root of this repository. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef DUBUQUE_H
#define DUBUQUE_H
/** \file
* \brief This is the Dubuque protocol.
*
* \note The Dubuque protocol uses a 38kHz carrier for On-Off Keying.
*
*/
#include <stdbool.h>
#include <stdint.h>
TimedPulseTrain_T *DBQ_EncodePacket(TagPacket_T *packet);
DecodedPacket_T *DBQ_MaybeDecodePacket(TimedPulseTrain_T *packet);
color_t DBQ_GetTeamColor(uint8_t team_ID);
#endif // DUBUQUE_H

214
Protocols/Dynasty.c Executable file
View file

@ -0,0 +1,214 @@
/*
* This program source code file is part of SystemK, a library in the KTag project.
*
* 🛡 <https://ktag.clubk.club> 🃞
*
* Copyright © 2016-2025 Joseph P. Kearney and the KTag developers.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* There should be a copy of the GNU Affero General Public License in the LICENSE
* file in the root of this repository. If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdbool.h>
#include <stdio.h>
#include "SystemK.h"
#define DYNASTY_BLUE_TEAM 0x01
#define DYNASTY_RED_TEAM 0x02
#define DYNASTY_GREEN_TEAM 0x03
#define DYNASTY_WHITE_TEAM 0x04
#define DYNASTY_HEADER_MARK_DURATION_IN_us 1700
#define DYNASTY_ZERO_DURATION_IN_us 450
#define DYNASTY_ONE_DURATION_IN_us 800
#define DYNASTY_TOLERANCE_IN_us 150
#define MIN_ZERO_IN_us (DYNASTY_ZERO_DURATION_IN_us - DYNASTY_TOLERANCE_IN_us)
#define MAX_ZERO_IN_us (DYNASTY_ZERO_DURATION_IN_us + DYNASTY_TOLERANCE_IN_us)
#define MIN_ONE_IN_us (DYNASTY_ONE_DURATION_IN_us - DYNASTY_TOLERANCE_IN_us)
#define MAX_ONE_IN_us (DYNASTY_ONE_DURATION_IN_us + DYNASTY_TOLERANCE_IN_us)
static const TimedBit_T DYNASTY_HEADER_MARK = {.symbol = MARK, .duration = DYNASTY_HEADER_MARK_DURATION_IN_us};
static const TimedBit_T DYNASTY_ZERO_SPACE = {.symbol = SPACE, .duration = DYNASTY_ZERO_DURATION_IN_us};
static const TimedBit_T DYNASTY_ONE_SPACE = {.symbol = SPACE, .duration = DYNASTY_ONE_DURATION_IN_us};
static const TimedBit_T DYNASTY_ZERO_MARK = {.symbol = MARK, .duration = DYNASTY_ZERO_DURATION_IN_us};
static const TimedBit_T DYNASTY_ONE_MARK = {.symbol = MARK, .duration = DYNASTY_ONE_DURATION_IN_us};
static const TimedBit_T DYNASTY_END = {.symbol = SPACE, .duration = LAST_PULSE};
static TimedPulseTrain_T Tag_Send_Buffer;
static DecodedPacket_T Dynasty_Tag_Received_Buffers[3] =
{
{.Tag.type = DECODED_PACKET_TYPE_BUFFER_FREE},
{.Tag.type = DECODED_PACKET_TYPE_BUFFER_FREE},
{.Tag.type = DECODED_PACKET_TYPE_BUFFER_FREE}};
static inline DecodedPacket_T *Get_Tag_Packet_Buffer(void)
{
if (Dynasty_Tag_Received_Buffers[0].Tag.type == DECODED_PACKET_TYPE_BUFFER_FREE)
{
return &Dynasty_Tag_Received_Buffers[0];
}
else if (Dynasty_Tag_Received_Buffers[1].Tag.type == DECODED_PACKET_TYPE_BUFFER_FREE)
{
return &Dynasty_Tag_Received_Buffers[1];
}
else
{
// Just use it.
return &Dynasty_Tag_Received_Buffers[2];
}
}
static inline uint8_t Calculate_Checksum(uint32_t data)
{
uint8_t result = 0;
// uint8_t byte_1 = (uint8_t)((data >> 8) & 0xFF);
// uint8_t byte_0 = (uint8_t)(data & 0xFF);
// TODO: Finish implementing the Dynasty checksum.
return result;
}
static inline void PackPulseTrain(TimedPulseTrain_T *pulsetrain, uint32_t data)
{
pulsetrain->bitstream[0] = DYNASTY_HEADER_MARK;
for (uint8_t n = 0; n < 16; n++)
{
if ((data & 0x0001) == 0x0001)
{
pulsetrain->bitstream[1 + (n * 2)] = DYNASTY_ONE_SPACE;
}
else
{
pulsetrain->bitstream[1 + (n * 2)] = DYNASTY_ZERO_SPACE;
}
data = data >> 1;
if ((data & 0x0001) == 0x0001)
{
pulsetrain->bitstream[1 + (n * 2)] = DYNASTY_ONE_MARK;
}
else
{
pulsetrain->bitstream[1 + (n * 2)] = DYNASTY_ZERO_MARK;
}
data = data >> 1;
}
// TODO: Append the Dynasty checksum.
pulsetrain->bitstream[(2 * 20) - 1] = DYNASTY_END;
}
TimedPulseTrain_T *DYNASTY_EncodePacket(TagPacket_T *packet)
{
uint16_t packed_data = 0;
PackPulseTrain(&Tag_Send_Buffer, packed_data);
return &Tag_Send_Buffer;
}
DecodedPacket_T *DYNASTY_MaybeDecodePacket(TimedPulseTrain_T *packet)
{
// Some implementations (ESP32) do not report odd numbers of pulses.
if ((packet->count != 41) && (packet->count != 42))
{
// Fail fast!
return NULL;
}
else
{
// Convert pulse durations to bits.
// packet->bitstream[0] is the header mark--ignore it.
uint32_t data = 0;
for (uint8_t n = 0; n < 32; n++)
{
if ((packet->bitstream[n + 1].duration > MIN_ONE_IN_us) && (packet->bitstream[n + 1].duration < MAX_ONE_IN_us))
{
// One
data |= ((uint32_t)1 << (31 - n));
}
else if ((packet->bitstream[n + 1].duration > MIN_ZERO_IN_us) && (packet->bitstream[n + 1].duration < MAX_ZERO_IN_us))
{
// Zero--nothing to do.
}
else
{
// This mark is neither a zero or a one; abort.
return NULL;
}
}
uint8_t checksum = 0;
for (uint8_t n = 0; n < 8; n++)
{
if ((packet->bitstream[n + 33].duration > MIN_ONE_IN_us) && (packet->bitstream[n + 33].duration < MAX_ONE_IN_us))
{
// One
checksum |= ((uint8_t)1 << (7 - n));
}
else if ((packet->bitstream[n + 33].duration > MIN_ZERO_IN_us) && (packet->bitstream[n + 33].duration < MAX_ZERO_IN_us))
{
// Zero--nothing to do.
}
else
{
// This mark is neither a zero or a one; abort.
return NULL;
}
}
DecodedPacket_T *Tag_Rx_Buffer = Get_Tag_Packet_Buffer();
Tag_Rx_Buffer->Tag.type = DECODED_PACKET_TYPE_TAG_RECEIVED;
Tag_Rx_Buffer->Tag.protocol = DYNASTY_PROTOCOL;
Tag_Rx_Buffer->Tag.player_ID = 0;
Tag_Rx_Buffer->Tag.team_ID = DYNASTY_RED_TEAM;
Tag_Rx_Buffer->Tag.damage = 10;
Tag_Rx_Buffer->Tag.color = COLOR_RED;
return Tag_Rx_Buffer;
}
}
color_t DYNASTY_GetTeamColor(uint8_t team_ID)
{
color_t result = COLOR_RED;
if (team_ID == DYNASTY_RED_TEAM)
{
result = COLOR_RED;
}
else if (team_ID == DYNASTY_BLUE_TEAM)
{
result = COLOR_BLUE;
}
else if (team_ID == DYNASTY_GREEN_TEAM)
{
result = COLOR_GREEN;
}
else if (team_ID == DYNASTY_WHITE_TEAM)
{
result = COLOR_WHITE;
}
else
{
// Nothing to do.
}
return result;
}

44
Protocols/Dynasty.h Executable file
View file

@ -0,0 +1,44 @@
/*
* This program source code file is part of SystemK, a library in the KTag project.
*
* 🛡 <https://ktag.clubk.club> 🃞
*
* Copyright © 2016-2025 Joseph P. Kearney and the KTag developers.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* There should be a copy of the GNU Affero General Public License in the LICENSE
* file in the root of this repository. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef DYNASTY_H
#define DYNASTY_H
/** \file
* \brief This is the Dynasty protocol.
*
* \note These devices use a 38kHz carrier. The modulation scheme is a little more complicated
* than on-off keying: the width of each mark or space represents one bit of data. This
* results in a more time-efficient data packet.
*
* https://dynastytoys.co/
*
*/
#include <stdbool.h>
#include <stdint.h>
TimedPulseTrain_T *DYNASTY_EncodePacket(TagPacket_T *packet);
DecodedPacket_T *DYNASTY_MaybeDecodePacket(TimedPulseTrain_T *packet);
color_t DYNASTY_GetTeamColor(uint8_t team_ID);
#endif // DYNASTY_H

212
Protocols/Laser_X.c Executable file
View file

@ -0,0 +1,212 @@
/*
* This program source code file is part of SystemK, a library in the KTag project.
*
* 🛡 <https://ktag.clubk.club> 🃞
*
* Copyright © 2016-2025 Joseph P. Kearney and the KTag developers.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* There should be a copy of the GNU Affero General Public License in the LICENSE
* file in the root of this repository. If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdbool.h>
#include <stdio.h>
#include "SystemK.h"
#define LASER_X_PURPLE_TEAM 0b00
#define LASER_X_RED_TEAM 0b01
#define LASER_X_BLUE_TEAM 0b10
#define RED_TEAM_PACKET_DATA 0x4A
#define BLUE_TEAM_PACKET_DATA 0x8A
#define PURPLE_TEAM_PACKET_DATA 0xCA
#define LASER_X_HEADER_MARK_DURATION_IN_us 6500
#define LASER_X_SPACE_DURATION_IN_us 500
#define LASER_X_MARK_ZERO_DURATION_IN_us 500
#define LASER_X_MARK_ONE_DURATION_IN_us 1500
#define LASER_X_TOLERANCE_IN_us 450
#define MIN_MARK_ZERO_IN_us (LASER_X_MARK_ZERO_DURATION_IN_us - LASER_X_TOLERANCE_IN_us)
#define MAX_MARK_ZERO_IN_us (LASER_X_MARK_ZERO_DURATION_IN_us + LASER_X_TOLERANCE_IN_us)
#define MIN_MARK_ONE_IN_us (LASER_X_MARK_ONE_DURATION_IN_us - LASER_X_TOLERANCE_IN_us)
#define MAX_MARK_ONE_IN_us (LASER_X_MARK_ONE_DURATION_IN_us + LASER_X_TOLERANCE_IN_us)
static const TimedBit_T LASER_X_HEADER_MARK = {.symbol = MARK, .duration = LASER_X_HEADER_MARK_DURATION_IN_us};
static const TimedBit_T LASER_X_SPACE = {.symbol = SPACE, .duration = LASER_X_SPACE_DURATION_IN_us};
static const TimedBit_T LASER_X_ONE = {.symbol = MARK, .duration = LASER_X_MARK_ONE_DURATION_IN_us};
static const TimedBit_T LASER_X_ZERO = {.symbol = MARK, .duration = LASER_X_MARK_ZERO_DURATION_IN_us};
static const TimedBit_T LASER_X_END = {.symbol = SPACE, .duration = LAST_PULSE};
static TimedPulseTrain_T Tag_Send_Buffer;
static DecodedPacket_T Laser_X_Tag_Received_Buffers[3] =
{
{.Tag.type = DECODED_PACKET_TYPE_BUFFER_FREE},
{.Tag.type = DECODED_PACKET_TYPE_BUFFER_FREE},
{.Tag.type = DECODED_PACKET_TYPE_BUFFER_FREE}};
static inline DecodedPacket_T *Get_Tag_Packet_Buffer(void)
{
if (Laser_X_Tag_Received_Buffers[0].Tag.type == DECODED_PACKET_TYPE_BUFFER_FREE)
{
return &Laser_X_Tag_Received_Buffers[0];
}
else if (Laser_X_Tag_Received_Buffers[1].Tag.type == DECODED_PACKET_TYPE_BUFFER_FREE)
{
return &Laser_X_Tag_Received_Buffers[1];
}
else
{
// Just use it.
return &Laser_X_Tag_Received_Buffers[2];
}
}
static inline void PackPulseTrain(TimedPulseTrain_T *pulsetrain, uint8_t data)
{
pulsetrain->bitstream[0] = LASER_X_HEADER_MARK;
pulsetrain->bitstream[1] = LASER_X_SPACE;
for (uint8_t n = 0; n < 8; n++)
{
if ((data & 0x01) == 0x01)
{
pulsetrain->bitstream[2 + (n * 2)] = LASER_X_ONE;
}
else
{
pulsetrain->bitstream[2 + (n * 2)] = LASER_X_ZERO;
}
pulsetrain->bitstream[2 + (n * 2) + 1] = LASER_X_SPACE;
data = data >> 1;
}
pulsetrain->bitstream[17] = LASER_X_END;
pulsetrain->count = 17;
}
TimedPulseTrain_T *LASER_X_EncodePacket(TagPacket_T *packet)
{
uint16_t packed_data = 0;
if (packet->team_ID == LASER_X_RED_TEAM)
{
packed_data = RED_TEAM_PACKET_DATA;
}
else if (packet->team_ID == LASER_X_BLUE_TEAM)
{
packed_data = BLUE_TEAM_PACKET_DATA;
}
else // LASER_X_PURPLE_TEAM
{
packed_data = PURPLE_TEAM_PACKET_DATA;
}
PackPulseTrain(&Tag_Send_Buffer, packed_data);
return &Tag_Send_Buffer;
}
DecodedPacket_T *LASER_X_MaybeDecodePacket(TimedPulseTrain_T *packet)
{
// Some implementations (ESP32) do not report odd numbers of pulses.
if ((packet->count != 17) && (packet->count != 18))
{
// Fail fast!
return NULL;
}
else
{
// Convert pulse durations to bits.
// packet->bitstream[0] is the header mark--ignore it.
// packet->bitstream[1] is the header space--ignore it also.
uint8_t data = 0;
for (uint8_t n = 0; n < 8; n++)
{
// Even pulses are marks; odd pulses are spaces.
if ((packet->bitstream[2 + (n * 2)].duration > MIN_MARK_ONE_IN_us) && (packet->bitstream[2 + (n * 2)].duration < MAX_MARK_ONE_IN_us))
{
// One
data |= ((uint8_t)1 << n);
}
else if ((packet->bitstream[2 + (n * 2)].duration > MIN_MARK_ZERO_IN_us) && (packet->bitstream[2 + (n * 2)].duration < MAX_MARK_ZERO_IN_us))
{
// Zero--nothing to do.
}
else
{
// This mark is neither a zero or a one; abort.
return NULL;
}
}
if (data == RED_TEAM_PACKET_DATA)
{
DecodedPacket_T *Tag_Rx_Buffer = Get_Tag_Packet_Buffer();
Tag_Rx_Buffer->Tag.type = DECODED_PACKET_TYPE_TAG_RECEIVED;
Tag_Rx_Buffer->Tag.protocol = LASER_X_PROTOCOL;
Tag_Rx_Buffer->Tag.player_ID = 0;
Tag_Rx_Buffer->Tag.team_ID = LASER_X_RED_TEAM;
Tag_Rx_Buffer->Tag.damage = 10;
Tag_Rx_Buffer->Tag.color = COLOR_RED;
return Tag_Rx_Buffer;
}
else if (data == BLUE_TEAM_PACKET_DATA)
{
DecodedPacket_T *Tag_Rx_Buffer = Get_Tag_Packet_Buffer();
Tag_Rx_Buffer->Tag.type = DECODED_PACKET_TYPE_TAG_RECEIVED;
Tag_Rx_Buffer->Tag.protocol = LASER_X_PROTOCOL;
Tag_Rx_Buffer->Tag.player_ID = 0;
Tag_Rx_Buffer->Tag.team_ID = LASER_X_BLUE_TEAM;
Tag_Rx_Buffer->Tag.damage = 10;
Tag_Rx_Buffer->Tag.color = COLOR_BLUE;
return Tag_Rx_Buffer;
}
else if (data == PURPLE_TEAM_PACKET_DATA)
{
DecodedPacket_T *Tag_Rx_Buffer = Get_Tag_Packet_Buffer();
Tag_Rx_Buffer->Tag.type = DECODED_PACKET_TYPE_TAG_RECEIVED;
Tag_Rx_Buffer->Tag.protocol = LASER_X_PROTOCOL;
Tag_Rx_Buffer->Tag.player_ID = 0;
Tag_Rx_Buffer->Tag.team_ID = LASER_X_PURPLE_TEAM;
Tag_Rx_Buffer->Tag.damage = 10;
Tag_Rx_Buffer->Tag.color = COLOR_PURPLE;
return Tag_Rx_Buffer;
}
return NULL;
}
}
color_t LASER_X_GetTeamColor(uint8_t team_ID)
{
color_t result = COLOR_PURPLE;
if (team_ID == LASER_X_RED_TEAM)
{
result = COLOR_RED;
}
else if (team_ID == LASER_X_BLUE_TEAM)
{
result = COLOR_BLUE;
}
else
{
// Nothing to do.
}
return result;
}

42
Protocols/Laser_X.h Executable file
View file

@ -0,0 +1,42 @@
/*
* This program source code file is part of SystemK, a library in the KTag project.
*
* 🛡 <https://ktag.clubk.club> 🃞
*
* Copyright © 2016-2025 Joseph P. Kearney and the KTag developers.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* There should be a copy of the GNU Affero General Public License in the LICENSE
* file in the root of this repository. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef LASER_X_H
#define LASER_X_H
/** \file
* \brief This is the Laser X protocol.
*
* \note These devices use 38kHz carrier for On-Off Keying.
*
* https://www.getlaserx.com/
*
*/
#include <stdbool.h>
#include <stdint.h>
TimedPulseTrain_T *LASER_X_EncodePacket(TagPacket_T *packet);
DecodedPacket_T *LASER_X_MaybeDecodePacket(TimedPulseTrain_T *packet);
color_t LASER_X_GetTeamColor(uint8_t team_ID);
#endif // LASER_X_H

195
Protocols/Miles_Tag_II.c Executable file
View file

@ -0,0 +1,195 @@
/*
* This program source code file is part of SystemK, a library in the KTag project.
*
* 🛡 <https://ktag.clubk.club> 🃞
*
* Copyright © 2016-2025 Joseph P. Kearney and the KTag developers.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* There should be a copy of the GNU Affero General Public License in the LICENSE
* file in the root of this repository. If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdbool.h>
#include <stdio.h>
#include "SystemK.h"
#define MILES_TAG_RED_TEAM 0b00
#define MILES_TAG_BLUE_TEAM 0b01
#define MILES_TAG_YELLOW_TEAM 0b10
#define MILES_TAG_GREEN_TEAM 0b11
#define MILES_TAG_II_HEADER_PULSE_DURATION_IN_us 2400
#define MILES_TAG_II_SPACE_DURATION_IN_us 600
#define MILES_TAG_II_MARK_ZERO_DURATION_IN_us 600
#define MILES_TAG_II_MARK_ONE_DURATION_IN_us 1200
#define MILES_TAG_II_TOLERANCE_IN_us 300
#define MIN_SPACE_IN_us (MILES_TAG_II_SPACE_DURATION_IN_us - MILES_TAG_II_TOLERANCE_IN_us)
#define MAX_SPACE_IN_us (MILES_TAG_II_SPACE_DURATION_IN_us + MILES_TAG_II_TOLERANCE_IN_us)
#define MIN_MARK_ZERO_IN_us (MILES_TAG_II_MARK_ZERO_DURATION_IN_us - MILES_TAG_II_TOLERANCE_IN_us)
#define MAX_MARK_ZERO_IN_us (MILES_TAG_II_MARK_ZERO_DURATION_IN_us + MILES_TAG_II_TOLERANCE_IN_us)
#define MIN_MARK_ONE_IN_us (MILES_TAG_II_MARK_ONE_DURATION_IN_us - MILES_TAG_II_TOLERANCE_IN_us)
#define MAX_MARK_ONE_IN_us (MILES_TAG_II_MARK_ONE_DURATION_IN_us + MILES_TAG_II_TOLERANCE_IN_us)
static const TimedBit_T MILES_TAG_HEADER = {.symbol = MARK, .duration = MILES_TAG_II_HEADER_PULSE_DURATION_IN_us};
static const TimedBit_T MILES_TAG_SPACE = {.symbol = SPACE, .duration = MILES_TAG_II_SPACE_DURATION_IN_us};
static const TimedBit_T MILES_TAG_ONE = {.symbol = MARK, .duration = MILES_TAG_II_MARK_ONE_DURATION_IN_us};
static const TimedBit_T MILES_TAG_ZERO = {.symbol = MARK, .duration = MILES_TAG_II_MARK_ZERO_DURATION_IN_us};
static const TimedBit_T MILES_TAG_END = {.symbol = SPACE, .duration = LAST_PULSE};
static TimedPulseTrain_T Tag_Send_Buffer;
static DecodedPacket_T Miles_Tag_II_Tag_Received_Buffers[3] =
{
{.Tag.type = DECODED_PACKET_TYPE_BUFFER_FREE},
{.Tag.type = DECODED_PACKET_TYPE_BUFFER_FREE},
{.Tag.type = DECODED_PACKET_TYPE_BUFFER_FREE}};
static inline DecodedPacket_T *Get_Tag_Packet_Buffer(void)
{
if (Miles_Tag_II_Tag_Received_Buffers[0].Tag.type == DECODED_PACKET_TYPE_BUFFER_FREE)
{
return &Miles_Tag_II_Tag_Received_Buffers[0];
}
else if (Miles_Tag_II_Tag_Received_Buffers[1].Tag.type == DECODED_PACKET_TYPE_BUFFER_FREE)
{
return &Miles_Tag_II_Tag_Received_Buffers[1];
}
else
{
// Just use it.
return &Miles_Tag_II_Tag_Received_Buffers[2];
}
}
static const uint16_t Miles_Tag_II_Damage[] = {1, 2, 4, 5, 7, 10, 15, 17, 20, 25, 30, 35, 40, 50, 75, 100};
static const color_t Miles_Tag_II_Team_Colors[] = {COLOR_RED, COLOR_BLUE, COLOR_YELLOW, COLOR_GREEN};
static inline uint8_t Encode_Damage(uint16_t damage)
{
uint8_t encoded_damage;
for (encoded_damage = 0; encoded_damage < 16; encoded_damage++)
{
if (damage <= Miles_Tag_II_Damage[encoded_damage])
{
break;
}
}
return encoded_damage;
}
// Use the 14 least-significant bits.
static inline void PackPulseTrain(TimedPulseTrain_T *pulsetrain, uint16_t data)
{
pulsetrain->bitstream[0] = MILES_TAG_HEADER;
pulsetrain->bitstream[1] = MILES_TAG_SPACE;
for (uint8_t n = 0; n < 14; n++)
{
if ((data & 0x01) == 0x01)
{
pulsetrain->bitstream[2 + (n * 2)] = MILES_TAG_ONE;
}
else
{
pulsetrain->bitstream[2 + (n * 2)] = MILES_TAG_ZERO;
}
pulsetrain->bitstream[2 + (n * 2) + 1] = MILES_TAG_SPACE;
data = data >> 1;
}
pulsetrain->bitstream[30] = MILES_TAG_END;
}
TimedPulseTrain_T *MILES_TAG_II_EncodePacket(TagPacket_T *packet)
{
uint16_t packed_data = Encode_Damage(packet->damage);
packed_data |= (packet->team_ID << 4);
packed_data |= (packet->player_ID << 6);
PackPulseTrain(&Tag_Send_Buffer, packed_data);
return &Tag_Send_Buffer;
}
DecodedPacket_T *MILES_TAG_II_MaybeDecodePacket(TimedPulseTrain_T *packet)
{
// Some implementations (ESP32) do not report odd numbers of pulses.
if ((packet->count != 29) && (packet->count != 30))
{
// Fail fast!
return NULL;
}
else
{
// Convert pulse durations to bits.
// packet->bitstream[0] is the header--ignore it.
// packet->bitstream[1] is the space after the header--ignore it also.
uint16_t data = 0;
for (uint8_t n = 0; n < 14; n++)
{
// Even pulses are marks; odd pulses are spaces.
// We only check the marks against the tolerances, to save duration.
if ((packet->bitstream[2 + (n * 2)].duration > MIN_MARK_ONE_IN_us) && (packet->bitstream[2 + (n * 2)].duration < MAX_MARK_ONE_IN_us))
{
// One
data |= ((uint16_t)1 << n);
}
else if ((packet->bitstream[2 + (n * 2)].duration > MIN_MARK_ZERO_IN_us) && (packet->bitstream[2 + (n * 2)].duration < MAX_MARK_ZERO_IN_us))
{
// Zero--nothing to do.
}
else
{
// This mark is neither a zero or a one; abort;
return NULL;
}
}
DecodedPacket_T *Tag_Rx_Buffer = Get_Tag_Packet_Buffer();
Tag_Rx_Buffer->Tag.type = DECODED_PACKET_TYPE_TAG_RECEIVED;
Tag_Rx_Buffer->Tag.protocol = MILES_TAG_II_PROTOCOL;
Tag_Rx_Buffer->Tag.player_ID = 0x007F & (data >> 6);
Tag_Rx_Buffer->Tag.team_ID = 0x0003 & (data >> 4);
Tag_Rx_Buffer->Tag.damage = Miles_Tag_II_Damage[0x000F & data];
Tag_Rx_Buffer->Tag.color = Miles_Tag_II_Team_Colors[Tag_Rx_Buffer->Tag.team_ID];
return Tag_Rx_Buffer;
}
}
color_t MILES_TAG_II_GetTeamColor(uint8_t team_ID)
{
color_t result = COLOR_RED;
if (team_ID == MILES_TAG_RED_TEAM)
{
// Nothing to do.
}
else if (team_ID == MILES_TAG_BLUE_TEAM)
{
result = COLOR_BLUE;
}
else if (team_ID == MILES_TAG_YELLOW_TEAM)
{
result = COLOR_YELLOW;
}
else if (team_ID == MILES_TAG_GREEN_TEAM)
{
result = COLOR_GREEN;
}
return result;
}

33
Protocols/Miles_Tag_II.h Executable file
View file

@ -0,0 +1,33 @@
/*
* This program source code file is part of SystemK, a library in the KTag project.
*
* 🛡 <https://ktag.clubk.club> 🃞
*
* Copyright © 2016-2025 Joseph P. Kearney and the KTag developers.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* There should be a copy of the GNU Affero General Public License in the LICENSE
* file in the root of this repository. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef MILES_TAG_H
#define MILES_TAG_H
#include <stdbool.h>
#include <stdint.h>
TimedPulseTrain_T *MILES_TAG_II_EncodePacket(TagPacket_T *packet);
DecodedPacket_T *MILES_TAG_II_MaybeDecodePacket(TimedPulseTrain_T *packet);
color_t MILES_TAG_II_GetTeamColor(uint8_t team_ID);
#endif // MILES_TAG_H

171
Protocols/NEC.c Executable file
View file

@ -0,0 +1,171 @@
/*
* This program source code file is part of SystemK, a library in the KTag project.
*
* 🛡 <https://ktag.clubk.club> 🃞
*
* Copyright © 2016-2025 Joseph P. Kearney and the KTag developers.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* There should be a copy of the GNU Affero General Public License in the LICENSE
* file in the root of this repository. If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdbool.h>
#include <stdio.h>
#include "SystemK.h"
#define NEC_BLUE_TEAM 0x01
#define NEC_RED_TEAM 0x02
#define NEC_HEADER_MARK_DURATION_IN_us 9000
#define NEC_HEADER_SPACE_DURATION_IN_us 4500
#define NEC_ZERO_MARK_DURATION_IN_us 562
#define NEC_ZERO_SPACE_DURATION_IN_us 562
#define NEC_ONE_MARK_DURATION_IN_us 562
#define NEC_ONE_SPACE_DURATION_IN_us 1688
#define NEC_TOLERANCE_IN_us 200
#define MIN_ZERO_MARK_IN_us (NEC_ZERO_MARK_DURATION_IN_us - NEC_TOLERANCE_IN_us)
#define MAX_ZERO_MARK_IN_us (NEC_ZERO_MARK_DURATION_IN_us + NEC_TOLERANCE_IN_us)
#define MIN_ZERO_SPACE_IN_us (NEC_ZERO_SPACE_DURATION_IN_us - NEC_TOLERANCE_IN_us)
#define MAX_ZERO_SPACE_IN_us (NEC_ZERO_SPACE_DURATION_IN_us + NEC_TOLERANCE_IN_us)
#define MIN_ONE_MARK_IN_us (NEC_ONE_MARK_DURATION_IN_us - NEC_TOLERANCE_IN_us)
#define MAX_ONE_MARK_IN_us (NEC_ONE_MARK_DURATION_IN_us + NEC_TOLERANCE_IN_us)
#define MIN_ONE_SPACE_IN_us (NEC_ONE_SPACE_DURATION_IN_us - NEC_TOLERANCE_IN_us)
#define MAX_ONE_SPACE_IN_us (NEC_ONE_SPACE_DURATION_IN_us + NEC_TOLERANCE_IN_us)
static const TimedBit_T NEC_HEADER_MARK = {.symbol = MARK, .duration = NEC_HEADER_MARK_DURATION_IN_us};
static const TimedBit_T NEC_HEADER_SPACE = {.symbol = MARK, .duration = NEC_HEADER_SPACE_DURATION_IN_us};
static const TimedBit_T NEC_ZERO_SPACE = {.symbol = SPACE, .duration = NEC_ZERO_SPACE_DURATION_IN_us};
static const TimedBit_T NEC_ONE_SPACE = {.symbol = SPACE, .duration = NEC_ONE_SPACE_DURATION_IN_us};
static const TimedBit_T NEC_ZERO_MARK = {.symbol = MARK, .duration = NEC_ZERO_MARK_DURATION_IN_us};
static const TimedBit_T NEC_ONE_MARK = {.symbol = MARK, .duration = NEC_ONE_MARK_DURATION_IN_us};
static const TimedBit_T NEC_END = {.symbol = SPACE, .duration = LAST_PULSE};
static TimedPulseTrain_T Command_Send_Buffer;
static DecodedPacket_T NEC_Command_Buffers[3] =
{
{.Command.type = DECODED_PACKET_TYPE_BUFFER_FREE},
{.Command.type = DECODED_PACKET_TYPE_BUFFER_FREE},
{.Command.type = DECODED_PACKET_TYPE_BUFFER_FREE}};
static inline DecodedPacket_T *Get_Command_Packet_Buffer(void)
{
if (NEC_Command_Buffers[0].Command.type == DECODED_PACKET_TYPE_BUFFER_FREE)
{
return &NEC_Command_Buffers[0];
}
else if (NEC_Command_Buffers[1].Command.type == DECODED_PACKET_TYPE_BUFFER_FREE)
{
return &NEC_Command_Buffers[1];
}
else
{
// Just use it.
return &NEC_Command_Buffers[2];
}
}
static inline void PackPulseTrain(TimedPulseTrain_T *pulsetrain, uint32_t data)
{
pulsetrain->bitstream[0] = NEC_HEADER_MARK;
pulsetrain->bitstream[1] = NEC_HEADER_SPACE;
for (uint8_t n = 0; n < 16; n++)
{
if ((data & 0x80000000) == 0x80000000)
{
pulsetrain->bitstream[1 + (n * 2)] = NEC_ONE_SPACE;
}
else
{
pulsetrain->bitstream[1 + (n * 2)] = NEC_ZERO_SPACE;
}
data = data << 1;
if ((data & 0x80000000) == 0x80000000)
{
pulsetrain->bitstream[2 + (n * 2)] = NEC_ONE_MARK;
}
else
{
pulsetrain->bitstream[2 + (n * 2)] = NEC_ZERO_MARK;
}
data = data << 1;
}
pulsetrain->bitstream[(2 * 20) + 1] = NEC_END;
}
TimedPulseTrain_T *NEC_EncodePacket(CommandPacket_T *packet)
{
uint32_t packed_data = 0x00000000;
PackPulseTrain(&Command_Send_Buffer, packed_data);
return &Command_Send_Buffer;
}
DecodedPacket_T *NEC_MaybeDecodePacket(TimedPulseTrain_T *packet)
{
// Sometimes the Roku remote will repeat keys, so only check for a minimum here.
if (packet->count < 67)
{
// Fail fast!
return NULL;
}
else
{
// Convert pulse durations to bits.
// packet->bitstream[0] is the header mark--ignore it.
// packet->bitstream[1] is the header space--ignore it, too.
uint32_t data = 0;
for (uint8_t n = 0; n < 32; n++)
{
uint8_t mark_index = (n * 2) + 2;
uint8_t space_index = (n * 2) + 3;
if ((packet->bitstream[mark_index].duration > MIN_ONE_MARK_IN_us) && (packet->bitstream[mark_index].duration < MAX_ONE_MARK_IN_us) &&
(packet->bitstream[space_index].duration > MIN_ONE_SPACE_IN_us) && (packet->bitstream[space_index].duration < MAX_ONE_SPACE_IN_us))
{
// One
data |= ((uint32_t)1 << n);
}
else if ((packet->bitstream[mark_index].duration > MIN_ZERO_MARK_IN_us) && (packet->bitstream[mark_index].duration < MAX_ZERO_MARK_IN_us) &&
(packet->bitstream[space_index].duration > MIN_ZERO_SPACE_IN_us) && (packet->bitstream[space_index].duration < MAX_ZERO_SPACE_IN_us))
{
// Zero--nothing to do.
}
else
{
// This mark is neither a zero or a one; abort.
return NULL;
}
}
// PulseDurations[66] is the end-of-message mark. It can also be ignored.
DecodedPacket_T *Command_Rx_Buffer = Get_Command_Packet_Buffer();
Command_Rx_Buffer->Command.type = DECODED_PACKET_TYPE_COMMAND_RECEIVED;
Command_Rx_Buffer->Command.protocol = NEC_PROTOCOL;
Command_Rx_Buffer->Command.data = data;
return Command_Rx_Buffer;
}
}
color_t NEC_GetTeamColor(uint8_t team_ID)
{
(void)team_ID;
return COLOR_BLACK;
}

39
Protocols/NEC.h Executable file
View file

@ -0,0 +1,39 @@
/*
* This program source code file is part of SystemK, a library in the KTag project.
*
* 🛡 <https://ktag.clubk.club> 🃞
*
* Copyright © 2016-2025 Joseph P. Kearney and the KTag developers.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* There should be a copy of the GNU Affero General Public License in the LICENSE
* file in the root of this repository. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef NEC_H
#define NEC_H
/** \file
* \brief This is the NEC Infrared Transmission Protocol.
*
* See https://techdocs.altium.com/display/FPGA/NEC+Infrared+Transmission+Protocol for more information.
*/
#include <stdbool.h>
#include <stdint.h>
TimedPulseTrain_T *NEC_EncodePacket(CommandPacket_T *packet);
DecodedPacket_T *NEC_MaybeDecodePacket(TimedPulseTrain_T *packet);
color_t NEC_GetTeamColor(uint8_t team_ID);
#endif // NEC_H

218
Protocols/Nerf_Laser_Ops_Pro.c Executable file
View file

@ -0,0 +1,218 @@
/*
* This program source code file is part of SystemK, a library in the KTag project.
*
* 🛡 <https://ktag.clubk.club> 🃞
*
* Copyright © 2016-2025 Joseph P. Kearney and the KTag developers.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* There should be a copy of the GNU Affero General Public License in the LICENSE
* file in the root of this repository. If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdbool.h>
#include <stdio.h>
#include "SystemK.h"
#define NERF_RED_TEAM 0b00
#define NERF_BLUE_TEAM 0b01
#define NERF_PURPLE_TEAM 0b10
#define RED_TEAM_PACKET_DATA 0x0010
#define BLUE_TEAM_PACKET_DATA 0x0210
#define PURPLE_TEAM_PACKET_DATA 0x0110
#define NERF_LASER_OPS_PRO_HEADER_MARK_DURATION_IN_us 3000
#define NERF_LASER_OPS_PRO_HEADER_SPACE_DURATION_IN_us 6000
#define NERF_LASER_OPS_PRO_SPACE_DURATION_IN_us 2000
#define NERF_LASER_OPS_PRO_MARK_ZERO_DURATION_IN_us 800
#define NERF_LASER_OPS_PRO_MARK_ONE_DURATION_IN_us 2000
#define NERF_LASER_OPS_PRO_TOLERANCE_IN_us 400
#define MIN_SPACE_IN_us (NERF_LASER_OPS_PRO_SPACE_DURATION_IN_us - NERF_LASER_OPS_PRO_TOLERANCE_IN_us)
#define MAX_SPACE_IN_us (NERF_LASER_OPS_PRO_SPACE_DURATION_IN_us + NERF_LASER_OPS_PRO_TOLERANCE_IN_us)
#define MIN_MARK_ZERO_IN_us (NERF_LASER_OPS_PRO_MARK_ZERO_DURATION_IN_us - NERF_LASER_OPS_PRO_TOLERANCE_IN_us)
#define MAX_MARK_ZERO_IN_us (NERF_LASER_OPS_PRO_MARK_ZERO_DURATION_IN_us + NERF_LASER_OPS_PRO_TOLERANCE_IN_us)
#define MIN_MARK_ONE_IN_us (NERF_LASER_OPS_PRO_MARK_ONE_DURATION_IN_us - NERF_LASER_OPS_PRO_TOLERANCE_IN_us)
#define MAX_MARK_ONE_IN_us (NERF_LASER_OPS_PRO_MARK_ONE_DURATION_IN_us + NERF_LASER_OPS_PRO_TOLERANCE_IN_us)
static const TimedBit_T NERF_LASER_OPS_PRO_HEADER_MARK = {.symbol = MARK, .duration = NERF_LASER_OPS_PRO_HEADER_MARK_DURATION_IN_us};
static const TimedBit_T NERF_LASER_OPS_PRO_HEADER_SPACE = {.symbol = SPACE, .duration = NERF_LASER_OPS_PRO_HEADER_SPACE_DURATION_IN_us};
static const TimedBit_T NERF_LASER_OPS_PRO_SPACE = {.symbol = SPACE, .duration = NERF_LASER_OPS_PRO_SPACE_DURATION_IN_us};
static const TimedBit_T NERF_LASER_OPS_PRO_ONE = {.symbol = MARK, .duration = NERF_LASER_OPS_PRO_MARK_ONE_DURATION_IN_us};
static const TimedBit_T NERF_LASER_OPS_PRO_ZERO = {.symbol = MARK, .duration = NERF_LASER_OPS_PRO_MARK_ZERO_DURATION_IN_us};
static const TimedBit_T NERF_LASER_OPS_PRO_END = {.symbol = SPACE, .duration = LAST_PULSE};
static TimedPulseTrain_T Tag_Send_Buffer;
static DecodedPacket_T Nerf_Laser_Ops_Pro_Tag_Received_Buffers[3] =
{
{.Tag.type = DECODED_PACKET_TYPE_BUFFER_FREE},
{.Tag.type = DECODED_PACKET_TYPE_BUFFER_FREE},
{.Tag.type = DECODED_PACKET_TYPE_BUFFER_FREE}};
static inline DecodedPacket_T *Get_Tag_Packet_Buffer(void)
{
if (Nerf_Laser_Ops_Pro_Tag_Received_Buffers[0].Tag.type == DECODED_PACKET_TYPE_BUFFER_FREE)
{
return &Nerf_Laser_Ops_Pro_Tag_Received_Buffers[0];
}
else if (Nerf_Laser_Ops_Pro_Tag_Received_Buffers[1].Tag.type == DECODED_PACKET_TYPE_BUFFER_FREE)
{
return &Nerf_Laser_Ops_Pro_Tag_Received_Buffers[1];
}
else
{
// Just use it.
return &Nerf_Laser_Ops_Pro_Tag_Received_Buffers[2];
}
}
static inline void PackPulseTrain(TimedPulseTrain_T *pulsetrain, uint16_t data)
{
pulsetrain->bitstream[0] = NERF_LASER_OPS_PRO_HEADER_MARK;
pulsetrain->bitstream[1] = NERF_LASER_OPS_PRO_HEADER_SPACE;
pulsetrain->bitstream[2] = NERF_LASER_OPS_PRO_HEADER_MARK;
pulsetrain->bitstream[3] = NERF_LASER_OPS_PRO_SPACE;
for (uint8_t n = 0; n < 16; n++)
{
if ((data & 0x01) == 0x01)
{
pulsetrain->bitstream[4 + (n * 2)] = NERF_LASER_OPS_PRO_ONE;
}
else
{
pulsetrain->bitstream[4 + (n * 2)] = NERF_LASER_OPS_PRO_ZERO;
}
pulsetrain->bitstream[4 + (n * 2) + 1] = NERF_LASER_OPS_PRO_SPACE;
data = data >> 1;
}
pulsetrain->bitstream[35] = NERF_LASER_OPS_PRO_END;
}
TimedPulseTrain_T *NERF_LASER_OPS_PRO_EncodePacket(TagPacket_T *packet)
{
uint16_t packed_data = 0;
if (packet->team_ID == NERF_RED_TEAM)
{
packed_data = RED_TEAM_PACKET_DATA;
}
else if (packet->team_ID == NERF_BLUE_TEAM)
{
packed_data = BLUE_TEAM_PACKET_DATA;
}
else // NERF_PURPLE_TEAM
{
packed_data = PURPLE_TEAM_PACKET_DATA;
}
PackPulseTrain(&Tag_Send_Buffer, packed_data);
return &Tag_Send_Buffer;
}
DecodedPacket_T *NERF_LASER_OPS_PRO_MaybeDecodePacket(TimedPulseTrain_T *packet)
{
// Some implementations (ESP32) do not report odd numbers of pulses.
if ((packet->count != 35) && (packet->count != 36))
{
// Fail fast!
return NULL;
}
else
{
// Convert pulse durations to bits.
// packet->bitstream[0] is the header mark--ignore it.
// packet->bitstream[1] is the header space--ignore it also.
// packet->bitstream[2] is the second header mark--ignore it as well.
// packet->bitstream[3] is the second header space--also ignored.
uint16_t data = 0;
for (uint8_t n = 0; n < 16; n++)
{
// Even pulses are marks; odd pulses are spaces.
if ((packet->bitstream[4 + (n * 2)].duration > MIN_MARK_ONE_IN_us) && (packet->bitstream[4 + (n * 2)].duration < MAX_MARK_ONE_IN_us))
{
// One
data |= ((uint16_t)1 << n);
}
else if ((packet->bitstream[4 + (n * 2)].duration > MIN_MARK_ZERO_IN_us) && (packet->bitstream[4 + (n * 2)].duration < MAX_MARK_ZERO_IN_us))
{
// Zero--nothing to do.
}
else
{
// This mark is neither a zero or a one; abort.
return NULL;
}
}
if (data == RED_TEAM_PACKET_DATA)
{
DecodedPacket_T *Tag_Rx_Buffer = Get_Tag_Packet_Buffer();
Tag_Rx_Buffer->Tag.type = DECODED_PACKET_TYPE_TAG_RECEIVED;
Tag_Rx_Buffer->Tag.protocol = NERF_LASER_OPS_PRO_PROTOCOL;
Tag_Rx_Buffer->Tag.player_ID = 0;
Tag_Rx_Buffer->Tag.team_ID = NERF_RED_TEAM;
Tag_Rx_Buffer->Tag.damage = 10;
Tag_Rx_Buffer->Tag.color = COLOR_RED;
return Tag_Rx_Buffer;
}
else if (data == BLUE_TEAM_PACKET_DATA)
{
DecodedPacket_T *Tag_Rx_Buffer = Get_Tag_Packet_Buffer();
Tag_Rx_Buffer->Tag.type = DECODED_PACKET_TYPE_TAG_RECEIVED;
Tag_Rx_Buffer->Tag.protocol = NERF_LASER_OPS_PRO_PROTOCOL;
Tag_Rx_Buffer->Tag.player_ID = 0;
Tag_Rx_Buffer->Tag.team_ID = NERF_BLUE_TEAM;
Tag_Rx_Buffer->Tag.damage = 10;
Tag_Rx_Buffer->Tag.color = COLOR_BLUE;
return Tag_Rx_Buffer;
}
else if (data == PURPLE_TEAM_PACKET_DATA)
{
DecodedPacket_T *Tag_Rx_Buffer = Get_Tag_Packet_Buffer();
Tag_Rx_Buffer->Tag.type = DECODED_PACKET_TYPE_TAG_RECEIVED;
Tag_Rx_Buffer->Tag.protocol = NERF_LASER_OPS_PRO_PROTOCOL;
Tag_Rx_Buffer->Tag.player_ID = 0;
Tag_Rx_Buffer->Tag.team_ID = NERF_PURPLE_TEAM;
Tag_Rx_Buffer->Tag.damage = 10;
Tag_Rx_Buffer->Tag.color = COLOR_PURPLE;
return Tag_Rx_Buffer;
}
return NULL;
}
}
color_t NERF_LASER_OPS_PRO_GetTeamColor(uint8_t team_ID)
{
color_t result = COLOR_PURPLE;
if (team_ID == NERF_RED_TEAM)
{
result = COLOR_RED;
}
else if (team_ID == NERF_BLUE_TEAM)
{
result = COLOR_BLUE;
}
else
{
// Nothing to do.
}
return result;
}

44
Protocols/Nerf_Laser_Ops_Pro.h Executable file
View file

@ -0,0 +1,44 @@
/*
* This program source code file is part of SystemK, a library in the KTag project.
*
* 🛡 <https://ktag.clubk.club> 🃞
*
* Copyright © 2016-2025 Joseph P. Kearney and the KTag developers.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* There should be a copy of the GNU Affero General Public License in the LICENSE
* file in the root of this repository. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef NERF_LASER_OPS_PRO_H
#define NERF_LASER_OPS_PRO_H
/** \file
* \brief This is the Nerf Laser Ops Pro protocol.
*
* \note These devices use 38kHz carrier for On-Off Keying.
*
* https://nerf.hasbro.com/en-us/product/nerf-laser-ops-pro-alphapoint-2-pack:CA310BBE-E75A-4354-8D69-AC2B28C4E007
*
* https://forum.arduino.cc/index.php?topic=582222.0
*
*/
#include <stdbool.h>
#include <stdint.h>
TimedPulseTrain_T *NERF_LASER_OPS_PRO_EncodePacket(TagPacket_T *packet);
DecodedPacket_T *NERF_LASER_OPS_PRO_MaybeDecodePacket(TimedPulseTrain_T *packet);
color_t NERF_LASER_OPS_PRO_GetTeamColor(uint8_t team_ID);
#endif // NERF_LASER_OPS_PRO_H

295
Protocols/Nerf_Laser_Strike.c Executable file
View file

@ -0,0 +1,295 @@
/*
* This program source code file is part of SystemK, a library in the KTag project.
*
* 🛡 <https://ktag.clubk.club> 🃞
*
* Copyright © 2025 Joseph P. Kearney and the KTag developers.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* There should be a copy of the GNU Affero General Public License in the LICENSE
* file in the root of this repository. If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdbool.h>
#include <stdio.h>
#include "SystemK.h"
static const char *KLOG_TAG = "Nerf Laser Strike";
#define NERF_LASER_STRIKE_SOLO_TEAM 0b00
#define NERF_LASER_STRIKE_ONE_TEAM 0b01
#define NERF_LASER_STRIKE_TWO_TEAM 0b10
#define NERF_LASER_STRIKE_BLASTER 0b000
#define NERF_LASER_STRIKE_ROCKET 0b001
#define NERF_LASER_STRIKE_PLASMA 0b010
#define NERF_LASER_STRIKE_BURST 0b011
#define NERF_LASER_STRIKE_LASER 0b100
static const uint16_t Packet_Data[5][3] = {
// Solo, Team 1, Team 2
{0x4D33, 0x34CC, 0x2B55}, // Blaster
{0x52B2, 0x354B, 0x2CD4}, // Rocket
{0x532D, 0x4ACA, 0x2D53}, // Plasma
{0x54AC, 0x4B35, 0x32D2}, // Burst
{0x552B, 0x4CB4, 0x334D} // Laser
};
static inline uint16_t Get_Packet_Data(uint8_t team, uint8_t weapon)
{
uint16_t result = 0;
if ((team <= NERF_LASER_STRIKE_TWO_TEAM) && (weapon <= NERF_LASER_STRIKE_LASER))
{
result = Packet_Data[weapon][team];
}
else
{
KLOG_ERROR(KLOG_TAG, "Requested team (%u) or weapon (%u) out of range!", team, weapon);
}
return result;
}
//! Given decoded packet data, this function searches the table to determine if the packet is a valid one.
/*!
* If it is valid, it will set team and weapon to their valid values, and return true.
* If it does not find the packet data in the table, it will return false.
*/
static inline bool Is_Valid_Packet_Data(const uint16_t data, uint8_t *team, uint8_t *weapon)
{
bool valid = false;
for (uint_fast8_t weapon_index = 0; (weapon_index < 5) && (valid == false); weapon_index++)
{
for (uint_fast8_t team_index = 0; (team_index < 3) && (valid == false); team_index++)
{
if (Packet_Data[weapon_index][team_index] == data)
{
*team = team_index;
*weapon = weapon_index;
valid = true;
}
}
}
return valid;
}
#define NERF_LASER_STRIKE_HEADER_MARK_DURATION_IN_us 6000
#define NERF_LASER_STRIKE_ZERO_DURATION_IN_us 500
#define NERF_LASER_STRIKE_ONE_DURATION_IN_us 1500
#define NERF_LASER_STRIKE_TOLERANCE_IN_us 450
#define MIN_ZERO_IN_us (NERF_LASER_STRIKE_ZERO_DURATION_IN_us - NERF_LASER_STRIKE_TOLERANCE_IN_us)
#define MAX_ZERO_IN_us (NERF_LASER_STRIKE_ZERO_DURATION_IN_us + NERF_LASER_STRIKE_TOLERANCE_IN_us)
#define MIN_ONE_IN_us (NERF_LASER_STRIKE_ONE_DURATION_IN_us - NERF_LASER_STRIKE_TOLERANCE_IN_us)
#define MAX_ONE_IN_us (NERF_LASER_STRIKE_ONE_DURATION_IN_us + NERF_LASER_STRIKE_TOLERANCE_IN_us)
static const TimedBit_T NERF_LASER_STRIKE_HEADER_MARK = {.symbol = MARK, .duration = NERF_LASER_STRIKE_HEADER_MARK_DURATION_IN_us};
static const TimedBit_T NERF_LASER_STRIKE_ZERO_SPACE = {.symbol = SPACE, .duration = NERF_LASER_STRIKE_ZERO_DURATION_IN_us};
static const TimedBit_T NERF_LASER_STRIKE_ONE_SPACE = {.symbol = SPACE, .duration = NERF_LASER_STRIKE_ONE_DURATION_IN_us};
static const TimedBit_T NERF_LASER_STRIKE_ZERO_MARK = {.symbol = MARK, .duration = NERF_LASER_STRIKE_ZERO_DURATION_IN_us};
static const TimedBit_T NERF_LASER_STRIKE_ONE_MARK = {.symbol = MARK, .duration = NERF_LASER_STRIKE_ONE_DURATION_IN_us};
static const TimedBit_T NERF_LASER_STRIKE_END = {.symbol = SPACE, .duration = LAST_PULSE};
static TimedPulseTrain_T Tag_Send_Buffer;
static DecodedPacket_T Nerf_Laser_Strike_Tag_Received_Buffers[3] =
{
{.Tag.type = DECODED_PACKET_TYPE_BUFFER_FREE},
{.Tag.type = DECODED_PACKET_TYPE_BUFFER_FREE},
{.Tag.type = DECODED_PACKET_TYPE_BUFFER_FREE}};
static inline DecodedPacket_T *Get_Tag_Packet_Buffer(void)
{
if (Nerf_Laser_Strike_Tag_Received_Buffers[0].Tag.type == DECODED_PACKET_TYPE_BUFFER_FREE)
{
return &Nerf_Laser_Strike_Tag_Received_Buffers[0];
}
else if (Nerf_Laser_Strike_Tag_Received_Buffers[1].Tag.type == DECODED_PACKET_TYPE_BUFFER_FREE)
{
return &Nerf_Laser_Strike_Tag_Received_Buffers[1];
}
else
{
// Just use it.
return &Nerf_Laser_Strike_Tag_Received_Buffers[2];
}
}
// Packs data into a pulse train, most significant bit first.
static inline void PackPulseTrain(TimedPulseTrain_T *pulsetrain, uint16_t data)
{
pulsetrain->bitstream[0] = NERF_LASER_STRIKE_HEADER_MARK;
for (uint8_t n = 0; n < 8; n++)
{
if ((data & 0x8000) == 0x8000)
{
pulsetrain->bitstream[1 + (n * 2)] = NERF_LASER_STRIKE_ONE_SPACE;
}
else
{
pulsetrain->bitstream[1 + (n * 2)] = NERF_LASER_STRIKE_ZERO_SPACE;
}
data = data << 1;
if ((data & 0x8000) == 0x8000)
{
pulsetrain->bitstream[2 + (n * 2)] = NERF_LASER_STRIKE_ONE_MARK;
}
else
{
pulsetrain->bitstream[2 + (n * 2)] = NERF_LASER_STRIKE_ZERO_MARK;
}
data = data << 1;
}
pulsetrain->bitstream[17] = NERF_LASER_STRIKE_END;
pulsetrain->count = 17;
}
TimedPulseTrain_T *NERF_LASER_STRIKE_EncodePacket(TagPacket_T *packet)
{
uint16_t packed_data = 0;
if (packet->team_ID == TEAM_RED)
{
packed_data = Get_Packet_Data(NERF_LASER_STRIKE_ONE_TEAM, NERF_LASER_STRIKE_BLASTER);
}
else if (packet->team_ID == TEAM_BLUE)
{
packed_data = Get_Packet_Data(NERF_LASER_STRIKE_TWO_TEAM, NERF_LASER_STRIKE_BLASTER);
}
else // NERF_LASER_STRIKE_SOLO_TEAM
{
packed_data = Get_Packet_Data(NERF_LASER_STRIKE_SOLO_TEAM, NERF_LASER_STRIKE_BLASTER);
}
PackPulseTrain(&Tag_Send_Buffer, packed_data);
return &Tag_Send_Buffer;
}
DecodedPacket_T *NERF_LASER_STRIKE_MaybeDecodePacket(TimedPulseTrain_T *packet)
{
// Some implementations (ESP32) do not report odd numbers of pulses.
// The PSoC sees all three copies of the tag at once.
if ((packet->count != 17) &&
(packet->count != 18) &&
(packet->count != 53))
{
// Fail fast!
return NULL;
}
else
{
// Convert pulse durations to bits, assuming most significant bit is sent first.
// packet->bitstream[0] is the header mark--ignore it.
uint16_t data = 0;
for (uint16_t n = 16; n > 0; n--)
{
if ((packet->bitstream[n].duration > MIN_ONE_IN_us) && (packet->bitstream[n].duration < MAX_ONE_IN_us))
{
// One
data |= ((uint16_t)1 << (16 - n));
}
else if ((packet->bitstream[n].duration > MIN_ZERO_IN_us) && (packet->bitstream[n].duration < MAX_ZERO_IN_us))
{
// Zero--nothing to do.
}
else
{
// This pulse is neither a zero or a one; abort.
return NULL;
}
}
uint8_t team = 0;
uint8_t weapon = 0;
if (Is_Valid_Packet_Data(data, &team, &weapon) == false)
{
KLOG_DEBUG(KLOG_TAG, "Decoded packet was not recognized: 0x%4X", data);
}
else
{
DecodedPacket_T *Tag_Rx_Buffer = Get_Tag_Packet_Buffer();
if (team == NERF_LASER_STRIKE_ONE_TEAM)
{
Tag_Rx_Buffer->Tag.type = DECODED_PACKET_TYPE_TAG_RECEIVED;
Tag_Rx_Buffer->Tag.protocol = NERF_LASER_STRIKE_PROTOCOL;
Tag_Rx_Buffer->Tag.player_ID = 0;
Tag_Rx_Buffer->Tag.team_ID = TEAM_RED;
Tag_Rx_Buffer->Tag.color = COLOR_RED;
}
else if (team == NERF_LASER_STRIKE_TWO_TEAM)
{
Tag_Rx_Buffer->Tag.type = DECODED_PACKET_TYPE_TAG_RECEIVED;
Tag_Rx_Buffer->Tag.protocol = NERF_LASER_STRIKE_PROTOCOL;
Tag_Rx_Buffer->Tag.player_ID = 0;
Tag_Rx_Buffer->Tag.team_ID = TEAM_BLUE;
Tag_Rx_Buffer->Tag.color = COLOR_BLUE;
}
else // NERF_LASER_STRIKE_SOLO_TEAM
{
Tag_Rx_Buffer->Tag.type = DECODED_PACKET_TYPE_TAG_RECEIVED;
Tag_Rx_Buffer->Tag.protocol = NERF_LASER_STRIKE_PROTOCOL;
Tag_Rx_Buffer->Tag.player_ID = 0;
Tag_Rx_Buffer->Tag.team_ID = TEAM_PURPLE;
Tag_Rx_Buffer->Tag.color = COLOR_PURPLE;
}
// Damages are 10x, normalized for MAX_HEALTH of 100.
if (weapon == NERF_LASER_STRIKE_BLASTER)
{
Tag_Rx_Buffer->Tag.damage = 10;
}
else if (weapon == NERF_LASER_STRIKE_ROCKET)
{
Tag_Rx_Buffer->Tag.damage = 30;
}
else // NERF_LASER_STRIKE_PLASMA, NERF_LASER_STRIKE_BURST, and NERF_LASER_STRIKE_LASER
{
Tag_Rx_Buffer->Tag.damage = 20;
}
return Tag_Rx_Buffer;
}
return NULL;
}
}
color_t NERF_LASER_STRIKE_GetTeamColor(uint8_t team_ID)
{
color_t result = COLOR_PURPLE;
if (team_ID == NERF_LASER_STRIKE_ONE_TEAM)
{
result = COLOR_RED;
}
else if (team_ID == NERF_LASER_STRIKE_TWO_TEAM)
{
result = COLOR_BLUE;
}
else
{
// Nothing to do.
}
return result;
}

40
Protocols/Nerf_Laser_Strike.h Executable file
View file

@ -0,0 +1,40 @@
/*
* This program source code file is part of SystemK, a library in the KTag project.
*
* 🛡 <https://ktag.clubk.club> 🃞
*
* Copyright © 2025 Joseph P. Kearney and the KTag developers.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* There should be a copy of the GNU Affero General Public License in the LICENSE
* file in the root of this repository. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef NERF_LASER_STRIKE_H
#define NERF_LASER_STRIKE_H
/** \file
* \brief This is the Nerf Laser Strike protocol.
*
* \note These devices use 38kHz carrier for On-Off Keying.
*
*/
#include <stdbool.h>
#include <stdint.h>
TimedPulseTrain_T *NERF_LASER_STRIKE_EncodePacket(TagPacket_T *packet);
DecodedPacket_T *NERF_LASER_STRIKE_MaybeDecodePacket(TimedPulseTrain_T *packet);
color_t NERF_LASER_STRIKE_GetTeamColor(uint8_t team_ID);
#endif // NERF_LASER_STRIKE_H

221
Protocols/Nerf_Phoenix_LTX.c Executable file
View file

@ -0,0 +1,221 @@
/*
* This program source code file is part of SystemK, a library in the KTag project.
*
* 🛡 <https://ktag.clubk.club> 🃞
*
* Copyright © 2016-2025 Joseph P. Kearney and the KTag developers.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* There should be a copy of the GNU Affero General Public License in the LICENSE
* file in the root of this repository. If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdbool.h>
#include <stdio.h>
#include "SystemK.h"
#define NERF_RED_TEAM 0b00
#define NERF_BLUE_TEAM 0b01
#define NERF_PURPLE_TEAM 0b10
//! *Team One* is the Red Team.
#define RED_TEAM_PACKET_DATA 0x0010
//! *Team Two* is the Blue Team.
#define BLUE_TEAM_PACKET_DATA 0x0008
//! *Solo* is the Purple Team.
#define PURPLE_TEAM_PACKET_DATA 0x0000
#define NERF_PHOENIX_LTX_HEADER_MARK_DURATION_IN_us 3000
#define NERF_PHOENIX_LTX_HEADER_SPACE_DURATION_IN_us 6000
#define NERF_PHOENIX_LTX_SPACE_DURATION_IN_us 2000
#define NERF_PHOENIX_LTX_MARK_ZERO_DURATION_IN_us 900
#define NERF_PHOENIX_LTX_MARK_ONE_DURATION_IN_us 2000
#define NERF_PHOENIX_LTX_TOLERANCE_IN_us 350
#define MIN_SPACE_IN_us (NERF_PHOENIX_LTX_SPACE_DURATION_IN_us - NERF_PHOENIX_LTX_TOLERANCE_IN_us)
#define MAX_SPACE_IN_us (NERF_PHOENIX_LTX_SPACE_DURATION_IN_us + NERF_PHOENIX_LTX_TOLERANCE_IN_us)
#define MIN_MARK_ZERO_IN_us (NERF_PHOENIX_LTX_MARK_ZERO_DURATION_IN_us - NERF_PHOENIX_LTX_TOLERANCE_IN_us)
#define MAX_MARK_ZERO_IN_us (NERF_PHOENIX_LTX_MARK_ZERO_DURATION_IN_us + NERF_PHOENIX_LTX_TOLERANCE_IN_us)
#define MIN_MARK_ONE_IN_us (NERF_PHOENIX_LTX_MARK_ONE_DURATION_IN_us - NERF_PHOENIX_LTX_TOLERANCE_IN_us)
#define MAX_MARK_ONE_IN_us (NERF_PHOENIX_LTX_MARK_ONE_DURATION_IN_us + NERF_PHOENIX_LTX_TOLERANCE_IN_us)
static const TimedBit_T NERF_PHOENIX_LTX_HEADER_MARK = {.symbol = MARK, .duration = NERF_PHOENIX_LTX_HEADER_MARK_DURATION_IN_us};
static const TimedBit_T NERF_PHOENIX_LTX_HEADER_SPACE = {.symbol = SPACE, .duration = NERF_PHOENIX_LTX_HEADER_SPACE_DURATION_IN_us};
static const TimedBit_T NERF_PHOENIX_LTX_SPACE = {.symbol = SPACE, .duration = NERF_PHOENIX_LTX_SPACE_DURATION_IN_us};
static const TimedBit_T NERF_PHOENIX_LTX_ONE = {.symbol = MARK, .duration = NERF_PHOENIX_LTX_MARK_ONE_DURATION_IN_us};
static const TimedBit_T NERF_PHOENIX_LTX_ZERO = {.symbol = MARK, .duration = NERF_PHOENIX_LTX_MARK_ZERO_DURATION_IN_us};
static const TimedBit_T NERF_PHOENIX_LTX_END = {.symbol = SPACE, .duration = LAST_PULSE};
static TimedPulseTrain_T Tag_Send_Buffer;
static DecodedPacket_T Nerf_Phoenix_LTX_Tag_Received_Buffers[3] =
{
{.Tag.type = DECODED_PACKET_TYPE_BUFFER_FREE},
{.Tag.type = DECODED_PACKET_TYPE_BUFFER_FREE},
{.Tag.type = DECODED_PACKET_TYPE_BUFFER_FREE}};
static inline DecodedPacket_T *Get_Tag_Packet_Buffer(void)
{
if (Nerf_Phoenix_LTX_Tag_Received_Buffers[0].Tag.type == DECODED_PACKET_TYPE_BUFFER_FREE)
{
return &Nerf_Phoenix_LTX_Tag_Received_Buffers[0];
}
else if (Nerf_Phoenix_LTX_Tag_Received_Buffers[1].Tag.type == DECODED_PACKET_TYPE_BUFFER_FREE)
{
return &Nerf_Phoenix_LTX_Tag_Received_Buffers[1];
}
else
{
// Just use it.
return &Nerf_Phoenix_LTX_Tag_Received_Buffers[2];
}
}
static inline void PackPulseTrain(TimedPulseTrain_T *pulsetrain, uint16_t data)
{
pulsetrain->bitstream[0] = NERF_PHOENIX_LTX_HEADER_MARK;
pulsetrain->bitstream[1] = NERF_PHOENIX_LTX_HEADER_SPACE;
pulsetrain->bitstream[2] = NERF_PHOENIX_LTX_HEADER_MARK;
pulsetrain->bitstream[3] = NERF_PHOENIX_LTX_SPACE;
for (uint8_t n = 0; n < 7; n++)
{
if ((data & 0x01) == 0x01)
{
pulsetrain->bitstream[4 + (n * 2)] = NERF_PHOENIX_LTX_ONE;
}
else
{
pulsetrain->bitstream[4 + (n * 2)] = NERF_PHOENIX_LTX_ZERO;
}
pulsetrain->bitstream[4 + (n * 2) + 1] = NERF_PHOENIX_LTX_SPACE;
data = data >> 1;
}
pulsetrain->bitstream[17] = NERF_PHOENIX_LTX_END;
}
TimedPulseTrain_T *NERF_PHOENIX_LTX_EncodePacket(TagPacket_T *packet)
{
uint16_t packed_data = 0;
if (packet->team_ID == NERF_RED_TEAM)
{
packed_data = RED_TEAM_PACKET_DATA;
}
else if (packet->team_ID == NERF_BLUE_TEAM)
{
packed_data = BLUE_TEAM_PACKET_DATA;
}
else // NERF_PURPLE_TEAM
{
packed_data = PURPLE_TEAM_PACKET_DATA;
}
PackPulseTrain(&Tag_Send_Buffer, packed_data);
return &Tag_Send_Buffer;
}
DecodedPacket_T *NERF_PHOENIX_LTX_MaybeDecodePacket(TimedPulseTrain_T *packet)
{
// Some implementations (ESP32) do not report odd numbers of pulses.
if ((packet->count != 17) && (packet->count != 18))
{
// Fail fast!
return NULL;
}
else
{
// Convert pulse durations to bits.
// packet->bitstream[0] is the header mark--ignore it.
// packet->bitstream[1] is the header space--ignore it also.
// packet->bitstream[2] is the second header mark--ignore it as well.
// packet->bitstream[3] is the second header space--also ignored..
uint16_t data = 0;
for (uint8_t n = 0; n < 7; n++)
{
// Even pulses are marks; odd pulses are spaces.
if ((packet->bitstream[4 + (n * 2)].duration > MIN_MARK_ONE_IN_us) && (packet->bitstream[4 + (n * 2)].duration < MAX_MARK_ONE_IN_us))
{
// One
data |= ((uint16_t)1 << n);
}
else if ((packet->bitstream[4 + (n * 2)].duration > MIN_MARK_ZERO_IN_us) && (packet->bitstream[4 + (n * 2)].duration < MAX_MARK_ZERO_IN_us))
{
// Zero--nothing to do.
}
else
{
// This mark is neither a zero or a one; abort.
return NULL;
}
}
if (data == RED_TEAM_PACKET_DATA)
{
DecodedPacket_T *Tag_Rx_Buffer = Get_Tag_Packet_Buffer();
Tag_Rx_Buffer->Tag.type = DECODED_PACKET_TYPE_TAG_RECEIVED;
Tag_Rx_Buffer->Tag.protocol = NERF_PHOENIX_LTX_PROTOCOL;
Tag_Rx_Buffer->Tag.player_ID = 0;
Tag_Rx_Buffer->Tag.team_ID = NERF_RED_TEAM;
Tag_Rx_Buffer->Tag.damage = 10;
Tag_Rx_Buffer->Tag.color = COLOR_RED;
return Tag_Rx_Buffer;
}
else if (data == BLUE_TEAM_PACKET_DATA)
{
DecodedPacket_T *Tag_Rx_Buffer = Get_Tag_Packet_Buffer();
Tag_Rx_Buffer->Tag.type = DECODED_PACKET_TYPE_TAG_RECEIVED;
Tag_Rx_Buffer->Tag.protocol = NERF_PHOENIX_LTX_PROTOCOL;
Tag_Rx_Buffer->Tag.player_ID = 0;
Tag_Rx_Buffer->Tag.team_ID = NERF_BLUE_TEAM;
Tag_Rx_Buffer->Tag.damage = 10;
Tag_Rx_Buffer->Tag.color = COLOR_BLUE;
return Tag_Rx_Buffer;
}
else if (data == PURPLE_TEAM_PACKET_DATA)
{
DecodedPacket_T *Tag_Rx_Buffer = Get_Tag_Packet_Buffer();
Tag_Rx_Buffer->Tag.type = DECODED_PACKET_TYPE_TAG_RECEIVED;
Tag_Rx_Buffer->Tag.protocol = NERF_PHOENIX_LTX_PROTOCOL;
Tag_Rx_Buffer->Tag.player_ID = 0;
Tag_Rx_Buffer->Tag.team_ID = NERF_PURPLE_TEAM;
Tag_Rx_Buffer->Tag.damage = 10;
Tag_Rx_Buffer->Tag.color = COLOR_PURPLE;
return Tag_Rx_Buffer;
}
return NULL;
}
}
color_t NERF_PHOENIX_LTX_GetTeamColor(uint8_t team_ID)
{
color_t result = COLOR_PURPLE;
if (team_ID == NERF_RED_TEAM)
{
result = COLOR_RED;
}
else if (team_ID == NERF_BLUE_TEAM)
{
result = COLOR_BLUE;
}
else
{
// Nothing to do.
}
return result;
}

42
Protocols/Nerf_Phoenix_LTX.h Executable file
View file

@ -0,0 +1,42 @@
/*
* This program source code file is part of SystemK, a library in the KTag project.
*
* 🛡 <https://ktag.clubk.club> 🃞
*
* Copyright © 2016-2025 Joseph P. Kearney and the KTag developers.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* There should be a copy of the GNU Affero General Public License in the LICENSE
* file in the root of this repository. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef NERF_PHOENIX_LTX_H
#define NERF_PHOENIX_LTX_H
/** \file
* \brief This is the Nerf Phoenix LTX protocol.
*
* \note These devices use 38kHz carrier for On-Off Keying.
*
* https://nerf.hasbro.com/en-us/product/nerf-lazer-tag-phoenix-ltx-tagger-2-pack:F0C4410E-19B9-F369-D914-B940ADA55500
*
*/
#include <stdbool.h>
#include <stdint.h>
TimedPulseTrain_T *NERF_PHOENIX_LTX_EncodePacket(TagPacket_T *packet);
DecodedPacket_T *NERF_PHOENIX_LTX_MaybeDecodePacket(TimedPulseTrain_T *packet);
color_t NERF_PHOENIX_LTX_GetTeamColor(uint8_t team_ID);
#endif // NERF_PHOENIX_LTX_H

365
Protocols/Protocols.c Executable file
View file

@ -0,0 +1,365 @@
/*
* This program source code file is part of SystemK, a library in the KTag project.
*
* 🛡 <https://ktag.clubk.club> 🃞
*
* Copyright © 2016-2025 Joseph P. Kearney and the KTag developers.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* There should be a copy of the GNU Affero General Public License in the LICENSE
* file in the root of this repository. If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdbool.h>
#include <stdio.h>
#include "SystemK.h"
// #define DEBUG_PACKET_ENCODE
#define DEBUG_PACKET_DECODE
static const char *KLOG_TAG = "Protocols";
static const char *PROTOCOL_NAME[] = {
FOREACH_PROTOCOL(GENERATE_STRING)};
static inline const char *ProtocolNameAsString(Protocol_T p)
{
if (p < LAST_PROTOCOL)
{
return PROTOCOL_NAME[p];
}
return "Invalid Protocol!";
}
static inline const char *DecodedPacketTypeAsString(DecodedPacketType_T p)
{
if (p == DECODED_PACKET_TYPE_IGNORED)
{
return "Ignored";
}
if (p == DECODED_PACKET_TYPE_TAG_RECEIVED)
{
return "Tag Received";
}
if (p == DECODED_PACKET_TYPE_COMMAND_RECEIVED)
{
return "Command Received";
}
return "Unexpected Packet Type";
}
static inline void PrintPulseTrainToConsole(TimedPulseTrain_T *train)
{
for (uint_fast16_t i = 0; i < train->count; i += 2)
{
KLOG_DEBUG(KLOG_TAG, "%2d: (%d, %4d) (%d, %4d)", i + 1, train->bitstream[i].symbol, train->bitstream[i].duration, train->bitstream[i + 1].symbol, train->bitstream[i + 1].duration);
vTaskDelay(pdMS_TO_TICKS(10));
}
}
ModulationFrequency_T PROTOCOLS_GetModulationFrequency(Protocol_T protocol)
{
ModulationFrequency_T result = FREQUENCY_38kHz;
switch (protocol)
{
default:
case UNKNOWN_PROTOCOL:
break;
case TEST_PROTOCOL:
result = FREQUENCY_38kHz;
break;
case DUBUQUE_PROTOCOL:
result = FREQUENCY_38kHz;
break;
case MILES_TAG_II_PROTOCOL:
result = FREQUENCY_56kHz;
break;
case NERF_LASER_OPS_PRO_PROTOCOL:
result = FREQUENCY_38kHz;
break;
case NERF_PHOENIX_LTX_PROTOCOL:
result = FREQUENCY_38kHz;
break;
case DYNASTY_PROTOCOL:
result = FREQUENCY_38kHz;
break;
case SQUAD_HERO_PROTOCOL:
result = FREQUENCY_38kHz;
break;
case NEC_PROTOCOL:
result = FREQUENCY_38kHz;
break;
case LASER_X_PROTOCOL:
result = FREQUENCY_38kHz;
break;
}
return result;
}
color_t PROTOCOLS_GetColor(Protocol_T protocol, uint8_t team_ID, uint8_t player_ID)
{
color_t result = COLOR_AQUA;
switch (protocol)
{
default:
case UNKNOWN_PROTOCOL:
break;
case TEST_PROTOCOL:
result = COLOR_WHITE;
break;
case DUBUQUE_PROTOCOL:
result = DBQ_GetTeamColor(team_ID);
break;
case MILES_TAG_II_PROTOCOL:
result = MILES_TAG_II_GetTeamColor(team_ID);
break;
case NERF_LASER_OPS_PRO_PROTOCOL:
result = NERF_LASER_OPS_PRO_GetTeamColor(team_ID);
break;
case NERF_LASER_STRIKE_PROTOCOL:
result = NERF_LASER_STRIKE_GetTeamColor(team_ID);
break;
case NERF_PHOENIX_LTX_PROTOCOL:
result = NERF_PHOENIX_LTX_GetTeamColor(team_ID);
break;
case DYNASTY_PROTOCOL:
result = DYNASTY_GetTeamColor(team_ID);
break;
case SQUAD_HERO_PROTOCOL:
result = SQUAD_HERO_GetTeamColor(team_ID);
break;
case LASER_X_PROTOCOL:
result = LASER_X_GetTeamColor(team_ID);
break;
}
return result;
}
TimedPulseTrain_T *PROTOCOLS_EncodePacket(TagPacket_T *packet)
{
TimedPulseTrain_T *result = NULL;
switch (packet->protocol)
{
default:
case UNKNOWN_PROTOCOL:
break;
case TEST_PROTOCOL:
result = TEST_EncodePacket(packet);
break;
case DUBUQUE_PROTOCOL:
result = DBQ_EncodePacket(packet);
break;
case MILES_TAG_II_PROTOCOL:
result = MILES_TAG_II_EncodePacket(packet);
break;
case NERF_LASER_OPS_PRO_PROTOCOL:
result = NERF_LASER_OPS_PRO_EncodePacket(packet);
break;
case NERF_LASER_STRIKE_PROTOCOL:
result = NERF_LASER_STRIKE_EncodePacket(packet);
break;
case NERF_PHOENIX_LTX_PROTOCOL:
result = NERF_PHOENIX_LTX_EncodePacket(packet);
break;
case DYNASTY_PROTOCOL:
result = DYNASTY_EncodePacket(packet);
break;
case SQUAD_HERO_PROTOCOL:
result = SQUAD_HERO_EncodePacket(packet);
break;
case NEC_PROTOCOL:
result = NULL;
break;
case LASER_X_PROTOCOL:
result = LASER_X_EncodePacket(packet);
break;
}
#ifdef DEBUG_PACKET_ENCODE
KLOG_DEBUG(KLOG_TAG, "\nEncoded %s packet (%u):", ProtocolNameAsString(packet->protocol), result->count);
PrintPulseTrainToConsole(result);
#endif // DEBUG_PACKET_ENCODE
return result;
}
//! Attempts to decode the pulse durations by comparing to the known protocols.
DecodedPacket_T *PROTOCOLS_MaybeDecodePacket(TimedPulseTrain_T *packet)
{
DecodedPacket_T *result = NULL;
if (result == NULL)
{
result = LASER_X_MaybeDecodePacket(packet);
}
if (result == NULL)
{
result = SQUAD_HERO_MaybeDecodePacket(packet);
}
if (result == NULL)
{
result = NEC_MaybeDecodePacket(packet);
if (result != NULL)
{
// Many NEC remotes repeat packets when the button is held down.
// Too avoid double-counting, endure 500ms of silence between packets.
static TickType_t lastPacketTime = 0;
const TickType_t minimumInterval = pdMS_TO_TICKS(500);
TickType_t currentTime = xTaskGetTickCount();
TickType_t timeSinceLastPacket = currentTime - lastPacketTime;
if (timeSinceLastPacket < minimumInterval)
{
// This packet is possibly the result of a held button--ignore it.
KLOG_WARN("NEC", "Received potentially redundant packet--ignoring.");
result->Generic.type = DECODED_PACKET_TYPE_IGNORED;
}
lastPacketTime = currentTime;
}
}
if (result == NULL)
{
result = MILES_TAG_II_MaybeDecodePacket(packet);
}
if (result == NULL)
{
result = DYNASTY_MaybeDecodePacket(packet);
}
if (result == NULL)
{
result = NERF_LASER_OPS_PRO_MaybeDecodePacket(packet);
}
if (result == NULL)
{
result = NERF_LASER_STRIKE_MaybeDecodePacket(packet);
if (result != NULL)
{
// The Nerf Laser Strike blasters send out three identical packets whenever the trigger is pulled,
// over a time of about 200 milliseconds. Only one is necessary to hit. To avoid double-counting,
// we ignore further packets for a short time after receiving a valid one.
static TickType_t lastValidPacketTime = 0;
const TickType_t minimumInterval = pdMS_TO_TICKS(200);
TickType_t currentTime = xTaskGetTickCount();
TickType_t timeSinceLastValidPacket = currentTime - lastValidPacketTime;
if (timeSinceLastValidPacket < minimumInterval)
{
// This packet is possibly redundant with the previous valid packet--ignore it.
KLOG_WARN("Nerf Laser Strike", "Received potentially redundant packet--ignoring.");
result->Generic.type = DECODED_PACKET_TYPE_IGNORED;
}
else
{
lastValidPacketTime = currentTime;
}
}
}
if (result == NULL)
{
result = NERF_PHOENIX_LTX_MaybeDecodePacket(packet);
}
// Keep this close to the end of the list; it's purposely sensitive, and prone to false positives.
if (result == NULL)
{
result = DBQ_MaybeDecodePacket(packet);
}
// Keep this one last, so it doesn't slow down game play.
if (result == NULL)
{
result = TEST_MaybeDecodePacket(packet);
}
#ifdef DEBUG_PACKET_DECODE
#ifdef ESP_PLATFORM
esp_log_level_set(KLOG_TAG, ESP_LOG_DEBUG);
#endif // ESP_PLATFORM
if (result != NULL)
{
KLOG_DEBUG(KLOG_TAG, "Successfully decoded packet as %s: %s", DecodedPacketTypeAsString(result->Generic.type), ProtocolNameAsString(result->Generic.protocol));
vTaskDelay(pdMS_TO_TICKS(10));
if (result->Generic.type == DECODED_PACKET_TYPE_COMMAND_RECEIVED)
{
KLOG_DEBUG(KLOG_TAG, "Command data: %lu", result->Command.data);
vTaskDelay(pdMS_TO_TICKS(10));
}
}
else
{
KLOG_DEBUG(KLOG_TAG, "Couldn't decode packet. Size was %d symbols:", packet->count);
vTaskDelay(pdMS_TO_TICKS(10));
PrintPulseTrainToConsole(packet);
}
#endif // DEBUG_PACKET_DECODE
// Remember which receiver saw the packet.
if (result != NULL)
{
result->Generic.receiver = packet->receiver;
}
return result;
}

188
Protocols/Protocols.h Executable file
View file

@ -0,0 +1,188 @@
/*
* This program source code file is part of SystemK, a library in the KTag project.
*
* 🛡 <https://ktag.clubk.club> 🃞
*
* Copyright © 2016-2025 Joseph P. Kearney and the KTag developers.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* There should be a copy of the GNU Affero General Public License in the LICENSE
* file in the root of this repository. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef PROTOCOLS_H
#define PROTOCOLS_H
#include <stdbool.h>
#include <stdint.h>
#include <stddef.h>
#define FOREACH_PROTOCOL(PROTOCOL) \
PROTOCOL(UNKNOWN_PROTOCOL) \
PROTOCOL(TEST_PROTOCOL) \
PROTOCOL(DUBUQUE_PROTOCOL) \
PROTOCOL(MILES_TAG_II_PROTOCOL) \
PROTOCOL(NERF_LASER_OPS_PRO_PROTOCOL) \
PROTOCOL(NERF_LASER_STRIKE_PROTOCOL) \
PROTOCOL(NERF_PHOENIX_LTX_PROTOCOL) \
PROTOCOL(DYNASTY_PROTOCOL) \
PROTOCOL(SQUAD_HERO_PROTOCOL) \
PROTOCOL(NEC_PROTOCOL) \
PROTOCOL(LASER_X_PROTOCOL) \
PROTOCOL(LAST_PROTOCOL)
#define GENERATE_ENUM(ENUM) ENUM,
#define GENERATE_STRING(STRING) #STRING,
typedef enum
{
FOREACH_PROTOCOL(GENERATE_ENUM)
} Protocol_T;
#define MAX_PULSES 125
#define LAST_PULSE 0
typedef enum
{
FREQUENCY_38kHz,
FREQUENCY_56kHz
} ModulationFrequency_T;
//! On-Off Keying (OOK) symbols
typedef enum
{
SPACE = 0,
MARK = 1
} OOK_Symbol_T;
//! Represents a length of time, in microseconds.
typedef uint32_t Duration_in_us_T;
typedef enum
{
TAG_SENSOR_UNKNOWN = 0,
TAG_SENSOR_NONE,
TAG_SENSOR_FORWARD,
TAG_SENSOR_LEFT,
TAG_SENSOR_RIGHT,
TAG_SENSOR_REMOTE
} TagSensorLocation_T;
typedef struct
{
//! Use a value of #LAST_PULSE to mean the pulsetrain is over.
Duration_in_us_T duration : 15;
OOK_Symbol_T symbol : 1;
} __attribute__((packed)) TimedBit_T;
//! Timing information for a single pulse, packed into a 32-bit integer.
/*
* \see #rmt_item32_t in the ESP-IDF.
*/
typedef struct
{
union
{
struct
{
Duration_in_us_T duration0 : 15;
OOK_Symbol_T symbol0 : 1;
Duration_in_us_T duration1 : 15;
OOK_Symbol_T symbol1 : 1;
};
uint32_t val;
};
} __attribute__((packed)) TimedPulse_T;
typedef struct
{
union
{
TimedPulse_T pulsetrain[MAX_PULSES];
TimedBit_T bitstream[2 * MAX_PULSES];
};
uint16_t count;
TagSensorLocation_T receiver;
} __attribute__((packed)) TimedPulseTrain_T;
typedef enum
{
DECODED_PACKET_TYPE_BUFFER_FREE,
DECODED_PACKET_TYPE_UNKNOWN,
DECODED_PACKET_TYPE_IGNORED,
DECODED_PACKET_TYPE_TAG_RECEIVED,
DECODED_PACKET_TYPE_COMMAND_RECEIVED
} DecodedPacketType_T;
typedef struct
{
DecodedPacketType_T type;
TagSensorLocation_T receiver;
uint8_t protocol;
} GenericDecodedPacket_T;
//! Contents of the decoded packet #DECODED_PACKET_TYPE_TAG_RECEIVED.
typedef struct
{
DecodedPacketType_T type;
TagSensorLocation_T receiver;
uint8_t protocol;
uint16_t team_ID;
uint16_t player_ID;
uint16_t damage;
color_t color;
} TagPacket_T;
//! Contents of the decoded packet #DECODED_PACKET_TYPE_COMMAND_RECEIVED.
typedef struct
{
DecodedPacketType_T type;
TagSensorLocation_T receiver;
uint8_t protocol;
uint16_t command;
uint32_t data;
} CommandPacket_T;
typedef union
{
GenericDecodedPacket_T Generic;
TagPacket_T Tag;
CommandPacket_T Command;
} DecodedPacket_T;
static inline void FreeDecodedPacketBuffer(DecodedPacket_T *buffer)
{
if (buffer != NULL)
{
buffer->Generic.type = DECODED_PACKET_TYPE_BUFFER_FREE;
}
}
ModulationFrequency_T PROTOCOLS_GetModulationFrequency(Protocol_T protocol);
color_t PROTOCOLS_GetColor(Protocol_T protocol, uint8_t team_ID, uint8_t player_ID);
TimedPulseTrain_T *PROTOCOLS_EncodePacket(TagPacket_T *packet);
DecodedPacket_T *PROTOCOLS_MaybeDecodePacket(TimedPulseTrain_T *packet);
// Include the supported protocols.
#include "Test.h"
#include "Dubuque.h"
#include "Miles_Tag_II.h"
#include "NEC.h"
#include "Nerf_Laser_Ops_Pro.h"
#include "Nerf_Laser_Strike.h"
#include "Nerf_Phoenix_LTX.h"
#include "Dynasty.h"
#include "Squad_Hero.h"
#include "Laser_X.h"
#endif // PROTOCOLS_H

244
Protocols/Squad_Hero.c Executable file
View file

@ -0,0 +1,244 @@
/*
* This program source code file is part of SystemK, a library in the KTag project.
*
* 🛡 <https://ktag.clubk.club> 🃞
*
* Copyright © 2016-2025 Joseph P. Kearney and the KTag developers.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* There should be a copy of the GNU Affero General Public License in the LICENSE
* file in the root of this repository. If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdbool.h>
#include <stdio.h>
#include "SystemK.h"
#define SQUAD_HERO_BLUE_TEAM 0x01
#define SQUAD_HERO_RED_TEAM 0x02
#define SQUAD_HERO_GREEN_TEAM 0x03
#define SQUAD_HERO_ORANGE_TEAM 0x04
#define SQUAD_HERO_WHITE_TEAM 0x06
#define SQUAD_HERO_HEADER_MARK_DURATION_IN_us 1500
#define SQUAD_HERO_ZERO_DURATION_IN_us 400
#define SQUAD_HERO_ONE_DURATION_IN_us 800
#define SQUAD_HERO_TOLERANCE_IN_us 175
#define MIN_ZERO_IN_us (SQUAD_HERO_ZERO_DURATION_IN_us - SQUAD_HERO_TOLERANCE_IN_us)
#define MAX_ZERO_IN_us (SQUAD_HERO_ZERO_DURATION_IN_us + SQUAD_HERO_TOLERANCE_IN_us)
#define MIN_ONE_IN_us (SQUAD_HERO_ONE_DURATION_IN_us - SQUAD_HERO_TOLERANCE_IN_us)
#define MAX_ONE_IN_us (SQUAD_HERO_ONE_DURATION_IN_us + SQUAD_HERO_TOLERANCE_IN_us)
static const TimedBit_T SQUAD_HERO_HEADER_MARK = {.symbol = MARK, .duration = SQUAD_HERO_HEADER_MARK_DURATION_IN_us};
static const TimedBit_T SQUAD_HERO_ZERO_SPACE = {.symbol = SPACE, .duration = SQUAD_HERO_ZERO_DURATION_IN_us};
static const TimedBit_T SQUAD_HERO_ONE_SPACE = {.symbol = SPACE, .duration = SQUAD_HERO_ONE_DURATION_IN_us};
static const TimedBit_T SQUAD_HERO_ZERO_MARK = {.symbol = MARK, .duration = SQUAD_HERO_ZERO_DURATION_IN_us};
static const TimedBit_T SQUAD_HERO_ONE_MARK = {.symbol = MARK, .duration = SQUAD_HERO_ONE_DURATION_IN_us};
static const TimedBit_T SQUAD_HERO_END = {.symbol = SPACE, .duration = LAST_PULSE};
#define SQUAD_HERO_FOOTER 0x9E
static TimedPulseTrain_T Tag_Send_Buffer;
static DecodedPacket_T Squad_Hero_Tag_Received_Buffers[3] =
{
{.Tag.type = DECODED_PACKET_TYPE_BUFFER_FREE},
{.Tag.type = DECODED_PACKET_TYPE_BUFFER_FREE},
{.Tag.type = DECODED_PACKET_TYPE_BUFFER_FREE}};
static inline DecodedPacket_T *Get_Tag_Packet_Buffer(void)
{
if (Squad_Hero_Tag_Received_Buffers[0].Tag.type == DECODED_PACKET_TYPE_BUFFER_FREE)
{
return &Squad_Hero_Tag_Received_Buffers[0];
}
else if (Squad_Hero_Tag_Received_Buffers[1].Tag.type == DECODED_PACKET_TYPE_BUFFER_FREE)
{
return &Squad_Hero_Tag_Received_Buffers[1];
}
else
{
// Just use it.
return &Squad_Hero_Tag_Received_Buffers[2];
}
}
static inline uint8_t Calculate_Checksum(uint32_t data)
{
uint8_t result = 0;
return result;
}
static inline void PackPulseTrain(TimedPulseTrain_T *pulsetrain, uint32_t data)
{
pulsetrain->bitstream[0] = SQUAD_HERO_HEADER_MARK;
for (uint8_t n = 0; n < 16; n++)
{
if ((data & 0x80000000) == 0x80000000)
{
pulsetrain->bitstream[1 + (n * 2)] = SQUAD_HERO_ONE_SPACE;
}
else
{
pulsetrain->bitstream[1 + (n * 2)] = SQUAD_HERO_ZERO_SPACE;
}
data = data << 1;
if ((data & 0x80000000) == 0x80000000)
{
pulsetrain->bitstream[2 + (n * 2)] = SQUAD_HERO_ONE_MARK;
}
else
{
pulsetrain->bitstream[2 + (n * 2)] = SQUAD_HERO_ZERO_MARK;
}
data = data << 1;
}
// Append the footer.
uint8_t footer = SQUAD_HERO_FOOTER;
for (uint8_t n = 16; n < 20; n++)
{
if ((footer & 0x80) == 0x80)
{
pulsetrain->bitstream[1 + (n * 2)] = SQUAD_HERO_ONE_SPACE;
}
else
{
pulsetrain->bitstream[1 + (n * 2)] = SQUAD_HERO_ZERO_SPACE;
}
footer = footer << 1;
if ((footer & 0x80) == 0x80)
{
pulsetrain->bitstream[2 + (n * 2)] = SQUAD_HERO_ONE_MARK;
}
else
{
pulsetrain->bitstream[2 + (n * 2)] = SQUAD_HERO_ZERO_MARK;
}
footer = footer << 1;
}
pulsetrain->bitstream[(2 * 20) + 1] = SQUAD_HERO_END;
}
TimedPulseTrain_T *SQUAD_HERO_EncodePacket(TagPacket_T *packet)
{
uint32_t packed_data = 0x10AA0000;
packed_data |= (packet->team_ID << 8);
if (packet->damage > 4)
{
packet->damage = 4;
}
packed_data |= packet->damage;
PackPulseTrain(&Tag_Send_Buffer, packed_data);
return &Tag_Send_Buffer;
}
DecodedPacket_T *SQUAD_HERO_MaybeDecodePacket(TimedPulseTrain_T *packet)
{
// Some implementations (ESP32) do not report odd numbers of pulses.
if ((packet->count != 41) && (packet->count != 42))
{
// Fail fast!
return NULL;
}
else
{
// Convert pulse durations to bits.
// packet->bitstream[0] is the header mark--ignore it.
uint32_t data = 0;
for (uint8_t n = 0; n < 32; n++)
{
if ((packet->bitstream[n + 1].duration > MIN_ONE_IN_us) && (packet->bitstream[n + 1].duration < MAX_ONE_IN_us))
{
// One
data |= ((uint32_t)1 << (31 - n));
}
else if ((packet->bitstream[n + 1].duration > MIN_ZERO_IN_us) && (packet->bitstream[n + 1].duration < MAX_ZERO_IN_us))
{
// Zero--nothing to do.
}
else
{
// This mark is neither a zero or a one; abort.
return NULL;
}
}
uint8_t checksum = 0;
for (uint8_t n = 0; n < 8; n++)
{
if ((packet->bitstream[n + 33].duration > MIN_ONE_IN_us) && (packet->bitstream[n + 33].duration < MAX_ONE_IN_us))
{
// One
checksum |= ((uint8_t)1 << (7 - n));
}
else if ((packet->bitstream[n + 33].duration > MIN_ZERO_IN_us) && (packet->bitstream[n + 33].duration < MAX_ZERO_IN_us))
{
// Zero--nothing to do.
}
else
{
// This mark is neither a zero or a one; abort.
return NULL;
}
}
DecodedPacket_T *Tag_Rx_Buffer = Get_Tag_Packet_Buffer();
Tag_Rx_Buffer->Tag.type = DECODED_PACKET_TYPE_TAG_RECEIVED;
Tag_Rx_Buffer->Tag.protocol = SQUAD_HERO_PROTOCOL;
Tag_Rx_Buffer->Tag.player_ID = 0;
Tag_Rx_Buffer->Tag.team_ID = (data & 0x0000FF00) >> 8;
Tag_Rx_Buffer->Tag.damage = (data & 0x000000FF);
Tag_Rx_Buffer->Tag.color = SQUAD_HERO_GetTeamColor(Tag_Rx_Buffer->Tag.team_ID);
return Tag_Rx_Buffer;
}
}
color_t SQUAD_HERO_GetTeamColor(uint8_t team_ID)
{
color_t result = COLOR_WHITE;
if (team_ID == SQUAD_HERO_BLUE_TEAM)
{
result = COLOR_BLUE;
}
else if (team_ID == SQUAD_HERO_RED_TEAM)
{
result = COLOR_RED;
}
else if (team_ID == SQUAD_HERO_GREEN_TEAM)
{
result = COLOR_GREEN;
}
else if (team_ID == SQUAD_HERO_ORANGE_TEAM)
{
result = COLOR_ORANGE;
}
else if (team_ID == SQUAD_HERO_WHITE_TEAM)
{
result = COLOR_WHITE;
}
else
{
// Nothing to do.
}
return result;
}

37
Protocols/Squad_Hero.h Executable file
View file

@ -0,0 +1,37 @@
/*
* This program source code file is part of SystemK, a library in the KTag project.
*
* 🛡 <https://ktag.clubk.club> 🃞
*
* Copyright © 2016-2025 Joseph P. Kearney and the KTag developers.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* There should be a copy of the GNU Affero General Public License in the LICENSE
* file in the root of this repository. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef SQUAD_HERO_H
#define SQUAD_HERO_H
/** \file
* \brief This is the Squad Hero protocol.
*/
#include <stdbool.h>
#include <stdint.h>
TimedPulseTrain_T *SQUAD_HERO_EncodePacket(TagPacket_T *packet);
DecodedPacket_T *SQUAD_HERO_MaybeDecodePacket(TimedPulseTrain_T *packet);
color_t SQUAD_HERO_GetTeamColor(uint8_t team_ID);
#endif // SQUAD_HERO_H

144
Protocols/Test.c Executable file
View file

@ -0,0 +1,144 @@
/*
* This program source code file is part of SystemK, a library in the KTag project.
*
* 🛡 <https://ktag.clubk.club> 🃞
*
* Copyright © 2016-2025 Joseph P. Kearney and the KTag developers.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* There should be a copy of the GNU Affero General Public License in the LICENSE
* file in the root of this repository. If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdbool.h>
#include <stdio.h>
#include "SystemK.h"
static const char *KLOG_TAG = "Test Protocol";
#define TEST_PULSE_SUBTRAHEND_IN_us 300
//! The shortest test packet is a two marks, the first of duration (3 * TEST_PULSE_SUBTRAHEND_IN_us), and the second of duration TEST_PULSE_SUBTRAHEND_IN_us.
#define TEST_MINIMUM_LONGEST_PULSE_DURATION_IN_us (3 * TEST_PULSE_SUBTRAHEND_IN_us)
#define TEST_TOLERANCE_IN_us ((TEST_PULSE_SUBTRAHEND_IN_us / 2) - 1)
static TimedPulseTrain_T Tag_Send_Buffer;
static DecodedPacket_T Test_Tag_Received_Buffers[3] =
{
{.Tag.type = DECODED_PACKET_TYPE_BUFFER_FREE},
{.Tag.type = DECODED_PACKET_TYPE_BUFFER_FREE},
{.Tag.type = DECODED_PACKET_TYPE_BUFFER_FREE}};
static inline DecodedPacket_T *Get_Tag_Packet_Buffer(void)
{
if (Test_Tag_Received_Buffers[0].Tag.type == DECODED_PACKET_TYPE_BUFFER_FREE)
{
return &Test_Tag_Received_Buffers[0];
}
else if (Test_Tag_Received_Buffers[1].Tag.type == DECODED_PACKET_TYPE_BUFFER_FREE)
{
return &Test_Tag_Received_Buffers[1];
}
else
{
// Just use it.
return &Test_Tag_Received_Buffers[2];
}
}
static inline void PackPulseTrain(TimedPulseTrain_T *pulsetrain, uint32_t longest_pulse_duration_in_us)
{
uint_fast8_t index = 0;
uint32_t next_pulse_duration_in_us = longest_pulse_duration_in_us;
TimedBit_T next_bit;
if (next_pulse_duration_in_us < TEST_MINIMUM_LONGEST_PULSE_DURATION_IN_us)
{
next_pulse_duration_in_us = TEST_MINIMUM_LONGEST_PULSE_DURATION_IN_us;
}
while ((next_pulse_duration_in_us >= TEST_PULSE_SUBTRAHEND_IN_us) && (index < MAX_PULSES))
{
// Even indicies are marks.
if ((index % 2) == 0)
{
next_bit.symbol = MARK;
}
// Odd indicies are spaces.
else
{
next_bit.symbol = SPACE;
}
next_bit.duration = next_pulse_duration_in_us;
pulsetrain->bitstream[index] = next_bit;
next_pulse_duration_in_us = next_pulse_duration_in_us - TEST_PULSE_SUBTRAHEND_IN_us;
index++;
}
// Mark the end of the train.
next_bit.symbol = SPACE;
next_bit.duration = LAST_PULSE;
pulsetrain->bitstream[index] = next_bit;
pulsetrain->count = index;
}
TimedPulseTrain_T *TEST_EncodePacket(TagPacket_T *packet)
{
uint32_t packed_data = 2100;
PackPulseTrain(&Tag_Send_Buffer, packed_data);
return &Tag_Send_Buffer;
}
DecodedPacket_T *TEST_MaybeDecodePacket(TimedPulseTrain_T *packet)
{
// packet->bitstream[0] is the longest pulse--assume it was received correctly.
uint32_t longest_pulse_duration_in_us = packet->bitstream[0].duration;
if ((longest_pulse_duration_in_us - TEST_TOLERANCE_IN_us) < TEST_MINIMUM_LONGEST_PULSE_DURATION_IN_us)
{
// The first pulse is too short!
return NULL;
}
for (uint_fast8_t index = 1; index < packet->count; index++)
{
uint32_t expected_pulse_duration_in_us = longest_pulse_duration_in_us - (index * TEST_PULSE_SUBTRAHEND_IN_us);
if (packet->bitstream[index].duration < (expected_pulse_duration_in_us - TEST_TOLERANCE_IN_us))
{
KLOG_WARN(KLOG_TAG, "Pulse %u is too short! Expected %lu; received %u.", index, expected_pulse_duration_in_us, packet->bitstream[index].duration);
return NULL;
}
if (packet->bitstream[index].duration > (expected_pulse_duration_in_us + TEST_TOLERANCE_IN_us))
{
KLOG_WARN(KLOG_TAG, "Pulse %u is too long! Expected %lu; received %u.", index, expected_pulse_duration_in_us, packet->bitstream[index].duration);
return NULL;
}
}
DecodedPacket_T *Command_Rx_Buffer = Get_Tag_Packet_Buffer();
Command_Rx_Buffer->Command.type = DECODED_PACKET_TYPE_COMMAND_RECEIVED;
Command_Rx_Buffer->Command.protocol = TEST_PROTOCOL;
Command_Rx_Buffer->Command.data = longest_pulse_duration_in_us;
return Command_Rx_Buffer;
}
color_t TEST_GetTeamColor(__attribute__((unused)) uint8_t team_ID)
{
color_t result = COLOR_WHITE;
return result;
}

38
Protocols/Test.h Executable file
View file

@ -0,0 +1,38 @@
/*
* This program source code file is part of SystemK, a library in the KTag project.
*
* 🛡 <https://ktag.clubk.club> 🃞
*
* Copyright © 2016-2025 Joseph P. Kearney and the KTag developers.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* There should be a copy of the GNU Affero General Public License in the LICENSE
* file in the root of this repository. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef TEST_H
#define TEST_H
/** \file
* \brief This is the test protocol.
*
*/
#include <stdbool.h>
#include <stdint.h>
TimedPulseTrain_T *TEST_EncodePacket(TagPacket_T *packet);
DecodedPacket_T *TEST_MaybeDecodePacket(TimedPulseTrain_T *packet);
color_t TEST_GetTeamColor(uint8_t team_ID);
#endif // TEST_H