API Contract
Status: accepted
Source of truth:
- shared C API:
src/ezo.h - shared calibration-transfer API:
src/ezo_calibration_transfer.h - shared control API:
src/ezo_control.h - DO product API:
src/ezo_do.h - EC product API:
src/ezo_ec.h - HUM product API:
src/ezo_hum.h - shared parse API:
src/ezo_parse.h - shared schema API:
src/ezo_schema.h - I2C C API:
src/ezo_i2c.h - I2C C++ API:
src/ezo_i2c.hpp - I2C Arduino adapter API:
src/ezo_i2c_arduino_wire.h - ORP product API:
src/ezo_orp.h - pH product API:
src/ezo_ph.h - product metadata API:
src/ezo_product.h - RTD product API:
src/ezo_rtd.h - UART C API:
src/ezo_uart.h - POSIX UART adapter API:
src/ezo_uart_posix_serial.h - UART Arduino adapter API:
src/ezo_uart_arduino_stream.h
This document records repo-level contract decisions. It does not duplicate every declaration from the headers.
Core Rules
- The core is C99.
- The core is synchronous.
- The core does not allocate dynamically.
- The core does not sleep internally.
- Callers own timing and buffers.
- Library results and device response state are separate.
- I2C and UART use separate transport contracts and separate device types.
Shared Public Surface
The shared public surface provides:
ezo_result_tezo_command_kind_tezo_timing_hint_tezo_get_timing_hint_for_command_kind()ezo_parse_double()
Shared timing hints remain:
- generic command:
300 ms - read:
1000 ms - read with temperature compensation:
1000 ms - calibration:
1200 ms
ezo_parse_double() accepts the decimal subset needed for EZO payloads:
- optional leading or trailing ASCII whitespace
- optional sign
- optional fractional part
- no exponent syntax
Product Surface
The product metadata API provides:
- product IDs for the initial six documented families
- static registry lookup for defaults, capabilities, and support tiers
- device-info parsing for
iresponses - product-aware timing lookup by transport and command kind
Primary product entry points:
ezo_product_id_from_short_code()ezo_parse_device_info()ezo_product_get_metadata()ezo_product_get_metadata_by_short_code()ezo_product_get_timing_hint()ezo_product_resolve_timing_hint()ezo_product_get_support_tier()ezo_product_supports_capability()ezo_product_has_command_family()
Product metadata rules:
- The registry is static and hand-authored.
- Product identity stays separate from the I2C and UART device structs.
- Syntactically valid but unsupported
iresponses parse successfully and map toEZO_PRODUCT_UNKNOWN. - Default UART-state metadata is bootstrapping guidance only; higher layers still verify or configure runtime state when determinism matters.
- Firmware-sensitive defaults may be recorded as query-required instead of as a guessed fact.
- The metadata layer is facts and lookups only; typed product helpers live in separate product modules.
ezo_product_resolve_timing_hint()prefers product-specific timing when the metadata can answer the request and otherwise falls back to the shared command-kind hint.
Shared Parse And Schema Surface
The shared parse/schema layer provides:
- borrowed text spans for non-owning field views
- CSV and common
?Prefix,...query parsing helpers - a small UART sequence state helper above one-line reads
- canonical output-schema descriptors for the initial six products
- scalar and multi-output reading structs
Primary shared entry points:
ezo_text_span_tezo_parse_text_span_double()ezo_parse_csv_fields()ezo_parse_query_response()ezo_parse_prefixed_fields()ezo_uart_sequence_init()ezo_uart_sequence_push_line()ezo_uart_sequence_is_complete()ezo_schema_get_output_schema()ezo_schema_count_enabled_fields()ezo_schema_parse_scalar_reading()ezo_schema_parse_multi_output_reading()
Rules:
- Text spans borrow caller-owned buffers and are not null-terminated copies.
ezo_parse_query_response()only handles the common?Prefix,...response shape; it is not a universal parser for every device response.- Some vendor query families use alternate shapes such as
?,O,...,?,P,..., or product-specific capitalization differences; those remain product-layer parsing concerns instead of widening the shared helper into a universal normalizer. - Query and CSV helpers trim surrounding ASCII whitespace on each field.
- Empty CSV fields are preserved as zero-length spans instead of being discarded.
ezo_uart_sequence_ttracks sequence state only; it does not read from transports or interpret product-specific workflow meaning.- Output schemas encode canonical field order, not guaranteed runtime configuration.
- Multi-output parsing requires an explicit enabled-field mask from the caller or higher layer.
Product Module Surface
The product-module layer currently provides:
- shared control-plane helpers in
src/ezo_control.h - shared calibration-transfer helpers in
src/ezo_calibration_transfer.h - typed pH helpers in
src/ezo_ph.h - typed ORP helpers in
src/ezo_orp.h - typed RTD helpers in
src/ezo_rtd.h - typed EC helpers in
src/ezo_ec.h - typed DO helpers in
src/ezo_do.h - typed HUM helpers in
src/ezo_hum.h
Common shape:
- parse helpers for typed readings and shared query forms
- command builders for product-specific setters or calibration commands
- explicit I2C send/read helpers
- explicit UART send/read helpers
Rules:
- Product modules stay transport-explicit; there is no unified product device object.
- Send helpers return timing hints but do not sleep.
- Typed read/query helpers assume the device returned the expected success payload shape; callers that need raw status distinctions still use the transport-level APIs directly.
- UART helpers may consume more than one line when a product response sequence requires it, including trailing success tokens such as
*OK. - RTD reading helpers require the caller to provide the current temperature scale unless that scale was queried separately first.
- Multi-output typed reading helpers require an explicit enabled-output mask from the caller and do not hide an output-configuration query internally.
- EC, DO, and HUM output-configuration helpers are product-specific; the shared parse/schema layer does not expose a public generic output-config parser.
- Shared control/admin helpers remain transport-explicit and product-aware only for timing lookup; they do not create a unified product device abstraction.
- Calibration-transfer helpers expose command/query primitives and sequence-shaped reads, but they do not hide reboot/reconnect or multi-line loop ownership from the caller.
- UART response-code mode is part of the shared control plane because raw UART callers and applications that need mode-agnostic behavior should not assume
*OKis enabled at runtime. - UART setter or admin commands that acknowledge with a bare terminal token are caller-owned workflows; the explicit success reader is
ezo_uart_read_ok(), and callers that need to inspect*ERor*DONEcan useezo_uart_read_terminal_response(). - RTD bulk memory recall remains caller-buffered; the library does not allocate storage for recalled history.
I2C Surface
The I2C API provides:
- device init and address accessors
- generic command send helpers
- read helpers for plain read and read-with-temperature-compensation
- text response reads
- raw response reads
- thin C++ wrapper over the same C surface
- Arduino
TwoWireadapter surface
Primary I2C C entry points:
ezo_device_init()ezo_send_command()ezo_send_command_with_float()ezo_send_read()ezo_send_read_with_temp_comp()ezo_read_response()ezo_read_response_raw()
I2C transport contract:
write_then_read(context, address, tx_data, tx_len, rx_data, rx_len, rx_received)
I2C Arduino adapter contract:
- wraps
TwoWire - remains a thin transport shim
- does not own timing, parsing, or retries
I2C response semantics:
- first byte is the device status byte
- text and raw response reads are explicit separate paths
- valid but unsuccessful device statuses still return
EZO_OK - send helpers clear the cached last status to
EZO_STATUS_UNKNOWNbefore a new command - read helpers update the cached last status from the decoded status byte
I2C status-byte mapping:
1->EZO_STATUS_SUCCESS2->EZO_STATUS_FAIL254->EZO_STATUS_NOT_READY255->EZO_STATUS_NO_DATA
UART Surface
The UART API provides:
- device init
- generic command send helpers
- read helpers for plain read and read-with-temperature-compensation
- CR-terminated text response reads
- terminal-status reads for explicit setter or admin acknowledgements
- optional explicit input discard
- POSIX serial adapter surface
- Arduino
Streamadapter surface
Primary UART C entry points:
ezo_uart_device_init()ezo_uart_send_command()ezo_uart_send_command_with_float()ezo_uart_send_read()ezo_uart_send_read_with_temp_comp()ezo_uart_read_line()ezo_uart_read_terminal_response()ezo_uart_read_ok()ezo_uart_response_kind_is_control()ezo_uart_response_kind_is_terminal()ezo_uart_discard_input()
UART transport contract:
write_bytes(context, tx_data, tx_len)read_bytes(context, rx_data, rx_len, rx_received)- optional
discard_input(context)
UART Arduino adapter contract:
- wraps
Stream - reports only currently available bytes to the core
- keeps CR framing policy in the core
- does not use
String - does not hide delays
POSIX UART adapter contract:
- owns the file descriptor it opens
- requires explicit baud selection
- configures termios for 8N1, no flow control, and bounded reads
- restores saved termios state on close
- exposes the standard
ezo_uart_transport_tthrough a transport getter
UART framing rules:
- public send helpers accept command text without terminators
- the core appends a single
\r ezo_uart_read_line()reads one CR-terminated line- returned buffers are null-terminated on success
response_lenexcludes the null terminator- a full-size caller buffer needs
EZO_UART_MAX_TEXT_RESPONSE_CAPACITYbytes for the documented 255-character payload ceiling plus the terminator
UART response classification:
EZO_UART_RESPONSE_DATA: any successful non-empty line that is not a control tokenEZO_UART_RESPONSE_OK: exact*OKEZO_UART_RESPONSE_ERROR: exact*EREZO_UART_RESPONSE_OVER_VOLTAGE: exact*OVEZO_UART_RESPONSE_UNDER_VOLTAGE: exact*UVEZO_UART_RESPONSE_RESET: exact*RSEZO_UART_RESPONSE_READY: exact*REEZO_UART_RESPONSE_SLEEP: exact*SLEZO_UART_RESPONSE_WAKE: exact*WAEZO_UART_RESPONSE_DONE: exact*DONEEZO_UART_RESPONSE_UNKNOWN: initial or failure state
Rules:
*OKand*ERare device responses, not transport errors.- A valid
*ERresponse still returnsEZO_OK; callers inspect the response kind. - The low-level UART primitive reads one line, not an entire command-response sequence.
- Multi-line sequences are caller-owned or higher-layer-owned flows built on repeated line reads.
ezo_uart_response_kind_is_control()identifies non-data control/status tokens.ezo_uart_response_kind_is_terminal()identifies line kinds that can complete a sequence without implying that all sequences are one line long.- Startup or power-state tokens such as
*WA,*RE, and*RSare surfaced as valid control events; the core does not hide them. - Higher layers that need a clean workflow boundary should consume or discard stale continuous output and trailing status lines before assuming the next line belongs to a new command.
- Typed UART convenience helpers may consume trailing success tokens such as
*OKwhen that is part of their documented workflow; callers that need line-by-line ownership use the raw UART API. - Setter or admin flows that return only a terminal token should be completed explicitly with
ezo_uart_read_ok()orezo_uart_read_terminal_response(). - Shipping defaults such as continuous mode enabled or
*OKenabled are only bootstrapping heuristics; deterministic higher layers should verify or configure the mode they depend on. - The explicit response-code bootstrap recipe is query via
ezo_control_send_response_code_query_uart()plusezo_control_read_response_code_uart(), then, if disabled, sendezo_control_send_response_code_set_uart()and consume the success ack withezo_uart_read_ok(). ezo_uart_discard_input()is the explicit resynchronization tool when a caller abandons a sequence or wants to drop stale input.- Zero-length or incomplete lines return
EZO_ERR_PROTOCOL. - Buffer exhaustion before
\rreturnsEZO_ERR_BUFFER_TOO_SMALL. - v1 does not expose a raw UART response API.
- v1 does not expose a UART C++ wrapper.
Validation Boundaries
Current validation covers:
- I2C core behavior with fake transports
- product metadata and device-info parsing with host-side tests
- shared parse, schema, and timing-resolution behavior with host-side tests
- typed product helpers for pH, ORP, RTD, EC, DO, and HUM with host-side fake-transport tests
- UART core behavior with fake transports
- Linux I2C and Linux host POSIX UART adapter behavior on host builds
- Arduino I2C and UART example compile validation through PlatformIO
Current gap by design:
- a UART C++ wrapper is not part of the current baseline yet
Explicit Non-Goals
Not part of the current baseline:
- async/state-machine behavior
- automatic reconnect or stale-input cleanup around rebooting, sleep, or mode changes
- hidden retries or hidden delays
- UART C++ wrapper