Skip to content

Conversation

@JCBird1012
Copy link

@JCBird1012 JCBird1012 commented Jul 25, 2025

Hello,

This decoder [ctt.c] adds support for decoding Cellular Tracking Technologies Life/Power/HybridTag(s) which are lightweight transmitters used for wildlife tracking and research - most commonly used with the Motus Wildlife Tracking System.

The CTT tags transmit on 434.00 MHz using FSK modulation at 25 kbps. The packet format consists of:

  • PREAMBLE: 16/24 bits of alternating 1/0 (0xAA if byte-aligned) for receiver bit-clock sync (the preamble length can vary - for my testing I've used 24 bits, though per comments I've seen, actual hardware is shorter)
  • SYNC: 2 bytes fixed pattern 0xD3 0x91 marking the packet start
  • ID: 20-bit tag ID encoded into 4 bytes (5 bits per byte) using a 32-entry dictionary
  • CRC: 1-byte SMBus CRC-8 over the 4 encoded ID bytes

I have to give credit to @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).

I'm planning on adding test data to https://github.com/merbanan/rtl_433_tests once I get a good variety of tags/IDs to test + some good recordings.

@JCBird1012
Copy link
Author

JCBird1012 commented Jul 25, 2025

There are some comments in [ctt.c] that are somewhat open questions on how to handle some things - especially around returning/continuing that I'm not 100% sure I'm handling correctly.

Additionally, since this is such a niche device - I'm open to leaving it disabled by default - though I guess, in theory, decodes in the wild could happen if a tagged bird happens to fly by. 😉

@gdt
Copy link
Collaborator

gdt commented Jul 25, 2025

My take is that as long as the decoder will not generate false decodes, it's ok to leave it enabled. So far we aren't really having CPU time problems, and getting a decode like that would be cool.

I see in the comments about 5 bits encoded into a byte with a table. I am wondering if this is supposed to be ECC, or if it's a way to reject bad decodes.

@gdt gdt added the device support Request for a new/improved device decoder label Jul 25, 2025
@JCBird1012
Copy link
Author

JCBird1012 commented Jul 25, 2025

If I had to guess - the 5 bits to a byte mapping is apart of the design for reliability reasons. I wouldn't call it ECC because I can't see that being used practically for error correction, just a way to reject bad decodes.

Tag IDs are 20-bits long, but get converted with that table to 32-bits on the air. Again guessing, it was probably a combination of trying to maintain Hamming distance between valid symbols and DC balance.

@gdt
Copy link
Collaborator

gdt commented Jul 25, 2025

Sounds sensible. The rtl_433 decoder should reject all frames that are considered invalid, as part of avoiding bad decodes. The rarer the actual use, probably the rarer bad decodes need to be for it to make sense to be default on.

I suspect opinions differ; I lean to enable unless false decodes personally.

@zuckschwerdt
Copy link
Collaborator

Some notes:
Please document the expected transmission: does it have repeats of the packet? How many nominally?
Try to format the packet format like in other decoders, one line with named bytes or nibbles and then a list of fields.
It might be good to have a filename and decoder name with more that ctt and the tag suffix is too generic, how about ctt_wildlife or ctt_motus?
The "model" needs to be a short "alphanum dash alphanum".
Continued line indent is 8 spaces (e.g. on data_make, fields, and r_device).
the fields should be output_fields and multiline with a comma also on the last line.
You should not output "id_raw", "id_hex", "crc".
You don't need enc_id, it's just a copy of payload.
You don't need crc_val, just crc8(payload, 5, 0x07, 0x00) != 0) will work.

@zuckschwerdt
Copy link
Collaborator

zuckschwerdt commented Jul 25, 2025

That 5-to-8 coding with e.g. 0x00, 0x80, and 0xff does not look like a proper constant-weight code and won't much help clock recovery (it doesn't limit runs effectively). Somewhat strange.

E.g. The Radiohead protocol has a proper constant-weight code: https://github.com/merbanan/rtl_433/blob/master/src/devices/radiohead_ask.c#L28

Tags actually have 32-bit identifiers - not 20
@JCBird1012
Copy link
Author

JCBird1012 commented Jul 25, 2025

