The answer
See the code at the end for how to calculate this, but if you're interested, here are the details explaining how I got there. Throughout this text, numbers are mostly in hex, so keep that in mind.
The process
For the first data set, I noticed that each set bit seemed to correspond to a particular XOR difference. For example in the first two samples only bit 0 changes and the XOR difference is 39^37 = 0e
We can quickly spot a pattern if we express in binary as well as hex:
04 92 39 00 00
04 92 37 01 00 ; bit 1 = 0e 00001110
04 92 25 02 00 ; bit 2 = 1c 00011100
04 92 01 04 00 ; bit 3 = 38 00111000
04 92 49 08 00 ; bit 4 = 70 01110000
So as the bit position moves one to the left, so does the difference. Nice and easy. So with this information we can calculate all the other values, so that 0A should be 39 ^ 1c ^ 70 = 55 and indeed, that's what your data shows:
04 92 55 0A 00
We can try the same thing with data set 2:
03 C0 66 00 00 00
03 C0 D3 01 00 00 ; bit 1 = b5 10110101
03 C0 23 02 00 00 ; bit 2 = 45 01000101
03 C0 EC 04 00 00 ; bit 4 = 8a 10001010
03 C0 5D 08 00 00 ; bit 8 = 3b 00111011
We can try the same thing with data set 3:
03 C0 9B 00 23 00
03 C0 2E 01 23 00 ; bit 1 = b5 10110101
03 C0 DE 02 23 00 ; bit 2 = 45 01000101
03 C0 11 04 23 00 ; bit 4 = 8a 10001010
03 C0 A0 08 23 00 ; bit 8 = 3b 00111011
So they both use the same pattern, but it's no longer just a left shift. If we rotate b5
left by one bit we get 6b
. If we the xor that with 2e
we get 45
. So the pattern seems to be this:
m <<= 1
if (m & 100)
m ^= 12f
This looks very much like an 8-bit CRC with polynomial 0x2f.
Since we only have mask values for the low four bits, we can only guess that the next four bits also have this same pattern, so the extended tables for each would look like this:
01 0e b5
02 1c 45
04 38 8a
08 70 3b
10 e0 76
20 ef ec
40 f1 f7
80 cd c1
We can keep going:
0100 b5 ad
0200 45 75
0400 8a ea
0800 3b fb
1000 76 d9
2000 ec 9d
4000 f7 15
8000 c1 2a
It's interesting that the mask values for data set 1, if extended beyond one byte, exactly match the values for data sets 2 and 3. This suggests that the mask values may be positional. If we start with the all zeroes message in data set 2:
03 C0 66 00 00 00
And then try this theory with an arbitrary message from data set 3, starting with the initial value of 66:
03 C0 15 09 23 00
Starting from the back we have 00, which contributes nothing so we still have 66. For 23 we use the first set of mask values:
23 => ef ^ 1c ^ 0e = fd
66 ^ fd = 9b
For the 09 we use the next set of mask values:
09 => 3b ^ b5 = 8e
9b ^ 8e = 15
This works and gives us the correct value. So now we're only left with the question of the initial value 66 or in the case of the shorter messages, 39.
What if we work backwards from the initial mask value of 0e? Assuming that other data byte and then the address bytes are processed, we need to calculate three bytes worth of mask values. To go backwards we use this:
if (m & 1)
m ^= 12f
m >= 1
800000 07
400000 94
200000 4a
100000 25
080000 85
040000 d5
020000 fd
010000 e9
008000 e3
004000 e6
002000 73
001000 ae
000800 57
000400 bc
000200 5e
000100 2f
000080 80
000040 40
000020 20
000010 10
000008 08
000004 04
000002 02
000001 01
That's very interesting, because the first byte's value is simply itself, which would make programming this very simple.
The question
Now that we have that worked out, how do the initial values get calculated? We have three 00 values from the three data sets. Perhaps the table above that we just calculated could be used to derive the initial value based on the address and an additional zero byte?
04 92 39 00 00
03 C0 66 00 00 00
We don't know the order of the address bytes, so we calculate both ways:
0492 => 2e nope
9204 => 17 nope
03c0 => b1 nope
c003 => 06 nope
Apparently it's not that simple. Perhaps the address is preprocessed by xoring it with some fixed value? If that's the case, then the xor of the two addresses should produce a value that can be used too obtain the initial value.
0492 ^ 03c0 = 752
However, nothing seems to work with that approach either.
One possibility I haven't tried yet is that the initial value is a CRC8 of the address, but that's just a guess.
Update: I tried it and that's exactly what it was. The bytes are processed sequentially from just after the checksum to the end of the message and then finally the XOR'd bytes of the ID.
The code
#include <cstdint>
#include <initializer_list>
#include <iostream>
#include <numeric>
#include <vector>
class PorscheCanMessage : public std::vector<std::uint8_t>
{
public:
PorscheCanMessage(std::initializer_list<std::uint8_t> l);
std::uint8_t checksum() const;
};
static std::vector<PorscheCanMessage> samples{
{0x04,0x92,0x39,0x00,0x00},
{0x04,0x92,0x37,0x01,0x00},
{0x04,0x92,0x25,0x02,0x00},
{0x04,0x92,0x2B,0x03,0x00},
{0x04,0x92,0x01,0x04,0x00},
{0x04,0x92,0x0F,0x05,0x00},
{0x04,0x92,0x1D,0x06,0x00},
{0x04,0x92,0x13,0x07,0x00},
{0x04,0x92,0x49,0x08,0x00},
{0x04,0x92,0x47,0x09,0x00},
{0x04,0x92,0x55,0x0A,0x00},
{0x04,0x92,0x5B,0x0B,0x00},
{0x04,0x92,0x71,0x0C,0x00},
{0x04,0x92,0x7F,0x0D,0x00},
{0x04,0x92,0x6D,0x0E,0x00},
{0x04,0x92,0x63,0x0F,0x00},
{0x03,0xC0,0x66,0x00,0x00,0x00},
{0x03,0xC0,0xD3,0x01,0x00,0x00},
{0x03,0xC0,0x23,0x02,0x00,0x00},
{0x03,0xC0,0x96,0x03,0x00,0x00},
{0x03,0xC0,0xEC,0x04,0x00,0x00},
{0x03,0xC0,0x59,0x05,0x00,0x00},
{0x03,0xC0,0xA9,0x06,0x00,0x00},
{0x03,0xC0,0x1C,0x07,0x00,0x00},
{0x03,0xC0,0x5D,0x08,0x00,0x00},
{0x03,0xC0,0xE8,0x09,0x00,0x00},
{0x03,0xC0,0x18,0x0A,0x00,0x00},
{0x03,0xC0,0xAD,0x0B,0x00,0x00},
{0x03,0xC0,0xD7,0x0C,0x00,0x00},
{0x03,0xC0,0x62,0x0D,0x00,0x00},
{0x03,0xC0,0x92,0x0E,0x00,0x00},
{0x03,0xC0,0x27,0x0F,0x00,0x00},
{0x03,0xC0,0x9B,0x00,0x23,0x00},
{0x03,0xC0,0x2E,0x01,0x23,0x00},
{0x03,0xC0,0xDE,0x02,0x23,0x00},
{0x03,0xC0,0x6B,0x03,0x23,0x00},
{0x03,0xC0,0x11,0x04,0x23,0x00},
{0x03,0xC0,0xA4,0x05,0x23,0x00},
{0x03,0xC0,0x54,0x06,0x23,0x00},
{0x03,0xC0,0xE1,0x07,0x23,0x00},
{0x03,0xC0,0xA0,0x08,0x23,0x00},
{0x03,0xC0,0x15,0x09,0x23,0x00},
{0x03,0xC0,0xE5,0x0A,0x23,0x00},
{0x03,0xC0,0x50,0x0B,0x23,0x00},
{0x03,0xC0,0x2A,0x0C,0x23,0x00},
{0x03,0xC0,0x9F,0x0D,0x23,0x00},
{0x03,0xC0,0x6F,0x0E,0x23,0x00},
{0x03,0xC0,0xDA,0x0F,0x23,0x00},
};
PorscheCanMessage::PorscheCanMessage(std::initializer_list<std::uint8_t> l) {
reserve(l.size());
insert(end(), l.begin(), l.end());
}
const std::uint8_t crc(std::uint8_t crc, std::uint8_t data) {
constexpr uint8_t poly{0x2f};
crc ^= data;
for (unsigned bits{8}; bits; --bits) {
if (crc & 0x80) {
crc = (crc << 1) ^ poly;
} else {
crc <<= 1;
}
}
return crc;
}
std::uint8_t PorscheCanMessage::checksum() const {
auto it{cbegin() + 3};
uint8_t crcval = 0xff;
crcval = std::accumulate(cbegin() + 3, cend(), 0xff, crc);
uint8_t combined_id{static_cast<uint8_t>(at(0) ^ at(1))};
crcval = crc(crcval, combined_id);
return crcval ^ 0xff;
}
int main() {
for (const auto& m : samples) {
std::cout << std::hex
<< ", message: " << static_cast<unsigned>(m.at(2))
<< ", calculated: " << static_cast<unsigned>(m.checksum())
<< '\n';
}
}