I won't explain it like you are 5-years old because it would take to
much effort, you are a grown up, and you are capable of reading articles
yourself. So start by putting aside ChatGPT, and instead read the
Wikipedia article Two's complement. I mean, read carefully,
taking all the time you need to reach a complete understanding.
Now, the number −3556 would be written in binary as either
11110010.00011100
or
11111111.11111111.11110010.00011100
depending on whether your Arduino uses 16-bit or 32-bit integers (it
varies across Arduino boards). The assignments to bytes[0] and
bytes[1] extract the two least-significant bytes of this number:
bytes[0] = 11110010 (in binary)
bytes[1] = 00011100 (in binary)
The assignment to celciusInt reconstructs the number by ORing together
three terms:
11111111.11111111.00000000.00000000 = (bytes[0] & 0x80 ? 0xFFFF0000 : 0)
00000000.00000000.11110010.00000000 = bytes[0] << 8
00000000.00000000.00000000.00011100 = bytes[1]
───────────────────────────────────
11111111.11111111.11110010.00011100 → assigned to celciusInt
Note that the first term in sign extension (you know what this is,
because you read the article I mentioned, didn't you?). It is only
needed if you want the result as a 32-bit integer. It would be simpler
to forego sign extension and instead store the result in a 16-bit
integer:
int16_t celciusInt = uint16_t(bytes[0]) << 8 | bytes[1];
Note that the code may work even without the cast of bytes[0] to
uint16_t: this cast is only needed to avoid signed integer overflow,
which in C++ is undefined behavior.
Edit: Note that a yet simpler way to split and reconstruct the
number is to use a union. This lets you have a 16-bit integer and an
array of two bytes share the same memory. You can then write to the
integer and read back the bytes or vice-versa:
// A 16-bit integer and a pair of bytes sharing their memory.
union {
int16_t value;
uint8_t bytes[2];
} packet;
// Split a 16-bit integer in two bytes.
packet.value = -3556;
Serial.println(packet.bytes[0]); // -> 28
Serial.println(packet.bytes[1]); // -> 242
// Reconstruct a 16-bit integer from its bytes.
packet.bytes[0] = 214;
packet.bytes[1] = 255;
Serial.println(packet.value); // -> -42
Beware of endianness though: unlike in the previous code, bytes[0]
here is the least-significant byte. Practically every Arduino and
computer in use today is little-endian, so this may not be an issue.
However, there are some communication protocols that are big-endian, so
you should be aware of the issue.