# Changelog

## [0.6.0]

No public API signature changes.

### Fixed

- `midi2_dispatch_feed` and `midi2_proc_feed` drop a typed message whose
  `word_count` is shorter than its type requires instead of over-reading.
- `midi2_ci` returns `MIDI2_CI_ERR_NULL` (was `MIDI2_CI_ERR_NOT_FOUND`) for a
  NULL `state` or name in `midi2_ci_remove_property`,
  `midi2_ci_pe_set_subscribable`, `midi2_ci_subscribe_add`,
  `midi2_ci_subscribe_remove`, and `midi2_ci_notify_property_changed`.

### Added

- Overridable `MIDI2_ASSERT` macro; compiled out under `NDEBUG`.
- Debug-only single-context reentrancy guard (`in_feed`) on `midi2_conv` and
  `midi2_proc`; compiled out under `NDEBUG`.

### Changed

- Feed preconditions and the single-context contract documented in the
  `midi2_dispatch`, `midi2_proc`, and `midi2_conv` headers.
- Reproducible amalgam (no build-date stamp).

## [0.5.0]

### Added

- Zephyr module support (`zephyr/module.yml`, `CMakeLists.txt`, `Kconfig`).
- Zephyr example for the Raspberry Pi Pico under `examples/rpi-pico-device-zephyr/`.

### Changed

- NULL pointer arguments at public entry points are now no-ops or zero returns.
- `midi2_msg_word_count` covers all 16 message types.
- Internal helper and lookup-table cleanup.

## [0.4.0]

Breaking signature changes across UMP Stream, Channel Voice,
Utility, and Flex Data builders.

### Breaking — UMP Stream (MT 0xF)

- **`midi2_msg_stream_config_request(w, protocol)` →
  `midi2_msg_stream_config_request(w, protocol, rx_jr_enable, tx_jr_enable)`**.
  Status 0x05 (M2-104-UM §7.1.4). Bits [1:0] of word 0 carry
  `JR_TX_ENABLE` (bit 0) / `JR_RX_ENABLE` (bit 1). Used host→device to
  request a session configuration; the dispatcher already decoded these
  bits, the builder did not emit them.

- **`midi2_msg_stream_config_notify(w, protocol)` →
  `midi2_msg_stream_config_notify(w, protocol, rx_jr_enable, tx_jr_enable)`**.
  Status 0x06 (M2-104-UM §7.1.5). Same bit layout as Config Request.
  Without these, devices running a JR Timestamp heartbeat (e.g. enabled
  through `midi2_proc_enable_jr_heartbeat`) showed `Sending JR
  Timestamps: False` upstream even though the wire traffic was correct.

- **`midi2_msg_stream_fb_info(w, active, fb_num, direction, first_group,
  num_groups, midi_ci_ver, sysex8, protocol)` →
  `midi2_msg_stream_fb_info(w, active, fb_num, direction, ui_hint,
  first_group, num_groups, midi_ci_ver, max_sysex8_streams, protocol)`**.
  Status 0x11 (M2-104-UM §7.1.3). Two fields filled in:
  - `ui_hint` (bits [5:4] of word 0): `0x00` Undeclared, `0x01`
    Receiver, `0x02` Sender, `0x03` Sender + Receiver. Hosts use this
    to pick FB icons / labels in their port pickers.
  - `max_sysex8_streams` (bits [7:2] of word 1 byte 2): replaces the
    `bool sysex8` reducer with the spec's 6-bit count (0..63
    concurrent streams). Pass `0` for "SysEx8 not supported" and `1`
    for "supports SysEx8" to match the prior bool behaviour exactly.

- **`midi2_dp_fb_info_cb` callback signature** gains `ui_hint` between
  `direction` and `first_group`. Recipes wiring `on_fb_info` directly
  must update their handler signature.

- **`midi2_dispatch.c on_fb_info` trampoline** decodes `ui_hint` from
  bits [5:4] and forwards it through the callback.

### Breaking — Utility (MT 0x0)

