Skip to content

Commit 1d705cf

Browse files
authored
Add support for Cellular Tracking Technologies Life/Power/HybridTag (#3335)
1 parent 20490fb commit 1d705cf

5 files changed

Lines changed: 148 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,7 @@ See [CONTRIBUTING.md](./docs/CONTRIBUTING.md).
407407
[317] Code Alarm FRDPC2002 Car Remote
408408
[318] RFM69 LowPowerLab Moteino board (-s 1000k)
409409
[319] Shenzhen Wale WL-TH6R Temperature & Humidity Sensor
410+
[320] Cellular Tracking Technologies LifeTag/PowerTag/HybridTag
410411
411412
* Disabled by default, use -R n or a conf file to enable
412413

conf/rtl_433.example.conf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,7 @@ convert si
558558
protocol 317 # Code Alarm FRDPC2002 Car Remote
559559
protocol 318 # RFM69 LowPowerLab Moteino board (-s 1000k)
560560
protocol 319 # Shenzhen Wale WL-TH6R Temperature & Humidity Sensor
561+
protocol 320 # Cellular Tracking Technologies LifeTag/PowerTag/HybridTag
561562

562563
## Flex devices (command line option "-X")
563564

include/rtl_433_devices.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,7 @@
327327
DECL(code_alarm_frdpc2000_car_remote) \
328328
DECL(rfm69_lowpowerlab_moteino) \
329329
DECL(shenzhen_wale_wl_th6r) \
330+
DECL(ctt_life_power_hybrid) \
330331
/* Add new decoders here. */
331332

332333
#define DECL(name) extern r_device name;

src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ add_library(r_433 STATIC
9999
devices/compustar_1wg3r.c
100100
devices/continental_car_remote.c
101101
devices/cotech_36_7959.c
102+
devices/ctt_life_power_hybrid.c
102103
devices/current_cost.c
103104
devices/danfoss.c
104105
devices/deltadore_x3d.c
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
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

Comments
 (0)