# TPL/2 — Compact Typed Path Line Protocol

A lightweight, text-based protocol for interacting with a hierarchical tree of values on resource-constrained devices.

Four commands: **GET**, **SET**, **LIST**, **MGET**. One line grammar. Tree-native discovery via mandatory `/server` nodes.

---

# 1. Transport

- UTF-8 text over a line-oriented transport (e.g. serial)
- Each **request** is one line terminated by LF (`\n`)
- CRLF MAY be accepted on input; servers SHOULD reply with LF only
- Each **response** is one line, a LIST/MGET block, or (before requests) comment lines

Request:

```
<CMD> <id> [arguments...]
```

Responses:

```
OK <id> <field>...
SAME <id> <field>...
ERR <id> <field>...
ITEM <id> <field>...
END <id>
```

Comment lines (ignored by clients):

```
# <any text>
```

Startup (mandatory):

```
# TPL/2 ready
```

Servers MUST emit `# TPL/2 ready` when ready to accept commands. Servers SHOULD repeat every 500 ms for 3 s after boot.

Rules:

- Tokens separated by single space (`0x20`)
- Tokens MUST NOT contain unencoded spaces or raw newlines
- Clients MUST ignore non-protocol lines
- Clients MUST wait for `# TPL/2 ready` before the first command
- One request at a time

---

# 2. Line grammar

After `TAG` and `id`, **every** token is `key=value`. No bare paths. No repeated command names.

```
OK  3 path=/cfg/mode kind=L acc=rw type=u brief=Mode vrev=2 val=u:1 live=0
ITEM 4 path=/cfg/mode kind=L acc=rw type=u brief=Mode val=u:1 live=0
ERR 7 code=readonly path=/server/proto
SAME 5 vrev=3
```

Clients parse: `tag`, `id`, then all `key=value` fields (skip tokens without `=`).

---

# 3. Encoding

- Paths and command tokens: ASCII-safe
- String field values: percent-encoded UTF-8

Percent encoding: `%HH` (case-insensitive). Required for space (`%20`), percent (`%25`), newline (`%0A`), CR (`%0D`), non-ASCII.

Invalid encoding → `ERR <id> code=badarg`

---

# 4. Paths

- Absolute, start with `/`
- `/` is root
- No trailing slash (except root)
- No `..` or `//`
- Case-sensitive

---

# 5. Typed values

Format: `type:payload` (single token)

| Type | Meaning |
|------|---------|
| `s:` | String (percent-encoded) |
| `u:` | Unsigned integer |
| `i:` | Signed integer |
| `f:` | Float (decimal) |
| `b:` | Boolean `0` or `1` |

---

# 6. Node model

- `kind=B` — branch (has children)
- `kind=L` — leaf (has value)

Leaf fields in responses:

- `type=s|u|i|f|b`
- `live=0|1` — see §8
- `val=<typed>` — when values requested
- `vrev=<int>` — stored leaves only (`live=0`)

Branch fields: `path`, `kind`, `acc`, `brief` (optional)

---

# 7. Mandatory `/server` nodes

Every server MUST expose:

| Path | Type | Access | Description |
|------|------|--------|-------------|
| `/server/proto` | `s:` | `r` | Protocol identifier (`TPL/2`) |
| `/server/name` | `s:` | `rw` | Canonical device name |
| `/server/uptime` | `u:` | `r` | Seconds since boot (`live=1`) |
| `/server/maxline` | `u:` | `r` | Maximum line length |
| `/server/trev` | `u:` | `r` | Root tree revision (`live=1`) |

`/server` is a branch. These five are leaves. No `/server/cmds` — commands are fixed by this spec.

---

# 8. Live vs stored leaves

| | Stored (`live=0`) | Live (`live=1`) |
|--|-------------------|-----------------|
| Examples | `/server/name`, `/cfg/mode` | `/server/uptime`, `/server/trev` |
| `vrev` | Present; bumps on SET | Omitted |
| `GET ifvrev` | Honored → `SAME` | Ignored; always fresh value |
| SET | Allowed if `acc=rw` | `ERR code=readonly` |

---

# 9. Revisions

- `vrev` — stored leaf value changed
- `trev` — subtree structure changed (children added/removed)
- `/server/trev` reflects root `trev`
- Clients compare for equality only

---

# 10. Commands

## GET

```
GET <id> <path> [ifvrev=<n>]
```

Leaf (stored):

```
OK <id> path=<path> kind=L acc=<r|rw> type=<t> brief=<s> vrev=<n> val=<typed> live=0
```

Leaf (live):

```
OK <id> path=<path> kind=L acc=r type=<t> val=<typed> live=1
```

```
SAME <id> vrev=<n>
ERR <id> code=notfound
ERR <id> code=notleaf path=<path>
```

## SET

```
SET <id> <path> <typed-value> [ifvrev=<n>]
```