Your point made me dig a bit more - I don't actually think the mapping is required. I was wrong - tags actually have 32-bit identifiers. This is consistent even from their site (https://celltracktech.com/collections/digital-radio-products/products/lifetag-with-flex-tab)

Over 4 billion unique digital IDs

20-bit tags identifiers would get nowhere near that - 2^32 lines up nicely. There's a typo elsewhere on their website (https://celltracktech.com/pages/hybridtag) where they say 4 million instead of billion and so I very incorrectly off-the-cuff told myself 2^20 would be in that ballpark (not even, it's off by a factor of 4).

Motus itself even represents it in that way as 32-bits (Manufacturer ID) - if it was really 20-bits, it wouldn't make sense for them to use the "on-the-air" value.

Screenshot 2025-07-25 at 12 49 18

I fell for a red-herring as the reverse-engineered tag implementation I was referencing (CTTTestTag/src/main.cpp) had the mapping logic as residual code that got #ifdef'ed out. I figured the author had manually done the mapping of 20-bits -> 32-bits to save a processing step and just #define'd that value instead. Turns out, that was the actual ID all along.
This is consistent with another file in that repo where the mapping is non-existent.

Sorry for the confusion - I'm not quite sure how I missed all that before.
On the bright side, the implementation is much simpler now - which I guess makes this less of a protocol decoder and more a recitation of bytes from on-air (with syncword + CRC checking).

@JCBird1012
Copy link
Author

After some more chatting with folks and experimenting, I have some clarity, however confusing it may be.

Tag IDs are 32-bits long - and that's how they're generally represented. However, from previous experiments/research (thanks @tve), behind the scenes, only a 20-bit subset of that 32-bit space are considered "valid" tags and get decoded by the official hardware used for Motus. It's... a little strange, but as @gdt pointed out, it's probably an extra error-detection step.
I originally had some logic to check against the dictionary and abort out if a matching byte wasn't found - so it looks like I'll need to re-add that.

Even stranger considering CTT advertises “Over 4 billion unique digital IDs” - weird to advertise a 32-bit ID space and then only use a subset of that. 🤷‍♂️

With that said, I'll touch up this PR and get everything squared away for another pass.

@zuckschwerdt
Copy link
Collaborator

But you wouldn't want to collect random ID's anyway, right? You'd have a catalog of used tags and then compare to that?
I.e. the encoding then works the other way: you have a number of known good tags in your operation and would just discard all other readings, right?

@gdt
Copy link
Collaborator

gdt commented Jul 29, 2025

I am shocked, shocked that marketing people would say something that is untrue/misleading. If the way the protocol really works is that only a subset are valid, that's a kind of checksum and it would be great if rtl_433 rejected ids that aren't valid.

As for whether the Motus-built receivers or rtl_433 receive and log all tag values, or only some configured set, idk on the Motus, but i'd think they'd log all valid decodes, and researchers might share, because any decode is interesting. The rtl_433 way should be to decode what's there, and let the user sort out what's wanted. It's not like we decline to decode thermometers because the id isn't in a config file, so I don't understand where you're going with the random ID comment.

@zuckschwerdt
Copy link
Collaborator

With "collect random ID" I wanted to say that you wouldn't assign value to just any ID, in some encoded subset or not. You'd need to know that it's a tag you (or someone you forward it to) know something about.

In other words, ID + known usage = usable value, and not ID + valid coding = usable value. To me it seems that way at least. Maybe other vendors/users use other parts of the possible ID space?

@gdt
Copy link
Collaborator

gdt commented Jul 29, 2025

I am not following "assign value". If I am randomly listening, and some tag shows up, I find that interesting, and I'd just as soon it is logged, and then if in the mood I might see if it recurs etc. This is sort of like watching neighbor thermometers.

As for the pseudo-checksum 20/32 scheme, so far we have one known kind of transmitter and that's what it does, so it makes sense to align the code. Evidence of similar devices with compatible coding and a different subset or no subset would warrant changes.

I therefore come down on

  • given that the 20/32 scheme seems to be true of every transmitter anyone has encountered, leave it in as a check
  • decode whatever ids show up and output them, just like any other decoder
  • people of course using this may wish to pay attention to their known ids, but that's up to them