- **`midi2_msg_jr_clock(group, timestamp)` →
  `midi2_msg_jr_clock(timestamp)`** and **`midi2_msg_jr_timestamp(group,
  timestamp)` → `midi2_msg_jr_timestamp(timestamp)`**. Per M2-104-UM
  v1.1.2 section 1.4 the former Group field on Utility messages is now
  Reserved (must be zero). Removing the parameter prevents silently
  writing a non-zero Group on the wire from existing recipes.

- **New: `midi2_msg_noop(void)`** builder for the NOOP Utility message
  (status 0x0, all-zero payload). Trivial, but completes coverage of
  the five Utility statuses.

- **`midi2_dp_jr_clock_cb` and `midi2_dp_jr_timestamp_cb` typedefs**
  drop the leading `uint8_t group` parameter. Symmetric with the
  builder change: the wire bits are reserved, the dispatcher no
  longer parses them, and the callbacks no longer surface them.
  Callers that wired `on_jr_clock` / `on_jr_timestamp` directly must
  update the handler signature to `(uint16_t timestamp, void *ctx)`.

### Breaking — MIDI 2.0 Channel Voice (MT 0x4)

- **`midi2_msg_note_on(w, group, channel, note, velocity, attribute)` →
  `midi2_msg_note_on(w, group, channel, note, velocity, attr_type,
  attr_data)`** (and same for `midi2_msg_note_off`). The old single
  `uint16_t attribute` parameter folded the 8-bit `Attribute Type` and
  16-bit `Attribute Data` into a 16-bit slot, truncating Pitch7_9 and
  other 16-bit-wide attribute payloads to 8 bits. The dispatcher
  callback already exposed them as `(uint8_t attr_type, uint16_t
  attr_data)`, so the builder now matches.

- **`midi2_msg_program(...)` Bank Valid bit relocates** from bit 31 of
  word 1 (where it was overwriting byte 5's reserved MSB) to bit 0 of
  word 0's byte 4 (the spec's "option flags" lane per M2-104-UM section
  7.4.9). API signature unchanged; on-the-wire bit position fixed. The
  dispatch parser was updated symmetrically. Cross-checked against
  AM_MIDI2.0Lib `mt4ProgramChange`.

### Breaking — Flex Data (MT 0xD)

- **`midi2_msg_time_sig(w, group, numerator, denominator)` →
  `midi2_msg_time_sig(w, group, numerator, denominator,
  num_32nd_notes)`**. Per M2-104-UM section 7.5.4 the Set Time
  Signature message carries three numeric fields, not two: numerator,
  denominator (negative power of 2), and the SMF-compat "Number of 1/32
  Notes per 24 MIDI Clocks". The builder was emitting only the first
  two and leaving byte 3 of word 1 at zero. Default value per SMF1: 8.

### Tests

- `test_stream_config_request_jr_bits` — asserts JR bits in Stream
  Config Request.
- `test_stream_config_notify_jr_bits` — asserts JR bits in Stream
  Config Notify.
- `test_stream_fb_info_ui_hint` — asserts `ui_hint` and `direction`
  occupy independent bit lanes (any combination, any FB).
- `test_stream_fb_info_sysex8_streams` — asserts
  `max_sysex8_streams` covers the full 0..63 range.
- `test_dp_config_request_jr_bits`, `test_dp_config_notify_jr_bits`
  — round-trip builder→dispatcher preserves both JR flags.
- `test_dp_fb_info` — re-asserts the round-trip with `ui_hint=0x03`.
- `test_jr_clock` / `test_jr_timestamp` — asserts the Group field is
  reserved/zero per the Groupless rule.
- `test_noop` — covers the new builder.
- `test_program_change_bank` / `_no_bank` — assert that the Bank Valid
  bit lives in byte 4 option flags and byte 5 MSB stays reserved.
- `test_dp_note_on` adds `attr_type` / `attr_data` assertions to lock
  in 16-bit attribute_data round-tripping.
- `test_time_sig` asserts the `num_32nd_notes` SMF-compat byte.

Total: 328 assertions across the 8 host test binaries, ASan + UBSan
clean.

### Migration

Default-safe values reproduce the prior behaviour on the wire:

```c
midi2_msg_stream_config_request(w, protocol, /*rx_jr*/ false, /*tx_jr*/ false);
midi2_msg_stream_config_notify (w, protocol, /*rx_jr*/ false, /*tx_jr*/ false);
midi2_msg_stream_fb_info(w, active, fb_num, direction, /*ui_hint*/ 0x00,
                         first_group, num_groups, midi_ci_ver,
                         /*max_sysex8_streams*/ 0, protocol);
```

