|
| 1 | +/** @file |
| 2 | + Cellular Tracking Technologies (CTT) LifeTag/PowerTag/HybridTag. |
| 3 | +
|
| 4 | + Copyright (C) 2025 Jonathan Caicedo <jonathan@jcaicedo.com> |
| 5 | + Credit to https://github.com/tve for the CTT tag implementation details via their work on RadioJay (https://radiojay.org/) and Motus Test Tags (https://github.com/tve/motus-test-tags). |
| 6 | +
|
| 7 | + This program is free software; you can redistribute it and/or modify |
| 8 | + it under the terms of the GNU General Public License as published by |
| 9 | + the Free Software Foundation; either version 2 of the License, or |
| 10 | + (at your option) any later version. |
| 11 | +*/ |
| 12 | +/** @fn static int ctt_decode(r_device *decoder, bitbuffer_t *bitbuffer) |
| 13 | +Cellular Tracking Technologies (https://celltracktech.com/) LifeTag/PowerTag/HybridTag. |
| 14 | +
|
| 15 | +The CTT LifeTag/PowerTag/HybridTag is a lightweight transmitter used for wildlife tracking and research - most commonly used with the Motus Wildlife Tracking System (https://motus.org/). |
| 16 | +The tags transmit a unique identifier (ID) at a fixed bitrate of 25 kbps using Frequency Shift Keying (FSK) modulation on 434 MHz. |
| 17 | +
|
| 18 | +The packet format consists of: |
| 19 | +
|
| 20 | +- PREAMBLE: 24 bits of alternating 1/0 (0xAA if byte-aligned) for receiver bit-clock sync (preamble length can be shorter, depending on hardware) |
| 21 | +- SYNC: 2 bytes fixed pattern 0xD3, 0x91 marking the packet start |
| 22 | +- ID: 20-bit tag ID encoded into 4 bytes (5 bits per byte) using a 32-entry dictionary |
| 23 | +- CRC: 1-byte SMBus CRC-8 over the 4 encoded ID bytes |
| 24 | +
|
| 25 | + AA AA AA D3 91 78 55 4C 33 58 |
| 26 | + |--------| |-----| |-----------| |--| |
| 27 | + Preamble Sync ID CRC |
| 28 | +
|
| 29 | + A beep is a single packet. |
| 30 | +
|
| 31 | +- LifeTag: programmed with a standard 5-second beep rate. |
| 32 | +- PowerTag: user-defined beep rate. |
| 33 | +- HybridTag: transmits a beep every 2-15 seconds. |
| 34 | +
|
| 35 | +There's a 20-bit subset of the 32-bit ID space set aside for Motus tag use. We set `valid_motus` to true if all 4 bytes of the ID are present in the Motus code dictionary. |
| 36 | +However, `valid_motus` not being set doesn't mean that a tag is invalid, just that it's not recognized as a tag used with Motus. |
| 37 | +
|
| 38 | +*/ |
| 39 | + |
| 40 | +#include "decoder.h" |
| 41 | + |
| 42 | +static const uint8_t sync[2] = {0xD3, 0x91}; |
| 43 | + |
| 44 | +static const uint8_t motus_code[32] = { |
| 45 | + 0x00, 0x07, 0x19, 0x1E, 0x2A, 0x2D, 0x33, 0x34, |
| 46 | + 0x4B, 0x4C, 0x52, 0x55, 0x61, 0x66, 0x78, 0x7F, |
| 47 | + 0x80, 0x87, 0x99, 0x9E, 0xAA, 0xAD, 0xB3, 0xB4, |
| 48 | + 0xCB, 0xCC, 0xD2, 0xD5, 0xE1, 0xE6, 0xF8, 0xFF}; |
| 49 | + |
| 50 | +// Helper: check if a byte is in motus_code |
| 51 | +static int byte_in_motus_code(uint8_t b) |
| 52 | +{ |
| 53 | + for (int j = 0; j < 32; ++j) { |
| 54 | + if (b == motus_code[j]) |
| 55 | + return 1; |
| 56 | + } |
| 57 | + return 0; |
| 58 | +} |
| 59 | + |
| 60 | +static int ctt_decode(r_device *decoder, bitbuffer_t *bitbuffer) |
| 61 | +{ |
| 62 | + int events = 0; |
| 63 | + int saw_bad_crc = 0; |
| 64 | + |
| 65 | + // Expect at least sync + payload (56 bits), but allow extra (e.g., preamble) |
| 66 | + const int min_bits = 56; |
| 67 | + |
| 68 | + for (int row = 0; row < bitbuffer->num_rows; ++row) { |
| 69 | + if (bitbuffer->bits_per_row[row] < min_bits) { |
| 70 | + continue; // DECODE_ABORT_LENGTH? |
| 71 | + } |
| 72 | + |
| 73 | + // Search for sync (allow 0 bit errors initially; increase to 2 for noisy signals) |
| 74 | + unsigned sync_pos = bitbuffer_search(bitbuffer, row, 0, sync, 16); // 2 bytes = 16 bits |
| 75 | + |
| 76 | + if (sync_pos >= bitbuffer->bits_per_row[row]) { |
| 77 | + continue; // DECODE_ABORT_EARLY? |
| 78 | + } |
| 79 | + |
| 80 | + // Ensure enough bits after sync for ID (4B) + CRC (1B) = 40 bits |
| 81 | + if (sync_pos + 16 + 40 > bitbuffer->bits_per_row[row]) { |
| 82 | + continue; // DECODE_ABORT_EARLY? |
| 83 | + } |
| 84 | + |
| 85 | + // Extract 5 bytes after sync |
| 86 | + uint8_t payload[5]; |
| 87 | + bitbuffer_extract_bytes(bitbuffer, row, sync_pos + 16, payload, 40); |
| 88 | + |
| 89 | + // SMBus CRC-8 over the 4 encoded ID bytes. |
| 90 | + uint8_t calc_crc = crc8(payload, 4, 0x07, 0x00); |
| 91 | + if (calc_crc != payload[4]) { |
| 92 | + decoder_logf(decoder, 2, __func__, "CRC fail (calc 0x%02X != rx 0x%02X)", calc_crc, payload[4]); |
| 93 | + saw_bad_crc = 1; |
| 94 | + continue; |
| 95 | + } |
| 96 | + |
| 97 | + uint32_t id = |
| 98 | + ((uint32_t)payload[0] << 24) | |
| 99 | + ((uint32_t)payload[1] << 16) | |
| 100 | + ((uint32_t)payload[2] << 8) | |
| 101 | + (uint32_t)payload[3]; |
| 102 | + |
| 103 | + // Check if all 4 ID bytes are in the Motus code dictionary; indicates a Motus-registered tag |
| 104 | + int motus_tag = |
| 105 | + byte_in_motus_code(payload[0]) && |
| 106 | + byte_in_motus_code(payload[1]) && |
| 107 | + byte_in_motus_code(payload[2]) && |
| 108 | + byte_in_motus_code(payload[3]); |
| 109 | + |
| 110 | + /* clang-format off */ |
| 111 | + data_t *data = data_make( |
| 112 | + "model", "", DATA_STRING, "CTT-Tag", |
| 113 | + "id", "Tag ID", DATA_FORMAT, "0x%08X", DATA_INT, id, |
| 114 | + "valid_motus", "Valid Motus tag", DATA_INT, motus_tag, |
| 115 | + "mic", "Integrity", DATA_STRING, "CRC", |
| 116 | + NULL); |
| 117 | + /* clang-format on */ |
| 118 | + |
| 119 | + decoder_output_data(decoder, data); |
| 120 | + events++; |
| 121 | + } |
| 122 | + |
| 123 | + return events > 0 ? events : saw_bad_crc ? DECODE_FAIL_MIC : 0; |
| 124 | +} |
| 125 | + |
| 126 | +static char const *const output_fields[] = { |
| 127 | + "model", |
| 128 | + "id", |
| 129 | + "valid_motus", |
| 130 | + "mic", |
| 131 | + NULL, |
| 132 | +}; |
| 133 | + |
| 134 | +r_device const ctt_life_power_hybrid = { |
| 135 | + .name = "Cellular Tracking Technologies LifeTag/PowerTag/HybridTag", |
| 136 | + .modulation = FSK_PULSE_PCM, |
| 137 | + /* at BR=25 kbps, bit_time=40µs*/ |
| 138 | + .short_width = 40, |
| 139 | + .long_width = 40, |
| 140 | + .reset_limit = 500, /* 500 µs: slightly above max run of 0's */ |
| 141 | + .decode_fn = &ctt_decode, |
| 142 | + .fields = output_fields, |
| 143 | + .disabled = 0, |
| 144 | +}; |
0 commit comments