{
  "spec_version": 1,
  "slug": "ysi-odo-rtu",
  "manufacturer": "YSI (Xylem)",
  "model": "ODO RTU optical dissolved oxygen and temperature sensor",
  "category": "Water quality sensor",
  "supplier_url": "https://www.ysi.com/File%20Library/Documents/Manuals/YSI-ODO-RTU-User-Manual.pdf",
  "default_configuration": {
    "protocol": "modbus_tcp",
    "port": 502,
    "unit_id": 1,
    "addressing": "zero_based",
    "byte_order": "BigEndian",
    "word_order": "HighFirst",
    "native_interface": "rs485",
    "serial": {
      "parity": "Even",
      "baud_rate": 9600,
      "data_bits": 8,
      "stop_bits": 1
    }
  },
  "coils": [],
  "discrete_inputs": [],
  "input_registers": [
    {
      "address": 0,
      "count": 2,
      "name": "ODO saturation",
      "data_type": "Float32",
      "units": "%",
      "scale_factor": 1,
      "offset": 0,
      "access": "R",
      "description": "32-bit IEEE-754 float, big-endian."
    },
    {
      "address": 2,
      "count": 2,
      "name": "ODO concentration",
      "data_type": "Float32",
      "units": "mg/L",
      "scale_factor": 1,
      "offset": 0,
      "access": "R",
      "description": "Dissolved oxygen in mg/L."
    },
    {
      "address": 4,
      "count": 2,
      "name": "ODO, local barometer compensated",
      "data_type": "Float32",
      "units": "%",
      "scale_factor": 1,
      "offset": 0,
      "access": "R",
      "description": "Saturation compensated with the local barometric pressure."
    },
    {
      "address": 6,
      "count": 2,
      "name": "Temperature",
      "data_type": "Float32",
      "units": "°C",
      "scale_factor": 1,
      "offset": 0,
      "access": "R",
      "description": "Water temperature."
    },
    {
      "address": 8,
      "count": 2,
      "name": "Reference temperature",
      "data_type": "Float32",
      "units": "°C",
      "scale_factor": 1,
      "offset": 0,
      "access": "R",
      "description": "Reference temperature used for the conductivity compensation."
    },
    {
      "address": 10,
      "count": 2,
      "name": "Time since boot",
      "data_type": "UInt32",
      "units": "ms",
      "scale_factor": 1,
      "offset": 0,
      "access": "R",
      "description": "Uptime counter in milliseconds."
    },
    {
      "address": 12,
      "count": 2,
      "name": "Conductivity",
      "data_type": "Float32",
      "units": "µS/cm",
      "scale_factor": 1,
      "offset": 0,
      "access": "R",
      "description": "Only available on the ODO/CT submodel; an ODO/T device returns NaN (0xFFFFFFFF)."
    },
    {
      "address": 14,
      "count": 2,
      "name": "Specific conductivity",
      "data_type": "Float32",
      "units": "µS/cm",
      "scale_factor": 1,
      "offset": 0,
      "access": "R",
      "description": "Only available on the ODO/CT submodel."
    },
    {
      "address": 16,
      "count": 2,
      "name": "Salinity",
      "data_type": "Float32",
      "units": "ppt",
      "scale_factor": 1,
      "offset": 0,
      "access": "R",
      "description": "Only available on the ODO/CT submodel."
    },
    {
      "address": 18,
      "count": 2,
      "name": "Conductivity, natural water (nLF)",
      "data_type": "Float32",
      "units": "µS/cm",
      "scale_factor": 1,
      "offset": 0,
      "access": "R",
      "description": "Non-linear function compensation to EN 27888. Only available on the ODO/CT submodel."
    },
    {
      "address": 20,
      "count": 2,
      "name": "Total dissolved solids",
      "data_type": "Float32",
      "units": "mg/L",
      "scale_factor": 1,
      "offset": 0,
      "access": "R",
      "description": "Only available on the ODO/CT submodel."
    }
  ],
  "holding_registers": [
    {
      "address": 0,
      "count": 1,
      "name": "Modbus slave address",
      "data_type": "UInt16",
      "units": "",
      "scale_factor": 1,
      "offset": 0,
      "access": "RW",
      "description": "1...0xF7, written with function code 0x06. Factory default 0x01; all ODO RTUs ship at 0x01, so probes have to be re-addressed one at a time. Address 0x00 is accepted as a broadcast write address."
    },
    {
      "address": 1,
      "count": 1,
      "name": "Baud rate and parity",
      "data_type": "UInt16",
      "units": "",
      "scale_factor": 1,
      "offset": 0,
      "access": "RW",
      "description": "Upper byte = parity (0x00 none, 0x01 odd, 0x02 even), lower byte = baud rate code (0x00 9600, 0x01 19200, 0x02 38400, 0x03 57600, 0x04 115200). Factory default 0x0200 = even parity, 9600 bps."
    },
    {
      "address": 512,
      "count": 1,
      "name": "Factory reset ODO calibration",
      "data_type": "UInt16",
      "units": "",
      "scale_factor": 1,
      "offset": 0,
      "access": "W",
      "description": "Write 0x0001 with function code 0x06 to restore the factory ODO calibration."
    },
    {
      "address": 528,
      "count": 2,
      "name": "ODO last calibration time",
      "data_type": "UInt32",
      "units": "s",
      "scale_factor": 1,
      "offset": 0,
      "access": "R",
      "description": "Epoch timestamp maintained by the calibration commands."
    },
    {
      "address": 530,
      "count": 1,
      "name": "ODO quality-control score",
      "data_type": "UInt16",
      "units": "",
      "scale_factor": 1,
      "offset": 0,
      "access": "R",
      "description": "0 = best, 1 = OK, 2 = bad."
    },
    {
      "address": 4096,
      "count": 1,
      "name": "Product",
      "data_type": "UInt16",
      "units": "",
      "scale_factor": 1,
      "offset": 0,
      "access": "R",
      "description": "Default 0x0003."
    },
    {
      "address": 4097,
      "count": 1,
      "name": "Model",
      "data_type": "UInt16",
      "units": "",
      "scale_factor": 1,
      "offset": 0,
      "access": "R",
      "description": "Default 0x0001."
    },
    {
      "address": 4098,
      "count": 1,
      "name": "Submodel",
      "data_type": "UInt16",
      "units": "",
      "scale_factor": 1,
      "offset": 0,
      "access": "R",
      "description": "0x0001 = ODO/CT (with conductivity), 0x0002 = ODO/T."
    },
    {
      "address": 4099,
      "count": 1,
      "name": "Firmware major revision",
      "data_type": "UInt16",
      "units": "",
      "scale_factor": 1,
      "offset": 0,
      "access": "R",
      "description": "Read-only device information block 0x1000-0x1010."
    },
    {
      "address": 4100,
      "count": 1,
      "name": "Firmware minor revision",
      "data_type": "UInt16",
      "units": "",
      "scale_factor": 1,
      "offset": 0,
      "access": "R",
      "description": "Read-only device information block 0x1000-0x1010."
    },
    {
      "address": 4101,
      "count": 1,
      "name": "Firmware subminor revision",
      "data_type": "UInt16",
      "units": "",
      "scale_factor": 1,
      "offset": 0,
      "access": "R",
      "description": "Read-only device information block 0x1000-0x1010."
    },
    {
      "address": 4102,
      "count": 1,
      "name": "Hardware major revision",
      "data_type": "UInt16",
      "units": "",
      "scale_factor": 1,
      "offset": 0,
      "access": "R",
      "description": "Read-only device information block 0x1000-0x1010."
    },
    {
      "address": 4103,
      "count": 1,
      "name": "Hardware minor revision",
      "data_type": "UInt16",
      "units": "",
      "scale_factor": 1,
      "offset": 0,
      "access": "R",
      "description": "Read-only device information block 0x1000-0x1010."
    },
    {
      "address": 4104,
      "count": 5,
      "name": "Manufacturing serial number",
      "data_type": "Ascii",
      "units": "",
      "scale_factor": 1,
      "offset": 0,
      "access": "R",
      "description": "9 characters plus a NULL pad, packed into registers 0x1008-0x100C."
    },
    {
      "address": 4109,
      "count": 4,
      "name": "Printed circuit board serial number",
      "data_type": "Ascii",
      "units": "",
      "scale_factor": 1,
      "offset": 0,
      "access": "R",
      "description": "8 characters, packed into registers 0x100D-0x1010."
    }
  ],
  "combined_registers": [],
  "notes": "Source: YSI 'ODO RTU User Manual - Optical dissolved oxygen & temperature sensor', item 627379-01, revision B, vendor-hosted PDF at the URL above (opened 7 Nov 2026): section 5.1 Modbus serial configuration, 5.2 device information, 5.3 data, 5.4 calibration, 5.7 Appendix A register mapping tables. Factory serial settings from section 5.1: 9600 baud, slave address 0x01, even parity, one stop bit, big-endian byte order; the address register (0x0000) and the combined baud/parity register (0x0001) are included so the bus can be reconfigured. Addressing: the appendix numbers the 16 values of the measurement block consecutively (0x0000 ODO saturation, 0x0001 ODO mg/L, ... 0x000A TDS) even though every one of them is a two-register 32-bit value; section 5.3 states that a read must start at an even (or zero) register address and cover an even number of registers, and the worked read (function code 0x04, start 0x0000, 32 registers, 64 bytes returned) answers with exactly those 16 values in table order, so value N starts at wire register 2*N. The seed stores those wire addresses (0, 2, 4, ... 20) and declares zero_based. All measured values are 32-bit IEEE-754 floats, big-endian, and must be read in whole 2-register pairs starting at an even address; unavailable values (for example conductivity on an ODO/T) return NaN (0xFFFFFFFF). Conductivity, specific conductivity, salinity, conductivity nLF and TDS exist only on the ODO/CT submodel, which is what the seed's notes and row descriptions say. Left out on purpose: the ODO cap coefficient block (0x0100-0x0110) and the calibration-write blocks (0x0220 zero, 0x0230 percent, 0x0240 mg/L), which are write-only calibration plumbing whose time/value pairs the document itself describes as user-maintained; the register list keeps the readable calibration timestamp and QC score (0x0210-0x0212)."
}