Recipes whose device actively emits JR Timestamps (typical when
`enableJRHeartbeat()` is on) should pass `tx_jr_enable=true`. Recipes
with a known FB role should declare `ui_hint` (`0x02` for senders,
`0x03` for bidirectional devices, `0x01` for pure listeners). Recipes
that previously passed `sysex8=true` now pass `max_sysex8_streams=1`.

For Utility / Channel Voice / Flex Data callers:

```c
/* old */ midi2_msg_jr_clock(group, ts);        /* new */ midi2_msg_jr_clock(ts);
/* old */ midi2_msg_jr_timestamp(group, ts);    /* new */ midi2_msg_jr_timestamp(ts);
/* old */ midi2_msg_note_on(w, g, ch, n, v,     /* new */ midi2_msg_note_on(w, g, ch, n, v,
                            (type<<8)|data8);                            type, data16);
/* old */ midi2_msg_time_sig(w, g, num, denom); /* new */ midi2_msg_time_sig(w, g, num, denom, 8);
```

Program Change keeps the same C signature; only the on-the-wire bit
moves. No recipe code change required there.

The repo's own consumer tests, AVR heredoc, and `examples/` C +
Arduino sketches were updated in the same release so `find_package`
consumers, the AVR cross-compile job, and the on-tree learning
material all build out of the box on v0.4.0. Recipes outside this
repo (midi2cpp, libDaisy fork, Teensy core fork) follow in their
own bumps.

## [0.3.4]

Fix the ESP-IDF Component Manager path. v0.3.3 routed
`idf_component_register` at `dist/midi2.c`, but the Component Manager
filters `dist/` out of the dependency tarball as a build-output
convention, so consumers saw `Include directory ... is not a
directory` at configure time.

### Fixed

- **ESP-IDF gate** in `CMakeLists.txt` now consumes the modular
  `src/midi2_proc.c`, `src/midi2_dispatch.c`, `src/midi2_conv.c`,
  `src/midi2_ci.c`, `src/midi2_ci_dispatch.c` (the source set that
  the Component Manager actually delivers). The header-only modules
  (`midi2_msg.h`, `midi2.h`, `midi2_ci_msg.h`) need no entry. Native
  CMake and PlatformIO consumers, which still build the amalgamated
  `dist/midi2.c`, are unaffected.

### Changed

- `idf_component.yml` example snippet pinned to `v0.3.4` (literal
  git tag, not a semver constraint, since `version:` for git
  dependencies is a git ref).

## [0.3.3]

ESP-IDF Component Manager support. midi2 can now be pulled into an
ESP-IDF project tree directly via the Component Manager:

```yaml
# main/idf_component.yml
dependencies:
  midi2:
    git: https://github.com/sauloverissimo/midi2.git
    version: ">=0.3.3"
```

The Component Manager clones midi2 under `managed_components/`,
ESP-IDF picks the directory up as a regular component (the top-level
`CMakeLists.txt` detects `ESP_PLATFORM` and routes to
`idf_component_register`). Native CMake consumers (Pico SDK, vcpkg,
FetchContent, system installs, etc.) are unaffected: the `if
(ESP_PLATFORM) ... return()` guard sits before `project()` so every
non-IDF path runs unchanged.

### Added

- **`idf_component.yml`** at the repo root with the component
  manifest (description, version, license, maintainers, tags).
- **ESP-IDF gate** at the top of `CMakeLists.txt` that calls
  `idf_component_register(SRCS dist/midi2.c INCLUDE_DIRS dist)`
  when `ESP_PLATFORM` is defined and returns before the native CMake
  build runs.

## [0.3.2]

CMake target + downstream package consumption. midi2 now ships an
official CMake build alongside the existing Makefile, so consumers can
pick the idiom that fits: `find_package(midi2 0.3.2 CONFIG)` for system
installs / vcpkg / conan, `FetchContent_MakeAvailable` on the GitHub
repo, or `add_subdirectory` of a vendored copy.

### Added

- **`CMakeLists.txt`** at repo root: builds `dist/midi2.c` into
  `libmidi2.a`, exposes the `midi2::midi2` alias target, declares
  `c_std_99` as a public compile feature, mirrors the Makefile's
  `-Wall -Wextra -Wpedantic` warnings on GCC/Clang/AppleClang.
- **install + export ruleset**: `install(EXPORT midi2-targets ...)`
  generates `midi2-config.cmake` and `midi2-config-version.cmake`
  (SameMajorVersion compatibility) so downstream
  `find_package(midi2 0.3.2 CONFIG)` succeeds. Install rules are gated
  by `PROJECT_IS_TOP_LEVEL` so subprojects pulling midi2 via
  FetchContent do not pollute their parent's install set.
- **`tests/cmake-consumer/`**: real downstream smoke
  (`CMakeLists.txt` + `main.c`) that the CI compiles, links against
  the imported `midi2::midi2` target, and runs.
- **CI `cmake-build` matrix** on `ubuntu-latest`, `macos-latest`,
  `windows-latest`. Configure -> build -> install -> downstream
  `find_package(midi2)` consumer compiled and run.
- **CI `cmake-fetchcontent`**: synthetic parent project pulls midi2
  via `FetchContent_Declare` and links `midi2::midi2` without an
  install step.
- **CI `cmake-build-sanitizers`**: ASan + UBSan parity for the
  amalgam build path.
- `architecture.png` referenced by the README.

### Fixed

- Makefile amalgam test isolated from `-I src` so the new
  `src/midi2.h` umbrella (added in v0.3.1) does not shadow
  `dist/midi2.h`. Without this fix the amalgam test compiled but
  failed to link.

## [0.3.1]

Arduino library compliance and Library Manager registration. No
runtime changes; same UMP and MIDI-CI surface as v0.3.0.

### Added

- **`library.properties`** for the Arduino Library Manager.
- **`src/midi2.h` umbrella** so Arduino IDE sketches can
  `#include <midi2.h>` and pick up every modular module in `src/`.
- **`examples/BasicUsage/BasicUsage.ino`** and
  **`examples/CIDiscovery/CIDiscovery.ino`**: two reference sketches
  the Arduino IDE surfaces under File > Examples after install.
- README install sections for Arduino IDE / arduino-cli and
  PlatformIO.

### Registered

- midi2 published on the Arduino Library Manager via PR
  `arduino/library-registry#8284` (merged 2026-05-07). Available in
  the Library Manager: search `midi2`.

## [0.3.0]

Property Exchange Subscribe/Notify state machine, UMP Stream and
SysEx8 fragmenters, registry symmetry, MT 0x4 to MT 0x2 downgrade,
and USB MIDI 1.0 cable-event helper. First release since v0.2.4
with a breaking struct change (semver minor bump justified); callers
using designated-initialiser style compile unchanged. Stays embedded
contract: only `<stdint.h>`, `<stdbool.h>`, `<string.h>` (no `<stdio.h>`,
no `snprintf`, no float formatter dragged in).

### Added

- **Subscribe / Notify state machine** (`midi2_ci_subscribe_add`,
  `_subscribe_remove`, `_notify_property_changed`,
  `_pe_set_subscribable`, `_get_subscriber_count`) per M2-101-UM
  sections 8.11 to 8.13. A caller-provided subscriber registry
  arrives via the new `midi2_ci_init_ex(...)`; legacy
  `midi2_ci_init()` stays source-compatible and delegates with a
  NULL subscriber array. Notify header is built with `memcpy` of a
  fixed JSON template, no `snprintf`.
- **`midi2_ci_remove_property`** and **`midi2_ci_reset_profiles` /
  `_reset_properties`**: symmetric registry helpers.
- **`midi2_ci_set_auto_invalidate_on_collision(state, enabled)`**
  (default on). Exposes the Invalidate MUID broadcast that the
  collision detector in v0.2.4 already emitted; apps can now opt
  out per M2-101-UM section 5.9 if they prefer manual control.