and I am not understanding what you are wanting the code to do that is different.

@zuckschwerdt
Copy link
Collaborator

All I wanted to convey is: I don't see the intersting'ness of an ID increasing by limiting it to a subset. An ID does not get better or more interesting with that filter. An ID get's interesting by combining it with knowledge about the tag.

We did have some subset filters in decoders before and that later resulted in users asking why their devices are not received.
Maybe we could add a "valid_id: 0/1" or second "motus_id: 20bit" field instead of rejecting (CRC validated) transmissions.

@gdt
Copy link
Collaborator

gdt commented Jul 29, 2025

I understand now and I see your point. The real question is if there are devices that use this scheme and don't do the subset. But a motus_20bit_valid boolean seems like a good way to begin to gather info.

@JCBird1012
Copy link
Author

JCBird1012 commented Jul 29, 2025

Tags can/are used outside of Motus, but it feels strange to have such a large ID space "wasted" for outside use. I would presume any use of these tags outside of Motus would be a minority, not the majority - but I'm speculating. CTT makes its own hardware for tag tracking as well - so maybe the rest of the tag space is aimed towards that - tags that will never be on the Motus network. However, I'm not 100% sure - all I have to go off of are my observations with Motus.

CTT used to sell a "Motus" adapter - https://celltracktech.com/pages/motus-adapter - and from what I've heard/tested (you can make one yourself now) - the filtering is done at the dongle level - it won't pass a tag ID along if it's not in that subset.
That makes sense - it's branded for use for Motus - why bother with tags that aren't set aside for Motus use?

The separation makes sense - Motus stations shouldn't decode non-Motus tags, but it feels strange to have that enforced at the station-level and not somewhere further along the chain. Now you're stuck with a "rigid" definition built into the firmware of each station instead of being able to tweak that more easily in the future somewhere in the data processing pipeline (especially considering tags have to be pre-registered with Motus - just check against that) - but I digress, I'm not here to police design decisions.

But I like to what you're both leaning towards - consider all tags as valid, and then add a Motus "bit" if we know it's valid for Motus - that's handy because it can tip people off to go to the website and enter that ID if they're interested in learning more. But we don't strictly limit ourselves to just Motus tags, which is nice.

However, that means, all we have to validiate against false decodes is the syncword, CRC, and packet length - I haven't run into trouble with any other decoders yet - if we're ok with that being the only three things defining the protocol, then I'll make those changes. I'm curious if there's been trouble with simple decoders like this when another device comes along that just happens to use the same syncword, packet length, etc...

@gdt
Copy link
Collaborator

gdt commented Jul 29, 2025

My take is we're ok until people are annoyed by false decodes and we can revisit it. Not a big deal.

@zuckschwerdt
Copy link
Collaborator

Sync, length and CRC are good enough. Usually we'd need to exclude an ID of all 0's as that CRC will be 0 and bitbuffers of all 0's might happen. But then there won't be a good sync I guess.

@JCBird1012
Copy link
Author

I went ahead and also moved away from the "Motus" naming scheme for the file + device - I figured that was a misnomer after all the discussion. Let me know if there's anything else I should tweak/change.

I can't type coherent sentences
@zuckschwerdt
Copy link
Collaborator

Timings might need a revision. It's 40 µs PCM. You don't need a .tolerance with PCM, 25% is default. 200 µs .gap_limit will only allow 5 consecutive 0's and then 50 ms reset is not effective but would allow 1250 0 bits.
Are there multiple packets in a single transmission? Otherwise remove gap, set reset to slightly above the maximum expected run of 0's, maybe 500 µs.

@JCBird1012
Copy link
Author

Adjusted - decodes feel much smoother now (placebo maybe?). Thank you!
I spit-balled those values initially until I landed on something that worked somewhat consistently.

Also, it's one packet per transmission - gap_limit wasn't really doing anything there, I suppose.

@zuckschwerdt zuckschwerdt force-pushed the master branch 2 times, most recently from 8d2ffe8 to a796732 Compare October 20, 2025 22:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

device support Request for a new/improved device decoder

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants