A network moves bytes and nothing else. The temperature your sensor measured, the name of the greenhouse bench, the command to open a vent: each one crosses the wire as a row of numbers from 0 to 255. Whoever receives them has to know how to turn those numbers back into meaning. This section covers the three ideas that make that possible: how text becomes bytes, how to look at bytes directly, and JSON, the text format most device messages use.
Bytes and text
A byte is a small number, anything from 0 to 255. Every file, message and picture a computer handles is, underneath, a long row of bytes.
Text is letters, digits and symbols, the things people read. To send text, a computer needs an agreed rule that turns each character into bytes and back again. That rule is called an encoding. A byte on its own means nothing. The byte 65 is the letter A only because both sides agreed to read it as text.
The oldest common rule is ASCII. It covers English letters, the digits, punctuation and a space, and gives each one a single byte. Capital A is 65. The digit 0 is 48.
ASCII has no room for é, the degree sign, Chinese or emoji. UTF-8 is the rule that covers all of them. It uses one byte for every ASCII character, exactly as ASCII does. Every other character takes two, three or four bytes. The degree sign takes two. A seedling emoji takes four.
So here is the trap. Counting the characters you see does not tell you how many bytes there are. "21.5 °C" looks like 7 characters, but it is 8 bytes. Memory and message limits are counted in bytes, so a program must count bytes.
This section builds three ideas: bytes, text, and JSON, a text format
for structured data that the last part of the section teaches. A byte is 8 bits, so it holds one of 256 values, 0 to 255, written
in hex as 0x00 to 0xFF. Text is a sequence of characters. An
encoding is the rule that turns each character into bytes and back.
Bytes have no meaning of their own: 0x41 is the letter A under one
encoding, the number 65 to a program reading an unsigned byte, and part
of a larger number to a program reading two bytes at once.
ASCII is the oldest encoding still in daily use. It assigns 128
characters, English letters, digits, punctuation and a few control
codes, to the values 0–127, one byte each. A is 0x41, a is
0x61, the digit 0 is 0x30, a space is 0x20, { is 0x7B.
UTF-8 encodes every character in Unicode, the catalogue of more than 150 000 characters from every writing system plus symbols and emoji. It uses 1 to 4 bytes per character, and its first 128 one-byte codes are exactly ASCII, so plain ASCII text is already valid UTF-8. Other characters take more:
| Character | Unicode code point | UTF-8 bytes | Count |
|---|---|---|---|
C |
U+0043 | 43 |
1 |
° (degree sign) |
U+00B0 | C2 B0 |
2 |
é |
U+00E9 | C3 A9 |
2 |
€ |
U+20AC | E2 82 AC |
3 |
| 🌱 (seedling) | U+1F331 | F0 9F 8C B1 |
4 |
So a character count is not a byte count. 21.5 °C is 7 characters
and 8 bytes. Buffers, payload limits and radio packets are sized in
bytes, so firmware must count the encoded bytes, not the characters a
person sees. UTF-8 is the encoding of the web, of JSON, and of MQTT's
topic names; assume it unless a document says otherwise.
Characters versus bytes
Pick each sample. Before you look at the counts, predict how many bytes it takes in UTF-8. Then type the name of your own greenhouse bench and check.
Loading interactive…
Worked example
Sizing a buffer for a label
-
1
A device stores its display label in a 16-byte buffer, with one byte kept for the end-of-string marker C++ needs. Will
Gewächshaus 2(German for "greenhouse 2") fit? -
2
Count characters. G-e-w-ä-c-h-s-h-a-u-s, a space, 2: 13 characters.
-
3
Count bytes. Every character is ASCII except
ä, which is 2 bytes in UTF-8. So 12 \times 1 + 2 = 14 bytes. -
4
Add the terminator. 14 + 1 = 15 \le 16. It fits, with one byte to spare. A label with two more non-ASCII letters, or one emoji, would not, even though it might still look short.
Hex dumps
Sometimes you need to see the actual bytes a program sent, not the text it was meant to be. A hex dump shows them.
It prints the bytes in rows of 16. Each byte appears as two hex digits.
Hex is counting in sixteens, using the digits 0 to 9 and then the letters
a to f. So 7b is 7 sixteens plus 11, which is 123.
Each row starts with a position number, also in hex, called the offset.
It says how far into the data that row begins, counting from 0. The
second row starts at 00000010, which is 16 in ordinary numbers. To
find any byte's position, add its row's offset to its place along the
row.
On the right, the same bytes are shown as characters, where that makes sense. Anything that isn't a plain letter, digit or symbol shows as a dot. That's how you spot a hidden line break at the end of a message, or a degree sign that arrived broken.
A hex dump shows raw bytes as text, one row per 16 bytes. Each row
starts with an offset, the position of its first byte counted from
0 and written in hex, then the 16 bytes as two hex digits each, then the
same bytes as characters, with a dot for any byte that isn't printable
ASCII. Here are the 18 bytes of the JSON text {"t":21.5,"rh":64}, as
hexdump -C or xxd-style tools print them:
00000000 7b 22 74 22 3a 32 31 2e 35 2c 22 72 68 22 3a 36 |{"t":21.5,"rh":6|
00000010 34 7d |4}|
The second row's offset is 0x10, which is 16: it starts with the
seventeenth byte. A byte's offset is its row's offset plus its column,
so 7d (the closing brace) is at \texttt{0x10} + 1 = \texttt{0x11}, or
17. Reading a hex dump answers questions a printed string hides: whether
a payload ends with an invisible newline (0a), whether a degree sign
arrived as the two bytes c2 b0 or got mangled, whether a number was
sent as text digits (32 31) or as a binary value.
A payload, 16 bytes to a row
Find the byte at offset 0x11 before you hover over it. Then find where the number 21.5 starts and ends.
Loading interactive…
Worked example
Reading digits out of a dump
-
1
A board sends a reading and the receiver's hex dump shows, at offsets
0x05to0x08, the bytes32 31 2e 35. What value was sent, and in what form? -
2
Look each byte up.
0x32is the character2,0x31is1,0x2eis.,0x35is5. Digits in ASCII run from0x30for0, so subtract0x30from each digit byte. -
3
Read the text.
21.5, written as four characters of text, not as a binary number. -
4
Why it matters. A receiver that tried to read those four bytes as one 32-bit number would get nonsense. The bytes are text, and must be parsed as text.
JSON
JSON is a way of writing data as text so that any program can read it. It's short for JavaScript Object Notation, but every language uses it now.
It has a handful of building blocks. Words and names go in double
quotes, like "bench1". Numbers are written plainly, with no quotes and
no units, like 21.5. There are also true, false and null, which
means "no value."
Curly braces make an object: a set of labels, each with a value.
{"t": 21.5, "rh": 64} has two labels. t holds 21.5, the temperature,
and rh holds 64, the relative humidity. A colon goes between a label
and its value, and commas go between pairs.
Square brackets make an array: a list in order, like [21.5, 21.6, 21.4]. Objects and arrays can sit inside each other.
Why do devices use JSON so much? The labels travel with the numbers, so the receiver knows which number is which. Every programming language can read it. And people can read it too, in a log or a hex dump.
The price is size. Those two readings take 18 bytes as JSON. If both sides agreed on a fixed layout in advance, they could fit in 3 bytes.
JSON (JavaScript Object Notation) is a text format for structured data. A JSON document is one value, and a value is one of:
- a string in double quotes:
"bench1"; - a number, with no quotes and no units:
21.5,64,-3; true,falseornull;- an object:
{key-value pairs}, where each key is a string, a colon separates it from its value, and commas separate pairs:{"t": 21.5, "rh": 64}; - an array: an ordered list of values in brackets:
[21.5, 21.6, 21.4].
Objects and arrays nest, so {"bench": "b1", "temps": [21.5, 21.6]}
is one object holding a string and an array. JSON text is UTF-8, keys
are case-sensitive, and whitespace outside strings means nothing, so
{"t":21.5} and { "t": 21.5 } are the same document in 10 and 13
bytes.
JSON is the most common payload in this course and in device systems
generally, for three reasons. It is self-describing: the keys travel
with the values, so a reader can tell t from rh without a separate
format description. Every language this course uses parses it with its
standard library or one well-known package (Python's json, ArduinoJson
on the board). And it is readable in a hex dump and in a log.
Its cost is size. The two values above take 18 bytes as compact JSON. A binary encoding that both sides agree on in advance could send the temperature as a 16-bit integer in hundredths of a degree (2150) and the humidity as one byte: 3 bytes. CBOR (Concise Binary Object Representation) is a standard binary format with JSON's data model that sits between the two; Chapter 10 §6 compares them properly.
// Build a compact JSON payload with ArduinoJson 7 and count its bytes.
#include <ArduinoJson.h>
void setup() {
Serial.begin(115200);
JsonDocument doc;
doc["t"] = 21.5;
doc["rh"] = 64;
char buf[64];
size_t n = serializeJson(doc, buf, sizeof(buf)); // bytes written
Serial.printf("%u bytes: %s\n", (unsigned)n, buf);
}
void loop() {}
On a board this should print 18 bytes: {"t":21.5,"rh":64}. The same
payload in Python, on your computer, is
json.dumps({"t": 21.5, "rh": 64}, separators=(",", ":")).encode("utf-8");
separators drops the spaces that json.dumps adds by default, and
.encode("utf-8") turns the text into the bytes that are actually sent.
In Chapter 9 these bytes travel inside an MQTT message, the message format this course uses most. Here is one, built for this example, so you can see that the payload is the same 18 bytes.
The JSON payload inside a message
Click the payload field and find its first and last bytes in the hex. Then count: how many bytes come before the payload, and what do they hold?
Loading interactive…
Worked example
The size of a day of readings
-
1
A sensor publishes
{"t":21.5,"rh":64}every 10 seconds. How many payload bytes is that per day, as JSON and as the 3-byte binary layout? -
2
Messages per day. 86\,400 / 10 = 8640.
-
3
JSON. 8640 \times 18 = 155\,520 bytes, about 156 kB.
-
4
Binary. 8640 \times 3 = 25\,920 bytes, about 26 kB: six times less.
-
5
Is it worth it? On home Wi-Fi, 156 kB a day is nothing, and JSON's readability wins. On a LoRa link limited to tens of bytes per message and seconds of airtime per hour, the difference decides whether the design works at all. The choice depends on the link, which is why Chapters 7 and 10 return to it.
Worked example
When whitespace and labels cost bytes
-
1
Compare
{"temperature": 21.5, "humidity": 64}with{"t":21.5,"rh":64}. Same data? -
2
Same values, but not the same document. The keys differ, so a receiver expecting
twon't findtemperature. Keys are part of the agreement between sender and receiver. -
3
Count the bytes. The long one is 37 bytes and the short one is 18. The 19 extra bytes are 16 more characters of key names, plus three spaces: two after the colons and one after the comma.
-
4
The habit. Choose short, clear keys once, write them down, and keep the spaces out of payloads a device sends. Pretty-printed JSON is for people reading files.
🛠️ Build it yourself
Lab: a hex dump and a payload sizer in Python
A small Python package with two functions to finish: hexdump(), which
prints bytes 16 to a row with offsets and a character column, and
payload_size(), which counts the UTF-8 bytes of a compact JSON
payload. The tests, already written, check both against the examples
on this page. Nothing touches a network.
What's inside (11 files)
- payload-bytes/.devcontainer/devcontainer.json
- payload-bytes/.vscode/extensions.json
- payload-bytes/.vscode/launch.json
- payload-bytes/.vscode/settings.json
- payload-bytes/.vscode/tasks.json
- payload-bytes/README.md
- payload-bytes/pyproject.toml
- payload-bytes/src/payload_bytes/__init__.py
- payload-bytes/src/payload_bytes/__main__.py
- payload-bytes/src/payload_bytes/dump.py
- payload-bytes/tests/test_dump.py
Unzip it, then make a virtual environment, install pytest with python -m pip install pytest, and run the tests with python -m pytest. The README has the steps for every OS and IDE. In VS Code, open the folder: the test task (Tasks: Run Test Task) and the debugger (F5) are already set up.
Check your understanding (counts toward your progress)
A receiver's hex dump of a payload ends 36 34 7d 0a. What is the last byte?
Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.
Each pair of hex digits is one byte. 0a is one byte, value 10.
36 34 is 64; the brace 7d comes between it and 0a.
A closing brace is 7d, which appears just before.
Why
0x0a is the line feed. A printed string hides it; a hex dump shows it, and a strict receiver may reject it.
0a is 10, a control code, not a printable character.
A device sends readings over a link that allows a few dozen bytes per message and a few seconds of airtime per hour. Which payload format is the better starting point?
Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.
Indentation and long keys multiply the bytes for no gain to a machine reader.
Readable, but keys travel with every message on a link where each byte counts.
Text with units is bigger than JSON's numbers and harder to parse.
Why
When the link is the bottleneck, a fixed binary layout (3 bytes for the section's reading) beats JSON's self-describing 18. On home Wi-Fi, JSON's readability usually wins.
On this link, every byte costs airtime.
In {"bench": "b1", "temps": [21.5, 21.6, 21.4]}, what is the value of temps?
Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.
An object uses braces and key-value pairs. These are brackets and bare values.
The values aren't in quotes, so they are numbers, not text.
The brackets hold all three numbers as one ordered list.
Why
Square brackets make an array, an ordered list of values; here, three numbers.
Square brackets or curly braces?
How many bytes does the text café take in UTF-8?
Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.
Why
c, a and f take 1 byte each; é takes 2 (C3 A9). 3 + 2 = 5 bytes for 4 characters.
Correct answer
Also accepted:
Why
Three of the letters are ASCII. One is not.
Why can a program read plain ASCII text as UTF-8 without converting it?
Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.
UTF-8 uses those bytes: they are its one-byte characters.
Networks move bytes and never convert text.
ASCII is one byte per character, and so is UTF-8 for those same characters.
Why
UTF-8 was designed so that its first 128 codes match ASCII byte for byte. Any ASCII text is already valid UTF-8.
Compare A in both encodings.
Practice: Bytes, hex dumps and JSON (5 problems · doesn’t affect your progress)
A byte holds the value 0x41. What does it mean?
Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.
Only if both sides agreed to read it as text.
0x41 is hex for 65, and even then it's only a number if read as one.
Encryption has nothing to do with meaning.
Why
Bytes carry no meaning on their own. The agreed encoding or format gives them one.
It depends on how the receiver reads it
How many bytes does 5 € (five, a space, the euro sign) take in UTF-8?
Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.
Why
1 + 1 + 3 = 5 bytes for 3 characters.
Correct answer
Also accepted:
Why
The euro sign is one of the three-byte characters.
This is the JSON payload {"t":21.5,"rh":64}, 16 bytes to a row. Click the four bytes that hold the text 21.5, then check.
Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.
What this task checks
Structure checked automatically. Whether your ideas are convincing isn't graded, so reread with the checklist.
The compiler says
A model answer
Why
The number is four ASCII characters at offsets 5–8: 32 31 2e 35.
Correct answer
Also accepted:
Why
Count the bytes before the number: brace, quote, t, quote, colon.
How many bytes is {"temperature": 21.5, "humidity": 64} minus {"t":21.5,"rh":64}?
Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.
Why
37 − 18 = 19: 10 more for temperature, 6 more for humidity, and 3 spaces.
Correct answer
Also accepted:
Why
The short one is 18 bytes. Count the long one, spaces included.
A receiver reads the four bytes 32 31 2e 35 as one 32-bit binary number and gets a huge value. What went wrong?
Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.
Byte order matters for binary numbers. These aren't one.
They decode cleanly as the characters 2, 1, . and 5.
Any width gives nonsense when text is read as a binary number.
Why
32 31 2e 35 is the text 21.5. Sender and receiver must agree on the format, here text inside JSON.
They are text digits and must be parsed as text
Where this goes next
Bytes and hex dumps come back in Chapter 2 §1, where packets are read layer by layer, and in Chapter 2 §8, where you capture your own device's traffic. Chapter 5 §5 builds and validates JSON on the board with ArduinoJson, and Chapter 10 §6 weighs JSON against CBOR and Protobuf. UTF-8 byte counts matter again in Chapter 9 §3, because MQTT limits topic names in bytes.