- **UMP Stream and SysEx8 fragmenters in `midi2_proc`**:
  `midi2_proc_send_endpoint_name` (status 0x003),
  `midi2_proc_send_product_id` (status 0x004),
  `midi2_proc_send_device_identity` (status 0x002, single UMP), and
  `midi2_proc_send_sysex8` (MT 0x5, 13 bytes per UMP). All delegate
  to the canonical single-packet builders in `midi2_msg.h`.
- **`midi2_msg_mt4_to_mt2`** inline helper: inverse of the existing
  `midi2_msg_mt2_to_mt4`. Bit-scales MIDI 2.0 Channel Voice down to
  MIDI 1.0 per M2-115. Lossy by spec: RPN / NRPN / per-note are
  dropped (no MIDI 1.0 single-word form). Returns 0 or 1; caller
  detects drops by counting.
- **`midi2_msg_cable_event_to_ump`** inline helper: USB MIDI v1.0
  4-byte cable event to UMP MT 0x2. Channel Voice (CIN 0x8-0xE) and
  System Common (CIN 0x2-0x3) handled. SysEx fragment CINs (0x4-0x7,
  0xF) and reserved CINs return false; SysEx fragments belong in
  `midi2_conv` (stateful).
- **`midi2_msg_set_group`** inline helper: rewrites word 0 Group
  nibble on MT 0x2 to 0x5, leaves Utility / System / Flex Data /
  Stream alone.
- **MT 0x1 System message wrappers** (10 inline helpers): named
  shortcuts wrapping `midi2_msg_system{,_2byte,_3byte}` with the
  canonical status byte for each MIDI 1.0 System Common and System
  Real-Time message per M2-104-UM section 4.3.
  - `midi2_msg_system_tune_request(group)` (0xF6)
  - `midi2_msg_system_timing_clock(group)` (0xF8)
  - `midi2_msg_system_start(group)` (0xFA)
  - `midi2_msg_system_continue(group)` (0xFB)
  - `midi2_msg_system_stop(group)` (0xFC)
  - `midi2_msg_system_active_sensing(group)` (0xFE)
  - `midi2_msg_system_reset(group)` (0xFF)
  - `midi2_msg_system_mtc(group, time_code)` (0xF1, 1 data byte)
  - `midi2_msg_system_song_select(group, song)` (0xF3, 1 data byte)
  - `midi2_msg_system_song_position(group, position)` (0xF2, 14-bit
    split as LSB/MSB)

  Pure DX, zero ROM cost (header inline). Removes the cargo cult of
  memorising magic status bytes (0xF1..0xFF) at the call site.

### Changed

- **Breaking:** `midi2_ci_property` gains a trailing `subscribable`
  bool. Designated-initialiser call sites compile unchanged;
  positional inits must append `false`.
- `midi2_ci_state` gains subscriber / auto_invalidate_on_collision
  fields. Documented as opaque, so this is non-breaking.
- `midi2_ci_add_property_static` / `_add_property_dynamic` now reset
  the new `subscribable` field so reused caller-provided slots start
  clean.

### Migration

Two paths for the breaking `midi2_ci_property` change:

```c
/* Before (positional init): */
midi2_ci_property props[] = {
  { "DeviceName", "MySynth", NULL, NULL }
};

/* After (either form works): */
midi2_ci_property props[] = {
  { "DeviceName", "MySynth", NULL, NULL, false }   /* explicit */
};
midi2_ci_property props[] = {
  { .name = "DeviceName", .static_value = "MySynth" }  /* designated */
};
```

Existing `midi2_ci_init` users see zero behaviour change; the
subscriber registry stays NULL. Opt in via `midi2_ci_init_ex`.

### Test suite

Full suite: **321 test functions across 8 suites**, all green.
Baseline v0.2.4 was 266 functions across 8 suites; this release
adds 55 net new test functions across the existing suites covering
the new public surface (Subscribe/Notify, fragmenters, MT4 to MT2
downgrade, USB cable event helper, registry symmetry, System
message wrappers). gcc -std=c99 -Wall -Wextra -Wpedantic clean,
ASan and UBSan clean.

## [0.2.4]

Convenience responder completeness. Every spec-required behavior from
M2-101-UM Appendix E is now handled inside `midi2_ci_process_sysex()`.
Applications no longer need to copy MUID regeneration, collision
detection, NAK-on-unknown, or PE Capability boilerplate. **Additive,
zero breaking change**: new state fields zero-initialise via
`midi2_ci_init()` and every new behavior is opt-in.