```
OK <id> path=<path> vrev=<n>
ERR <id> code=notfound path=<path>
ERR <id> code=notleaf path=<path>
ERR <id> code=readonly path=<path>
ERR <id> code=type_mismatch path=<path>
ERR <id> code=out_of_range path=<path>
ERR <id> code=stale path=<path> vrev=<n>
ERR <id> code=badarg
```

## LIST

Lists **direct children** of `<path>`.

```
LIST <id> <path> [values=0|1] [limit=<n>] [cursor=<path>] [iftrev=<n>]
```

Default `values=0` (structure only). Use `GET` for values when `values=0`.

```
OK <id> path=<path> trev=<n> count=<n> next=<path|->
ITEM <id> path=<child> kind=... acc=... [type=...] [brief=...] [val=...] [live=...] [vrev=...]
END <id>
SAME <id> trev=<n>
```

Rules:

- Items in lexicographic byte order of path
- `limit` omitted or `0` = no limit
- Path-based cursor: items where `strcmp(path, cursor) > 0`
- `cursor` must be inside requested subtree → else `code=badarg`
- `next=<path>` = last item this page; `next=-` = done

## MGET

Reads **multiple leaves by explicit path** in one round-trip. Response uses a compact block: `ITEM` lines contain only `path=` and `val=`. Clients must know types from prior discovery (`LIST` / `GET`).

```
MGET <id> <path> [<path> ...]
```

```
OK <id> count=<n> miss=<n>
ITEM <id> path=<path> val=<typed>
ERR <id> code=<code> path=<path>
END <id>
```

Rules:

- 1–N paths per request (server defines max, e.g. 16)
- Paths in **request order**; response lines follow the same order
- Duplicate paths → `ERR <id> code=badarg` (whole request)
- Zero paths → `code=badarg`
- Per-path errors (`notfound`, `notleaf`) appear as `ERR` lines inside the block; other paths still returned
- No `ifvrev` — always returns current values
- `count` = successful `ITEM` lines; `miss` = `ERR` lines in the block

Example:

```
C> MGET 7 /io/ain0 /cfg/mode /server/uptime
S> OK 7 count=3 miss=0
S> ITEM 7 path=/io/ain0 val=f:23.50
S> ITEM 7 path=/cfg/mode val=u:1
S> ITEM 7 path=/server/uptime val=u:42
S> END 7
```

Use **LIST** when all values are direct children of one parent. Use **MGET** for a fixed set of paths anywhere in the tree.

---

# 11. Errors

All errors use `code=`:

| Code | Meaning |
|------|---------|
| `badcmd` | Unknown command |
| `badarg` | Invalid argument |
| `badpath` | Invalid path syntax |
| `notfound` | Path does not exist |
| `notleaf` | GET/SET/MGET on branch |
| `readonly` | SET on read-only or live leaf |
| `type_mismatch` | SET value type wrong |
| `out_of_range` | SET value out of range |
| `stale` | `ifvrev` mismatch |
| `busy` | Server busy |
| `denied` | Permission denied |
| `internal` | Internal error |
| `too_large` | Line/path/value too large |

---

# 12. Example session

```
S> # TPL/2 ready

C> LIST 1 /server values=1
S> OK 1 path=/server trev=1 count=5 next=-
S> ITEM 1 path=/server/maxline kind=L acc=r type=u val=u:128 live=0
S> ITEM 1 path=/server/name kind=L acc=rw type=s val=s:Boiler live=0
S> ITEM 1 path=/server/proto kind=L acc=r type=s val=s:TPL/2 live=0
S> ITEM 1 path=/server/trev kind=L acc=r type=u val=u:1 live=1
S> ITEM 1 path=/server/uptime kind=L acc=r type=u val=u:42 live=1
S> END 1

C> LIST 2 / values=0
S> OK 2 path=/ trev=1 count=3 next=-
S> ITEM 2 path=/cfg kind=B acc=r brief=Configuration
S> ITEM 2 path=/io kind=B acc=r brief=I/O
S> ITEM 2 path=/server kind=B acc=r brief=Server
S> END 2

C> GET 3 /cfg/mode
S> OK 3 path=/cfg/mode kind=L acc=rw type=u brief=Operating%20mode vrev=2 val=u:1 live=0

C> SET 4 /cfg/mode u:2 ifvrev=2
S> OK 4 path=/cfg/mode vrev=3

C> GET 5 /cfg/mode ifvrev=3
S> SAME 5 vrev=3

C> GET 6 /server/uptime
S> OK 6 path=/server/uptime kind=L acc=r type=u val=u:99 live=1

C> MGET 7 /io/ain0 /cfg/mode /server/uptime
S> OK 7 count=3 miss=0
S> ITEM 7 path=/io/ain0 val=f:23.50
S> ITEM 7 path=/cfg/mode val=u:1
S> ITEM 7 path=/server/uptime val=u:99
S> END 7
```
