# Contributing to ImudClient

Thanks for considering a contribution. This is a small, focused library —
most of the value is in the parser being *exactly* right, so precision
matters more than breadth here.

For anything beyond a small, obvious fix, please open an issue first to
discuss the approach before writing code.

## Development setup

You'll need [PlatformIO Core](https://platformio.org/install/cli)
(`pip install -U platformio`); no Arduino IDE or hardware is required for
the test suite.

```sh
git clone https://github.com/richcreations/imud-arduino.git
cd imud-arduino
pio test -e native          # run the parser unit tests
```

To sanity-check an example against a real board's toolchain without
flashing hardware:

```sh
pio ci examples/TcpBasic/TcpBasic.ino --lib="." --board=esp32dev
```

To try a sketch against a live (fake) server, see
[tools/fake_daemon.py](tools/fake_daemon.py) and the
[Testing](README.md#testing) section of the README.

## Coding style

- Everything lives in `src/ImudClient.h` — the library is header-only by
  design (see the README for why). Keep it that way; don't split it into
  multiple headers or add a `.cpp`.
- `ImudParser` must stay Arduino-free: no `Arduino.h`, no `Client`/`UDP`,
  no I/O. That's what lets it be unit-tested on a host PC with no mocks —
  it's guarded behind `#ifdef ARDUINO` for a reason.
- No heap allocation, no `String`, no exceptions, no RTTI in `src/`.
  Fixed buffers only. (Examples are ordinary Arduino sketches and may use
  `String`, `Serial.printf`, etc. as normal — the constraint is on the
  library, not on code that consumes it.)
- 4-space indentation, matching brace style to the existing code. Every
  source file starts with the MIT + SPDX header block — copy it from an
  existing file rather than retyping it.
- Don't add abstractions, config options, or "just in case" flexibility
  beyond what a change actually needs.

## Testing requirements

- Any change to `ImudParser`'s behavior needs a native unit test in
  `test/test_parser/test_parser.cpp`. `pio test -e native` must pass
  before a PR is merged.
- Run the sanitizer build too: `pio test -e native-asan` re-runs the same
  suite under ASan/UBSan with `-Werror` and `-DIMUD_ENABLE_ASSERTS`. CI runs
  both, and the sanitizer job is not advisory.
- Check the repo hasn't drifted out of sync with itself:

  ```
  python3 tools/check_repo_integrity.py
  ```

  It fails the build if an example exists that no CI job compiles, if
  `library.properties` / `library.json` / `CHANGELOG.md` disagree on the
  version, if the byte arrays in `test_parser.cpp` no longer match
  `extras/golden/*.hex`, if a public symbol is missing from `keywords.txt`,
  or if a documentation link is broken. Each of those produces a *green*
  build otherwise, which is why they're checked explicitly.

### Fuzzing

`ImudParser` reads untrusted bytes off a socket, so it is fuzzed
continuously — 60 s on every PR and 15 min nightly
(`.github/workflows/fuzz.yml`). If you change `feed()`, `resyncScan()` or
`validate()`, run it locally first:

```
clang++ -std=c++14 -g -O1 -fsanitize=fuzzer,address,undefined \
        -DIMUD_ENABLE_ASSERTS -Isrc -o fuzz test/fuzz/fuzz_parser.cpp
./fuzz test/fuzz/corpus -max_total_time=120
```

Note that Apple clang does **not** ship libFuzzer. On macOS, build the same
file with `-DIMUD_FUZZ_REPLAY` instead to replay the corpus without it.

Two things about this are load-bearing and easy to break:

- **`-DIMUD_ENABLE_ASSERTS` is not optional.** `ImudParser::buf_` is followed
  by padding, so a small overrun of it is an *intra-object* write into slack
  space that AddressSanitizer cannot see. Without the assertions a planted
  off-by-one in `resyncScan()` fuzzes completely clean.
- **The seed corpus is not optional either.** A valid packet needs a correct
  32-bit CRC, which random mutation will never produce, so an unseeded fuzzer
  only ever exercises the reject paths.

If the fuzzer finds something, CI uploads the offending input as an artifact.
Commit it to `test/fuzz/corpus/` (as hex, matching the convention there) so it
becomes a permanent regression case.
- Golden vectors (`extras/golden/*.hex`, mirrored from imud's `lib/golden/`)
  are generated by, and verified against, imud's reference Python client —
  don't hand-edit the hex files. If you need a new fixture (e.g. to cover
  a new edge case), generate it programmatically and document the
  expected values the way `valid_packet.md` does, so the next person can
  verify it independently rather than trusting the bytes on faith.
- If you touch `ImudClient` (the Arduino wrapper) rather than just the
  parser, note in your PR what hardware/core you tested on, if any — CI
  only proves it *compiles* for the listed boards, not that it behaves
  correctly against a real transport.

## Known CI gaps

- **RP2040 / Pico W isn't compiled in CI.** The plan for this library
  originally targeted a PlatformIO board ID `rpipicow`, but as of writing
  that ID doesn't exist in PlatformIO's official registry — only `pico`
  (plain RP2040, no wireless chip) and `nanorp2040connect` (RP2040 with a
  WiFiNINA module, a different API than `WiFi.h`) are available under the
  `raspberrypi` platform. Neither represents the actual target (an
  RP2040 board with a CYW43 chip running the Earle Philhower `arduino-pico`
  core's `WiFi.h`-compatible stack), so rather than have CI "verify"
  against the wrong hardware, this target is dropped from
  `.github/workflows/ci.yml` until a real Pico W board definition is
  available. `examples/*.ino` already branch on `ARDUINO_ARCH_RP2040` to
  use `WiFi.h` there (matching that core's actual API), but this is
  untested by CI — if you have real Pico W hardware, manual verification
  and a PR restoring CI coverage (once a usable board ID exists) would be
  very welcome.
- Similarly, `d1_mini` (ESP8266) is compiled in CI but marked
  best-effort/`continue-on-error` — the examples branch on `ESP8266` for
  its differently-named `ESP8266WiFi.h` header and its 3-argument
  `WiFiUDP::beginMulticast()`, but this core isn't exercised against real
  hardware by anyone maintaining this library day-to-day.

## Wire-sync changes

This library pins **wire v18** and intentionally rejects any other
version (see the [wire-sync warning](README.md#wire-sync-warning)). If
you're updating it to track a new imud wire version:

1. Coordinate with the imud repo's `AGENTS.md` wire-sync checklist — that
   repo is the source of truth for the wire format, not this one.
2. Update the struct in `src/ImudClient.h` to match the new layout
   exactly, bump `IMUD_VERSION`, and update `docs/PROTOCOL.md`'s field
   table to match.
3. Regenerate the golden vectors rather than hand-patching the old ones:

   ```
   python3 tools/gen_vectors.py --imud ~/path/to/imud --out extras/golden/
   ```

   Update the `FIELDS` table at the top of that script to match imud's
   `include/types.h` first. It imports imud's own reference Python client and
   refuses to write anything unless every vector round-trips through it, so
   the vectors cannot drift from the daemon — if you get the table wrong it
   fails loudly instead of emitting plausible-looking garbage.

   It also writes `c_arrays.txt`, which replaces the byte arrays at the top of
   `test/test_parser/test_parser.cpp`.
4. **Bump the minor version** in `library.properties`/`library.json` — a
   wire bump is a minor release here, not a major one. Most releases of this
   library *are* wire updates, so bumping major each time would run the
   number up without signalling anything.

   This is consistent with the Semantic Versioning claim in `CHANGELOG.md`:
   semver governs *this library's* public API, and a pure-append wire change
   leaves that API untouched — no signatures change, nothing is removed,
   only new struct fields appear. The incompatibility is between the user's
   **daemon** and their library build, not between two versions of calling
   code. Save major for large feature work or structural changes to the
   library's own API.

5. **Call the compatibility break out prominently in `CHANGELOG.md`** — this
   is what makes the minor bump safe, so it is not optional. State the
   minimum daemon version and which library line still serves the old one.
   The failure mode for a mismatched pair is *silence*, not an error: the
   parser rejects every packet, so the user sees no data and no diagnostic.
   The `[1.2.0]` entry is the pattern to follow.

## Reporting bugs

Please include:

- Board and Arduino core version (e.g. `esp32` core 2.x/3.x, ESP8266 core
  version, etc.).
- Transport in use (TCP or UDP) and, if relevant, whether you're going
  through `fake_daemon.py` or a real `imud` daemon.
- If the bug is a decode issue, a captured hex dump of the offending
  packet(s) if you can get one — the same format as
  `extras/golden/*.hex` (lowercase hex, whitespace-insensitive) is easiest
  to turn into a regression test.

## License

By contributing, you agree that your contributions are licensed under the
MIT License (see [LICENSE](LICENSE)).