### Added

- **`midi2_ci_set_rng(state, rng_fn, ctx)`**: platform RNG injection.
  Unlocks automatic MUID regeneration for the next two features.
- **`midi2_ci_new_muid(state)`**: helper that returns a fresh 28-bit
  MUID avoiding the reserved values `0x00000000` and `0x0FFFFFFF`
  (broadcast). Falls back to a deterministic perturbation if no RNG
  installed, so callers always get a usable value.
- **Automatic Invalidate MUID (Sub-ID#2 0x7E) handling** inside
  `midi2_ci_process_sysex()`. When the incoming message's target MUID
  equals ours and an RNG is set, state->muid is regenerated.
- **Automatic MUID collision detection**. Every inbound CI message's
  src_muid is compared to ours before dispatch; on match, state->muid
  is regenerated and Invalidate MUID is broadcast for the old value.
- **`midi2_ci_set_nak_on_unknown(state, enabled)`**: when enabled, the
  convenience responder replies to any unsupported CI sub-id with a
  Sub-ID#2 0x7F NAK v2 (status 0x01 NOT_SUPPORTED). Off by default to
  preserve v0.2.3 semantics.
- **`midi2_ci_process_sysex()` now handles `MIDI2_CI_PE_CAPABILITY`**
  (0x30) with an automatic `MIDI2_CI_PE_CAPABILITY_REPLY` (0x31)
  declaring max_simultaneous = 1, PE v1.0. Without this, Initiators
  treated PE as unsupported and never sent PE_GET.
- **`midi2_proc_send_fb_name(fb_idx, name, write_fn, ctx)`**: UMP
  Stream Function Block Name Notification sender (MT 0xF status 0x12)
  per M2-104-UM §7.1.9. Handles the Complete / Start / Continue / End
  fragmentation scheme with 13 name bytes per UMP, spec-compliant up
  to 91 bytes total.
- 14 new unit tests across `test_midi2_ci.c` and `test_midi2_proc.c`
  covering all of the above. Full suite now 266 assertions across 8
  files, all green.

### Fixed

- **Discovery Reply `ci_cat` now advertises PROCESS_INQUIRY** (bit 4)
  alongside PROFILE_CONFIG (bit 2) and PROPERTY_EXCHANGE (bit 3). The
  library handled `MIDI2_CI_PI_CAPABILITY` via
  `ci_handle_process_inquiry` since v0.2.2 but the Discovery Reply
  only exposed two of the three categories it could service. Closes
  an Initiator-side negotiation gap.

## [0.2.3]

### Added

- **`dist/midi2.c`**: thin companion to `dist/midi2.h` for projects
  that want to keep the single-header include and a separate one-line
  implementation unit. `#define MIDI2_IMPLEMENTATION` + `#include
  "midi2.h"` inside `midi2.c`.
- `tools/amalgamate.sh` now generates both files and places them under
  `dist/` (moved from repo root).

### Changed

- Source headers carry spec references (M2-101-UM, M2-104-UM) and
  metadata across every module for traceability.

## [0.2.2]

### Added
- **midi2.h**: Single-header amalgamation of all 7 modules (stb-style). One file,
  drop into any project, `#define MIDI2_IMPLEMENTATION` in one `.c`. Auto-generated
  via `tools/amalgamate.sh`.
- **test_midi2_amalgam**: 20 new tests validating the amalgamated header across
  all modules. Total: 252 tests across 8 test suites.
- **midi2_msg**: `midi2_msg_mt2_to_mt4()` -- stateless protocol translation from
  MT 0x2 (MIDI 1.0 Channel Voice) to MT 0x4 (MIDI 2.0 Channel Voice) with proper
  value scaling per M2-104-UM v1.1.2 Section 7. Handles Note On velocity 0 ->
  Note Off, Pitch Bend LSB/MSB combine, Program Change without bank.
- **midi2_dispatch**: `upscale_mt2` flag. When true, incoming MT 0x2 messages are
  automatically translated to MT 0x4 and dispatched through on_note_on/on_cc/etc.
  Default false (backward compatible).
- Hardware validation: Daisy Seed (STM32H750) confirmed Device mode via STM32 HAL USB.
- Hardware validation table expanded with real board names (T-Display S3, Waveshare
  ESP32-P4, T-PicoC3, Raspberry Pi Pico/Pico 2, etc.) and Transport column replacing
  the previous TinyUSB column.

### Changed
- **midi2_conv**: Streaming SysEx support for any message length. The converter
  now emits UMP SysEx7 packets incrementally (START/CONTINUE/END) as bytes arrive,
  instead of buffering the entire message. This removes the previous 6-byte
  truncation limitation for SysEx.
- **midi2_conv**: `midi2_conv_init()` simplified to 2 parameters (state, group).
  The caller-provided SysEx buffer is no longer needed -- the converter uses a
  6-byte internal buffer. **Breaking change** from v0.2.0 API.
- Documentation updated: single-header integration as recommended path, examples
  use `#include "midi2.h"`, hardware validation with real board names and transports.

## [0.2.0]

### Added
- **midi2_dispatch**: New module with 42 granular callbacks for typed UMP message dispatch.
  Covers 100% of M2-104-UM v1.1.2 message types. Semantically-named parameters per
  message type. NULL callbacks silently skipped. Feed signature compatible with
  midi2_proc on_ump callback.
- **midi2_msg**: Complete UMP spec coverage. New construction functions:
  - Delta Clockstamp TPQ and DC (MT 0x0, status 0x3/0x4)
  - Registered/Assignable Per-Note Controllers (MT 0x4, status 0x0/0x1)
  - Relative Registered/Assignable Controllers (MT 0x4, status 0x4/0x5)
  - Mixed Data Set Header and Payload (MT 0x5, status 0x8/0x9)
  - Set Metronome (Flex Data, bank 0x00, status 0x02)
  - Set Chord Name (Flex Data, bank 0x00, status 0x06)
  - Key Signature with tonic note and channel addressing
  - Flex Data text builder for metadata (bank 0x01) and performance text (bank 0x02)
  - Endpoint Name, Product Instance ID, FB Name notifications (Stream)
  - All Flex Data status bank and text subtype enums
- **midi2_ci_msg**: New header-only module for MIDI-CI message construction and parsing.
  Covers all 31 MIDI-CI messages per M2-101-UM v1.2. MUID encoding, 14/28-bit field
  helpers, parse helpers for common fields.
- **midi2_ci_dispatch**: New module with 33 granular callbacks for typed MIDI-CI dispatch.
  Covers Management, Profile Configuration, Property Exchange, Process Inquiry.
- **midi2_ci** refactored: now uses midi2_ci_msg for message construction (eliminates
  duplicate encoding logic). Fixed spec-incorrect Profile Inquiry sub-IDs (was 0x24/0x25,
  corrected to 0x20/0x21). Replies now include version field per spec.
- 136 new tests (total: 213 across 7 modules).

### Fixed
- Utility message status nibble was at shift 16, corrected to shift 20 per
  M2-104-UM v1.1.2 Table 26. Affects JR Clock and JR Timestamp in v0.1.0.
- SysEx7/SysEx8 dispatch delivers status values matching the MIDI2_SYSEX7_*/MIDI2_SYSEX8_*
  enum values (0x00/0x10/0x20/0x30) instead of raw nibbles.

## [0.1.0]

Initial release.

### Added
- **midi2_msg**: UMP construction and parsing for all message types (MT 0x0-0xF).
  Value scaling with bit-replication (7/14/16/32-bit, round-trip safe).
  JR Timestamps, Stream messages, SysEx7, SysEx8, Flex Data.
- **midi2_proc**: UMP processor with group filtering, group remap,
  SysEx7 multi-packet reassembly, SysEx7 fragmented send.
- **midi2_ci**: MIDI-CI responder for Discovery Reply, Profile Inquiry Reply,
  and basic Property Exchange (PE Get).
- **midi2_conv**: MIDI 1.0 byte stream to UMP converter with Running Status,
  System Common, Real-Time pass-through, SysEx accumulation.
- 77 unit tests across all modules.
- Caller-provided storage for all stateful modules (no compile-time limits).
- Error codes for functions that can fail.
