Skip to content

onewire.tc

1-Wire Driver — Temperature Sensors + Output Switches

Source on GitHub

// ============================================================================
// 1-Wire Driver — Temperature Sensors + Output Switches
// ============================================================================
//
// Supported devices on the 1-Wire bus
// -----------------------------------
//   Temperature  DS18B20 / DS18S20 / DS1822
//   Switches     DS2413  (2-channel open-drain)        family 0x3A
//                DS2406  (1- or 2-channel open-drain)  family 0x12
//                DS2408  (8-channel GPIO)              family 0x29
//   Caps         16 temp sensors + 16 switch devices per bus
//
// Bus backends (auto-detected priority cascade at startup)
// --------------------------------------------------------
//   1. DS2484 I2C-to-1-Wire bridge   (I2C addr 0x18, both buses probed)
//   2. DS2480B serial-to-1-Wire      (rx/tx pins; presence response required)
//   3. GPIO bit-bang                 (timing in C, needs external 4.7k pullup)
//
// The first backend that answers wins; the others stay silent. Whichever
// won is shown in the WebUI status line. Re-running the cascade (e.g.
// after rewiring) only takes changing any pin/bus dropdown in the WebUI.
//
// WebUI
// -----
// Configuration form lets you set:
//   GPIO Pin    — pin for GPIO bit-bang fallback
//   DS2480B RX  — UART RX, only meaningful if a DS2480B chip is wired
//   DS2480B TX  — UART TX, same
// DS2484 has no pin field — its I2C bus is sniffed automatically. The
// status line reports which backend won and its config.
//
// First two switch devices get on-page buttons (A / B channels). Devices
// beyond #1 are controlled via the OW console command.
//
// Console commands  (prefix "OW")
// -------------------------------
//   OW                        — show status JSON of all devices
//   OW <n>                    — show status of one switch device
//   OW SCAN                   — re-run cascade + ROM search
//
//   Switch state — DS2413 / DS2406 convention: 0 = ON, 1 = OFF
//     OW <n> A <0|1>          — set channel A
//     OW <n> B <0|1>          — set channel B (2-channel devices only)
//     OW <n> <byte>           — DS2408: write 8-bit port byte (0xFF=all off)
//
//   Aliases — friendly names that follow each device's 64-bit ROM,
//   persist across reboots, show up in WebUI + JSON status
//     OW NAME T <idx> <name>  — name temp sensor #idx
//     OW NAME S <idx> <name>  — name switch device #idx
//     OW NAME T|S <idx>       — (no name arg) clear the alias
//
// Examples
// --------
//   OW NAME T 0 Boiler         OW 1 A 0       → switch 1 PIO-A ON
//   OW NAME S 0 Pump           OW 1 0x55      → DS2408 device 1: bits 0,2,4,6 on
//   OW SCAN                    OW             → JSON dump of everything
//
// ============================================================================

#define OW_MAX_SENS 16
#define OW_MAX_SW   16

// 1-Wire ROM commands
#define OW_SEARCH_ROM  0xF0
#define OW_SKIP_ROM    0xCC
#define OW_MATCH_ROM   0x55

// DS18B20 commands
#define DS_CONVERT     0x44
#define DS_READ_SCRATCH 0xBE

// DS18x20 family codes
#define FAM_DS18B20  0x28
#define FAM_DS18S20  0x10
#define FAM_DS1822   0x22

// DS2413 (2-ch switch) family + commands
#define FAM_DS2413       0x3A
#define DS2413_ACCESS_WRITE 0x5A
#define DS2413_ACCESS_READ  0xF5

// DS2406 (2-ch switch) family + commands
#define FAM_DS2406       0x12
#define DS2406_CH_ACCESS     0xF5
#define DS2406_WRITE_STATUS  0x55

// DS2408 (8-ch GPIO) family + commands
#define FAM_DS2408       0x29
#define DS2408_CHANNEL_WRITE 0x5A
#define DS2408_CHANNEL_READ  0xF0
#define DS2408_READ_PIO      0xF5

// DS2480B constants
#define DS_RESET      0xC1
#define DS_DATA_MODE  0xE1
#define DS_CMD_MODE   0xE3
#define DS_PULSE_TERM 0xF1
#define DS_WRITE1     0x91
#define DS_WRITE0     0x81
#define DS_RESET_OK   0xCD

// DS2484 I2C-to-1-Wire bridge constants
#define DS84_ADDR     0x18    // 7-bit I2C address (slave addr is fixed)
#define DS84_DRST     0xF0    // Device Reset (single-byte cmd)
#define DS84_SRP      0xE1    // Set Read Pointer (then ptr byte)
#define DS84_WCFG     0xD2    // Write Configuration (then byte: hi nibble = ~lo)
#define DS84_1WRS     0xB4    // 1-Wire Reset (single byte; status auto-pointed)
#define DS84_1WSB     0x87    // 1-Wire Single Bit (arg: 0x80=write-1, 0x00=write-0)
#define DS84_1WRB     0x96    // 1-Wire Read Byte (then read data from RDATA)
#define DS84_1WWB     0xA5    // 1-Wire Write Byte (then byte)
#define DS84_1WT      0x78    // 1-Wire Triplet (arg: branch direction in bit 7)
#define DS84_RP_STAT  0xF0    // read-pointer target: Status register
#define DS84_RP_RDATA 0xE1    // read-pointer target: Read-Data register
#define DS84_ST_1WB   0x01    // Status bit: 1-Wire Busy
#define DS84_ST_PPD   0x02    // Status bit: Presence Pulse Detected
#define DS84_ST_SBR   0x20    // Status bit: Single-Bit Result

// --- config ---
// Single-backend operation chosen via auto-detect cascade in ow_reinit():
//   1. probe DS2484 on the configured I2C bus (auto)
//   2. else try DS2480B (serial init using the configured rx/tx pins)
//   3. else fall back to native GPIO bit-bang on the configured pin
// ow_mode is the result, NOT a user choice — the WebUI just shows it
// and lets the user pick the pin/bus that each backend would use.
persist watch int ow_pin;   // GPIO mode pin           (WebUI-writable → watch)
persist watch int ow_rxpin; // DS2480B serial RX       (WebUI-writable → watch)
persist watch int ow_txpin; // DS2480B serial TX       (WebUI-writable → watch)
persist int       ow_i2c_bus; // DS2484 bus 0/1 — auto-set by ow_84_init probe
int ow_mode = 0;       // detected backend: 0=GPIO, 1=DS2480B, 2=DS2484
int ow_serial = -1;    // serial port handle (-1 = closed; 0..2 = valid TasmotaSerial slot returned by serialBegin)
int ow_ok = 0;         // driver active
int ow_tick = 0;       // even/odd second counter
int ow_ds_cmd = 1;     // DS2480B in command mode flag

// temperature sensor data
int ow_cnt = 0;        // number of temp sensors
char ow_rom[16][8];    // 16 sensors x 8 bytes ROM
float ow_temp[16];     // temperatures
int ow_valid[16];      // valid reading flag

// switch device data
int ow_sw_cnt = 0;     // number of switch devices
char ow_sw_rom[16][8]; // 16 devices x 8 bytes ROM
int ow_sw_type[16];    // family code (0x3A or 0x29)
int ow_sw_state[16];   // output state (DS2413: bits 0-1, DS2408: bits 0-7)
int ow_sw_input[16];   // readback input state
int ow_sw_valid[16];   // readback valid flag
int ow_sw_ch[16];      // channel count (1=DS2406P, 2=DS2413/DS2406, 8=DS2408)

// WebUI button variables for DS2413 switches (up to 2 devices x 2 channels)
int ow_sw0a = 0;       // device 0 PIO-A
int ow_sw0b = 0;       // device 0 PIO-B
int ow_sw1a = 0;       // device 1 PIO-A
int ow_sw1b = 0;       // device 1 PIO-B
// WebUI number inputs for DS2408 (up to 2 devices)
int ow_port0 = 0;      // device 0 port byte
int ow_port1 = 0;      // device 1 port byte
// track previous values for change detection
int ow_sw0a_prev = 0;
int ow_sw0b_prev = 0;
int ow_sw1a_prev = 0;
int ow_sw1b_prev = 0;
int ow_port0_prev = 0;
int ow_port1_prev = 0;

// alias names (persist across reboots)
// ROM-based: 16 entries x 20 bytes (8 ROM + 12 name) = 320 bytes
#define OW_NLEN 12
#define OW_ALEN 20
#define OW_MAX_ALIAS 16
persist char ow_alias[320];   // ROM-to-name alias table

// buffers
char ow_buf[16];
char ow_lbl[32];
char ow_arom[8];   // temp ROM buffer for alias lookup
char ow_aname[16]; // temp name buffer for alias result

// ---- CRC8 (1-Wire polynomial 0x8C reflected) ----
int ow_crc8(int len) {
    int crc = 0;
    int i = 0;
    while (i < len) {
        int byte = ow_buf[i] & 0xFF;
        int j = 0;
        while (j < 8) {
            int mix = (crc ^ byte) & 0x01;
            crc = crc >> 1;
            if (mix) crc = crc ^ 0x8C;
            byte = byte >> 1;
            j = j + 1;
        }
        i = i + 1;
    }
    return crc;
}

// ========================================
// DS2480B serial 1-Wire bridge
// ========================================

// Wait for serial reply, up to 50ms
int ow_ds_wait() {
    int i = 0;
    while (i < 50) {
        if (serialAvailable(ow_serial) > 0) return 1;
        delay(1);
        i = i + 1;
    }
    return 0;
}

int ow_ds_init() {
    int ret = serialBegin(ow_rxpin, ow_txpin, 9600, 3, 64);
    if (ret < 0) return 0;
    ow_serial = ret;   // store the handle (0..2) returned by serialBegin
    delay(100);

    // send 0xC1 to init (same as library begin()). A real DS2480B chip
    // echoes a presence byte; serialBegin alone returning OK just means
    // the pins were free, NOT that anything's wired there — so require
    // an actual response or release the port and fail the probe.
    serialWriteByte(ow_serial, DS_RESET);
    if (!ow_ds_wait()) {
        serialClose(ow_serial);
        ow_serial = -1;
        return 0;
    }
    serialRead(ow_serial);  // discard the presence byte

    ow_ds_cmd = 1;
    return 1;
}

int ow_ds_reset() {
    if (!ow_ds_cmd) {
        serialWriteByte(ow_serial,DS_CMD_MODE);
        delay(1);
        ow_ds_cmd = 1;
    }
    serialWriteByte(ow_serial,DS_RESET);

    if (ow_ds_wait()) {
        int resp = serialRead(ow_serial) & 0xFF;
        if (resp == DS_RESET_OK) return 1;  // 0xCD = presence detected
        return 0;
    }
    return 0;
}

void ow_ds_wbit(int b) {
    if (!ow_ds_cmd) {
        serialWriteByte(ow_serial,DS_CMD_MODE);
        ow_ds_cmd = 1;
    }
    if (b) {
        serialWriteByte(ow_serial,DS_WRITE1);
    } else {
        serialWriteByte(ow_serial,DS_WRITE0);
    }
    if (ow_ds_wait()) {
        serialRead(ow_serial);
    }
}

int ow_ds_rbit() {
    if (!ow_ds_cmd) {
        serialWriteByte(ow_serial,DS_CMD_MODE);
        ow_ds_cmd = 1;
    }
    serialWriteByte(ow_serial,DS_WRITE1);
    if (ow_ds_wait()) {
        int resp = serialRead(ow_serial) & 0xFF;
        return resp & 0x01;
    }
    return 1;
}

void ow_ds_write(int byte) {
    if (ow_ds_cmd) {
        serialWriteByte(ow_serial,DS_DATA_MODE);
        ow_ds_cmd = 0;
    }
    serialWriteByte(ow_serial,byte);
    // escape: if byte is 0xE1, 0xE3, or 0xF1, send it twice
    if (byte == DS_DATA_MODE || byte == DS_CMD_MODE || byte == DS_PULSE_TERM) {
        serialWriteByte(ow_serial,byte);
    }
    if (ow_ds_wait()) {
        serialRead(ow_serial);
    }
}

int ow_ds_read() {
    if (ow_ds_cmd) {
        serialWriteByte(ow_serial,DS_DATA_MODE);
        ow_ds_cmd = 0;
    }
    serialWriteByte(ow_serial,0xFF);
    if (ow_ds_wait()) {
        return serialRead(ow_serial) & 0xFF;
    }
    return 0xFF;
}

// ========================================
// DS2484 I2C-to-1-Wire bridge
// Same chip family as DS2480B, but I2C instead of UART. No pins to
// claim — sits on whichever I2C bus the user picked. Status polling
// (1WB bit) gates each 1-Wire operation.
// ========================================

char ow_84_buf[2];

// Encode the WCFG argument: lower nibble = config bits, upper nibble = ~lower.
int ow_84_cfg_byte(int v) {
    return (((~v) & 0x0F) << 4) | (v & 0x0F);
}

// Poll Status until 1WB (1-Wire Busy) clears. Caller is expected to have
// issued a 1-Wire command that auto-points to the Status register (Reset /
// Single Bit / Read Byte / Write Byte / Triplet all do). Returns the final
// Status byte, or 0xFF on I2C timeout.
int ow_84_wait() {
    int i = 0;
    while (i < 50) {
        if (i2cRead0(DS84_ADDR, ow_84_buf, 1, ow_i2c_bus) > 0) {
            int st = ow_84_buf[0] & 0xFF;
            if (!(st & DS84_ST_1WB)) return st;
        }
        delay(1);
        i = i + 1;
    }
    return 0xFF;
}

// Probe both I2C buses for a DS2484 at the fixed address 0x18. If found,
// sets ow_i2c_bus to whichever bus answered and initialises the chip.
// Returns 1 on success, 0 if no DS2484 is reachable on either bus.
int ow_84_init() {
    int b = 0;
    while (b < 2) {
        if (i2cExists(DS84_ADDR, b)) {
            ow_i2c_bus = b;
            if (!i2cWrite0(DS84_ADDR, DS84_DRST, b)) return 0;
            delay(1);
            // Write config: APU=1 (active pullup → cleaner edges, faster bus)
            i2cWrite8(DS84_ADDR, DS84_WCFG, ow_84_cfg_byte(0x01), b);
            return 1;
        }
        b = b + 1;
    }
    return 0;
}

int ow_84_reset() {
    if (!i2cWrite0(DS84_ADDR, DS84_1WRS, ow_i2c_bus)) return 0;
    int st = ow_84_wait();
    if (st == 0xFF) return 0;
    return (st & DS84_ST_PPD) ? 1 : 0;
}

void ow_84_write(int byte) {
    i2cWrite8(DS84_ADDR, DS84_1WWB, byte & 0xFF, ow_i2c_bus);
    ow_84_wait();
}

int ow_84_read() {
    i2cWrite0(DS84_ADDR, DS84_1WRB, ow_i2c_bus);
    ow_84_wait();
    // Switch read pointer to Read-Data register, then read 1 byte
    i2cWrite8(DS84_ADDR, DS84_SRP, DS84_RP_RDATA, ow_i2c_bus);
    if (i2cRead0(DS84_ADDR, ow_84_buf, 1, ow_i2c_bus) > 0) {
        return ow_84_buf[0] & 0xFF;
    }
    return 0xFF;
}

void ow_84_wbit(int b) {
    // 1-Wire Single Bit: arg bit 7 carries the value (0x80=1, 0x00=0).
    i2cWrite8(DS84_ADDR, DS84_1WSB, b ? 0x80 : 0x00, ow_i2c_bus);
    ow_84_wait();
}

int ow_84_rbit() {
    // To read a bit, write a 1 (releases the bus) and inspect SBR in Status.
    i2cWrite8(DS84_ADDR, DS84_1WSB, 0x80, ow_i2c_bus);
    int st = ow_84_wait();
    return (st & DS84_ST_SBR) ? 1 : 0;
}

// ========================================
// Abstraction layer
// GPIO mode uses native owXxx() syscalls (timing in C)
// DS2480B mode uses serial functions
// DS2484  mode uses I2C functions
// ========================================

// Dispatch helpers — operate on the active backend (ow_mode).

int ow_reset() {
    if (ow_mode == 0) return owReset();
    if (ow_mode == 2) return ow_84_reset();
    return ow_ds_reset();
}

void ow_write(int byte) {
    if (ow_mode == 0) owWrite(byte);
    else if (ow_mode == 2) ow_84_write(byte);
    else ow_ds_write(byte);
}

int ow_read() {
    if (ow_mode == 0) return owRead();
    if (ow_mode == 2) return ow_84_read();
    return ow_ds_read();
}

void ow_wbit(int b) {
    if (ow_mode == 0) owWriteBit(b);
    else if (ow_mode == 2) ow_84_wbit(b);
    else ow_ds_wbit(b);
}

int ow_rbit() {
    if (ow_mode == 0) return owReadBit();
    if (ow_mode == 2) return ow_84_rbit();
    return ow_ds_rbit();
}

// ========================================
// ROM Search — uses native OneWire library search
// ========================================

char ow_srom[8];

// Record one found ROM as a temp sensor or switch device.
void ow_record_found() {
    int fam = ow_srom[0] & 0xFF;
    int i = 0;
    if ((fam == FAM_DS18B20 || fam == FAM_DS18S20 || fam == FAM_DS1822) && ow_cnt < OW_MAX_SENS) {
        while (i < 8) { ow_rom[ow_cnt][i] = ow_srom[i]; i = i + 1; }
        ow_valid[ow_cnt] = 0;
        ow_cnt = ow_cnt + 1;
    } else if ((fam == FAM_DS2413 || fam == FAM_DS2406 || fam == FAM_DS2408) && ow_sw_cnt < OW_MAX_SW) {
        while (i < 8) { ow_sw_rom[ow_sw_cnt][i] = ow_srom[i]; i = i + 1; }
        ow_sw_type[ow_sw_cnt] = fam;
        ow_sw_state[ow_sw_cnt] = 0xFF;
        ow_sw_input[ow_sw_cnt] = 0;
        ow_sw_valid[ow_sw_cnt] = 0;
        if (fam == FAM_DS2408) ow_sw_ch[ow_sw_cnt] = 8;
        else ow_sw_ch[ow_sw_cnt] = 2;
        ow_sw_cnt = ow_sw_cnt + 1;
    }
}

// GPIO backend search — uses the native owSearch() library.
void ow_scan_gpio() {
    owSetPin(ow_pin);
    owSearchReset();
    int total = 0;
    while (total < (OW_MAX_SENS + OW_MAX_SW)) {
        if (!owSearch(ow_srom)) break;
        addLog("OW: found fam=0x%02X (gpio)", ow_srom[0] & 0xFF);
        ow_record_found();
        total = total + 1;
    }
}

// Bit-bang ROM search via the dispatch layer (works for both DS2480B and
// DS2484). Could be sped up on DS2484 via 1-Wire Triplet (0x78) — keeping
// the unified code path for simplicity.
void ow_scan_bitbang() {
    int ow_last_disc = 0;
    int ow_last_done = 0;
    int i = 0;
    while (i < 8) { ow_srom[i] = 0; i = i + 1; }

    int total = 0;
    while (total < (OW_MAX_SENS + OW_MAX_SW)) {
        if (ow_last_done) break;
        if (!ow_reset()) break;

        ow_write(OW_SEARCH_ROM);

        int last_zero = 0;
        int pos = 1;
        int ok = 1;

        while (pos <= 64) {
            int id_bit = ow_rbit();
            int cmp_bit = ow_rbit();

            if (id_bit == 1 && cmp_bit == 1) { ok = 0; break; }

            int direction = 0;
            if (id_bit == 0 && cmp_bit == 0) {
                if (pos == ow_last_disc) {
                    direction = 1;
                } else if (pos > ow_last_disc) {
                    direction = 0;
                } else {
                    int byteN = (pos - 1) / 8;
                    int bitN = (pos - 1) % 8;
                    direction = (ow_srom[byteN] >> bitN) & 1;
                }
                if (direction == 0) last_zero = pos;
            } else {
                direction = id_bit;
            }

            int byteN = (pos - 1) / 8;
            int bitN = (pos - 1) % 8;
            if (direction) ow_srom[byteN] = ow_srom[byteN] | (1 << bitN);
            else           ow_srom[byteN] = ow_srom[byteN] & ~(1 << bitN);

            ow_wbit(direction);
            pos = pos + 1;
        }

        if (!ok) break;

        ow_last_disc = last_zero;
        if (ow_last_disc == 0) ow_last_done = 1;

        // verify CRC
        i = 0;
        while (i < 8) { ow_buf[i] = ow_srom[i]; i = i + 1; }
        if (ow_crc8(8) != 0) { total = total + 1; continue; }

        char dd[48];
        if (ow_mode == 1) sprintf(dd, "OW: found fam=0x%02X (ds2480b)", ow_srom[0] & 0xFF);
        else              sprintf(dd, "OW: found fam=0x%02X (ds2484)",  ow_srom[0] & 0xFF);
        addLog(dd);
        ow_record_found();
        total = total + 1;
    }
}

void ow_scan() {
    ow_cnt = 0;
    ow_sw_cnt = 0;
    if (ow_mode == 0) ow_scan_gpio();
    else              ow_scan_bitbang();
}

// ========================================
// Device selection helpers
// ========================================

void ow_select(int idx) {
    ow_write(OW_MATCH_ROM);
    int i = 0;
    while (i < 8) {
        ow_write(ow_rom[idx][i] & 0xFF);
        i = i + 1;
    }
}

void ow_sw_select(int idx) {
    ow_write(OW_MATCH_ROM);
    int i = 0;
    while (i < 8) {
        ow_write(ow_sw_rom[idx][i] & 0xFF);
        i = i + 1;
    }
}

// ========================================
// DS18B20 Temperature Reading
// ========================================

void ow_convert_all() {
    // Use MATCH_ROM per sensor (not SKIP_ROM) to avoid
    // sending CONVERT_T to DS2406 switches on the same bus
    int i = 0;
    while (i < ow_cnt) {
        if (ow_reset()) {
            ow_select(i);
            ow_write(DS_CONVERT);
        }
        i = i + 1;
    }
}

void ow_read_temp(int idx) {
    if (!ow_reset()) return;
    ow_select(idx);
    ow_write(DS_READ_SCRATCH);

    int i = 0;
    while (i < 9) {
        ow_buf[i] = ow_read();
        i = i + 1;
    }

    if (ow_crc8(9) != 0) return;

    int fam = ow_rom[idx][0] & 0xFF;
    int lsb = ow_buf[0] & 0xFF;
    int msb = ow_buf[1] & 0xFF;
    int raw = (msb << 8) | lsb;

    if (raw & 0x8000) {
        raw = raw | 0xFFFF0000;
    }

    if (fam == FAM_DS18S20) {
        ow_temp[idx] = raw / 2.0;
    } else {
        ow_temp[idx] = raw / 16.0;
    }
    ow_valid[idx] = 1;
}

// ========================================
// DS2413 — 2-channel switch
// ========================================

// Write output state to DS2413
// state bits: bit0=PIO-A, bit1=PIO-B (0=conducting/ON, 1=off)
int ow_ds2413_write(int idx) {
    if (!ow_reset()) return 0;
    ow_sw_select(idx);
    ow_write(DS2413_ACCESS_WRITE);

    int st = ow_sw_state[idx] & 0x03;
    ow_write(st);
    ow_write((~st) & 0xFF);  // complement for verification

    int confirm = ow_read();
    if ((confirm & 0xFF) != 0xAA) return 0;  // write failed

    // read back status
    int status = ow_read();
    ow_sw_input[idx] = status & 0xFF;
    ow_sw_valid[idx] = 1;
    return 1;
}

// Read status from DS2413
// Returns status byte: bit0=PIO-A_latch, bit1=PIO-A_pin,
//                      bit2=PIO-B_latch, bit3=PIO-B_pin
// Upper nibble = complement of lower (for verification)
int ow_ds2413_read(int idx) {
    if (!ow_reset()) return 0;
    ow_sw_select(idx);
    ow_write(DS2413_ACCESS_READ);

    int status = ow_read();
    int lo = status & 0x0F;
    int hi = (status >> 4) & 0x0F;
    // verify: upper nibble should be complement of lower
    if ((lo ^ hi) != 0x0F) return 0;

    ow_sw_input[idx] = status & 0xFF;
    ow_sw_valid[idx] = 1;
    return 1;
}

// ========================================
// DS2406 — 2-channel switch
// ========================================

// Write output state to DS2406
// ow_sw_state uses DS2413 convention: 0=conducting/ON, 1=off
// DS2406 hardware: 1=conducting, 0=off → invert before writing
// CCB1 layout: [ALR(7) IM(6) TOG(5) IC(4) CHS1(3) CHS0(2) CRC1(1) CRC0(0)]
int ow_ds2406_write(int idx) {
    if (!ow_reset()) return 0;
    ow_sw_select(idx);
    ow_write(DS2406_CH_ACCESS);

    // CCB1: IM=0(write), CHS based on channel count, no CRC
    if (ow_sw_ch[idx] >= 2) {
        ow_write(0x0C);  // CHS=11 both channels
    } else {
        ow_write(0x04);  // CHS=01 channel A only
    }
    ow_write(0xFF);  // CCB2

    // read Channel Info byte (always sent by device)
    int info = ow_read();

    // Invert: ow_sw_state has 0=ON (DS2413 convention)
    // DS2406 needs 1=conducting, so invert bits
    int st = (~ow_sw_state[idx]) & 0x03;

    // DS2406 Channel Access writes bits sequentially to PIO flip-flops.
    // Only the LAST bit for each channel determines the final state.
    // CHS=01: all 8 bits → PIO-A, bit7 is last
    // CHS=11: bits alternate A,B,A,B... bit6→A(last), bit7→B(last)
    int data = 0;
    if (ow_sw_ch[idx] >= 2) {
        // CHS=11: 0x55 pattern sets A bits, 0xAA sets B bits
        if (st & 0x01) data = data | 0x55;  // PIO-A conducting
        if (st & 0x02) data = data | 0xAA;  // PIO-B conducting
    } else {
        // CHS=01: 0xFF for A=ON, 0x00 for A=OFF
        if (st & 0x01) data = 0xFF;
    }
    ow_write(data);

    // End channel access
    ow_reset();

    // Set expected input state immediately (DS2413-compatible format)
    // This prevents the readback from overriding the button on the same tick
    int mapped = 0x05;  // default: both OFF
    if (st & 0x01) mapped = mapped & 0xFE;  // PIO-A on → clear bit0
    if (st & 0x02) mapped = mapped & 0xFB;  // PIO-B on → clear bit2
    ow_sw_input[idx] = mapped;
    ow_sw_valid[idx] = 1;
    return 1;
}

// Read status from DS2406 via Channel Access
// Returns data in DS2413-compatible format:
//   bit0=PIO-A (0=conducting/ON), bit2=PIO-B (0=conducting/ON)
int ow_ds2406_read(int idx) {
    if (!ow_reset()) return 0;
    ow_sw_select(idx);
    ow_write(DS2406_CH_ACCESS);

    // CCB1: IM=1(read), CHS based on channel count, no CRC
    if (ow_sw_ch[idx] >= 2) {
        ow_write(0x4C);  // CHS=11 both channels
    } else {
        ow_write(0x44);  // CHS=01 channel A only
    }
    ow_write(0xFF);  // CCB2

    // Channel Info byte:
    //   bit0=PIO-A flip-flop (1=conducting)
    //   bit1=PIO-B flip-flop (1=conducting)
    //   bit6=has PIO-B (1=2ch, 0=1ch 3-pin device)
    //   bit7=supply (1=VDD, 0=parasitic)
    int info = ow_read();

    // Detect single-channel DS2406P (bit 6 = 0 → no PIO-B)
    if (info & 0x40) {
        ow_sw_ch[idx] = 2;
    } else {
        ow_sw_ch[idx] = 1;
    }

    // Convert to DS2413-compatible format:
    // DS2413: bit0=PIO-A(0=conducting), bit2=PIO-B(0=conducting)
    int mapped = 0x05;  // default: both OFF
    if (info & 0x01) mapped = mapped & 0xFE;  // PIO-A conducting → clear bit0
    if (info & 0x02) mapped = mapped & 0xFB;  // PIO-B conducting → clear bit2
    ow_sw_input[idx] = mapped;
    ow_sw_valid[idx] = 1;

    ow_reset();
    return 1;
}

// ========================================
// DS2408 — 8-channel GPIO
// ========================================

// Write output state to DS2408 (all 8 channels)
int ow_ds2408_write(int idx) {
    if (!ow_reset()) return 0;
    ow_sw_select(idx);
    ow_write(DS2408_CHANNEL_WRITE);

    int st = ow_sw_state[idx] & 0xFF;
    ow_write(st);
    ow_write((~st) & 0xFF);  // complement for verification

    int confirm = ow_read();
    if ((confirm & 0xFF) != 0xAA) return 0;

    // read back PIO state
    int status = ow_read();
    ow_sw_input[idx] = status & 0xFF;
    ow_sw_valid[idx] = 1;
    return 1;
}

// Read PIO input register from DS2408
int ow_ds2408_read(int idx) {
    if (!ow_reset()) return 0;
    ow_sw_select(idx);
    ow_write(DS2408_READ_PIO);
    // read PIO Logic State register at address 0x0088
    ow_write(0x88);  // address LSB
    ow_write(0x00);  // address MSB

    int status = ow_read();
    ow_sw_input[idx] = status & 0xFF;
    ow_sw_valid[idx] = 1;
    return 1;
}

// Is this a 2-channel switch? (DS2413 or DS2406 with 2 channels)
int ow_is_2ch(int idx) {
    if (ow_sw_type[idx] == FAM_DS2413) return 1;
    if (ow_sw_type[idx] == FAM_DS2406 && ow_sw_ch[idx] >= 2) return 1;
    return 0;
}

// Is this a 1-channel switch? (DS2406P — 3-pin package)
int ow_is_1ch(int idx) {
    return (ow_sw_type[idx] == FAM_DS2406 && ow_sw_ch[idx] == 1);
}

// ========================================
// Switch write helper — writes current state
// ========================================

void ow_sw_write(int idx) {
    if (ow_sw_type[idx] == FAM_DS2413) {
        ow_ds2413_write(idx);
    } else if (ow_sw_type[idx] == FAM_DS2406) {
        ow_ds2406_write(idx);
    } else {
        ow_ds2408_write(idx);
    }
}

void ow_sw_read(int idx) {
    if (ow_sw_type[idx] == FAM_DS2413) {
        ow_ds2413_read(idx);
    } else if (ow_sw_type[idx] == FAM_DS2406) {
        ow_ds2406_read(idx);
    } else {
        ow_ds2408_read(idx);
    }
}

// ========================================
// Sync WebUI button vars → sw_state[] and vice versa
// ========================================

// Copy WebUI button/number vars to sw_state array
// All 2-ch devices use DS2413 convention: 0=conducting/ON, 1=off
// DS2406 write function handles the inversion internally
void ow_ui_to_state() {
    if (ow_sw_cnt > 0) {
        if (ow_is_2ch(0) || ow_is_1ch(0)) {
            int st = 0x03;  // default: both off
            if (ow_sw0a) st = st & 0xFE;  // clear bit 0 = PIO-A on
            if (ow_is_2ch(0) && ow_sw0b) st = st & 0xFD;  // clear bit 1 = PIO-B on
            ow_sw_state[0] = st;
        } else {
            ow_sw_state[0] = ow_port0 & 0xFF;
        }
    }
    if (ow_sw_cnt > 1) {
        if (ow_is_2ch(1) || ow_is_1ch(1)) {
            int st = 0x03;
            if (ow_sw1a) st = st & 0xFE;
            if (ow_is_2ch(1) && ow_sw1b) st = st & 0xFD;
            ow_sw_state[1] = st;
        } else {
            ow_sw_state[1] = ow_port1 & 0xFF;
        }
    }
}

// Copy sw_input readback to WebUI variables
// All 2-ch devices now use DS2413-compatible format:
//   bit0=PIO-A (0=conducting=ON), bit2=PIO-B (0=conducting=ON)
void ow_state_to_ui() {
    if (ow_sw_cnt > 0 && ow_sw_valid[0]) {
        if (ow_is_2ch(0) || ow_is_1ch(0)) {
            if ((ow_sw_input[0] & 0x01) == 0) { ow_sw0a = 1; } else { ow_sw0a = 0; }
            if (ow_is_2ch(0)) {
                if ((ow_sw_input[0] & 0x04) == 0) { ow_sw0b = 1; } else { ow_sw0b = 0; }
            }
        } else {
            ow_port0 = ow_sw_input[0] & 0xFF;
        }
    }
    if (ow_sw_cnt > 1 && ow_sw_valid[1]) {
        if (ow_is_2ch(1) || ow_is_1ch(1)) {
            if ((ow_sw_input[1] & 0x01) == 0) { ow_sw1a = 1; } else { ow_sw1a = 0; }
            if (ow_is_2ch(1)) {
                if ((ow_sw_input[1] & 0x04) == 0) { ow_sw1b = 1; } else { ow_sw1b = 0; }
            }
        } else {
            ow_port1 = ow_sw_input[1] & 0xFF;
        }
    }
}

// Check if any WebUI var changed and write to device
void ow_check_ui_changes() {
    if (ow_sw_cnt > 0) {
        if (ow_is_2ch(0) || ow_is_1ch(0)) {
            if (ow_sw0a != ow_sw0a_prev || ow_sw0b != ow_sw0b_prev) {
                ow_ui_to_state();
                ow_sw_write(0);
                ow_sw0a_prev = ow_sw0a;
                ow_sw0b_prev = ow_sw0b;
            }
        } else {
            if (ow_port0 != ow_port0_prev) {
                ow_ui_to_state();
                ow_sw_write(0);
                ow_port0_prev = ow_port0;
            }
        }
    }
    if (ow_sw_cnt > 1) {
        if (ow_is_2ch(1) || ow_is_1ch(1)) {
            if (ow_sw1a != ow_sw1a_prev || ow_sw1b != ow_sw1b_prev) {
                ow_ui_to_state();
                ow_sw_write(1);
                ow_sw1a_prev = ow_sw1a;
                ow_sw1b_prev = ow_sw1b;
            }
        } else {
            if (ow_port1 != ow_port1_prev) {
                ow_ui_to_state();
                ow_sw_write(1);
                ow_port1_prev = ow_port1;
            }
        }
    }
}

// ========================================
// Re-initialize 1-Wire bus with current config
// ========================================

void ow_reinit() {
    // close existing serial if open (previous DS2480B attempt)
    if (ow_serial >= 0) {
        serialClose(ow_serial);
        ow_serial = -1;
    }
    ow_ok = 0;
    ow_cnt = 0;
    ow_sw_cnt = 0;
    ow_mode = -1;   // sentinel until cascade picks a backend

    // Cascade priority: DS2484 (I2C) → DS2480B (serial) → GPIO bit-bang.
    // The first backend that initialises wins; the others stay silent.
    if (ow_84_init()) {
        ow_mode = 2;
        addLog("1-Wire: backend = DS2484 (I2C)");
    } else if (ow_ds_init()) {
        ow_mode = 1;
        addLog("1-Wire: backend = DS2480B (serial)");
    } else {
        if (!pinFree(ow_pin)) {
            char m[64];
            sprintf(m, "1-Wire: GPIO pin %d claimed — pick another in WebUI", ow_pin);
            addLog(m);
            return;
        }
        ow_mode = 0;
        owSetPin(ow_pin);
        char m[64];
        sprintf(m, "1-Wire: backend = GPIO bit-bang on pin %d (fallback)", ow_pin);
        addLog(m);
    }

    if (!ow_reset()) {
        addLog("1-Wire: backend up but no devices on bus");
        return;
    }

    ow_scan();

    if (ow_cnt == 0 && ow_sw_cnt == 0) {
        addLog("1-Wire: no devices found");
        return;
    }

    addLog("1-Wire: %d temp, %d switch", ow_cnt, ow_sw_cnt);

    // initial switch state read
    if (ow_sw_cnt > 0) {
        int i = 0;
        while (i < ow_sw_cnt) {
            ow_sw_read(i);
            i = i + 1;
        }
        ow_state_to_ui();
        ow_sw0a_prev = ow_sw0a;
        ow_sw0b_prev = ow_sw0b;
        ow_sw1a_prev = ow_sw1a;
        ow_sw1b_prev = ow_sw1b;
        ow_port0_prev = ow_port0;
        ow_port1_prev = ow_port1;
    }

    ow_ok = 1;
}

// ========================================
// Callbacks
// ========================================

void EverySecond() {
    // detect WebUI dropdown writes via watch — fires on any pulldown change
    if (changed(ow_pin) || changed(ow_rxpin) || changed(ow_txpin)) {
        snapshot(ow_pin);
        snapshot(ow_rxpin);
        snapshot(ow_txpin);
        ow_reinit();
        return;
    }

    if (!ow_ok) return;

    ow_tick = ow_tick + 1;

    // Temperature cycle (if any temp sensors)
    if (ow_cnt > 0) {
        if ((ow_tick & 1) == 1) {
            ow_convert_all();
        } else {
            int i = 0;
            while (i < ow_cnt) {
                ow_read_temp(i);
                i = i + 1;
            }
        }
    }

    // Switch devices: check UI changes and read back state
    if (ow_sw_cnt > 0) {
        ow_check_ui_changes();
        // poll input state every 2 seconds
        if ((ow_tick & 1) == 0) {
            int i = 0;
            while (i < ow_sw_cnt) {
                ow_sw_read(i);
                i = i + 1;
            }
            ow_state_to_ui();
        }
    }
}

// ---- Alias helpers (ROM-based) ----

void ow_copy_trom(int idx) {
    int i = 0;
    while (i < 8) { ow_arom[i] = ow_rom[idx][i]; i = i + 1; }
}

void ow_copy_srom(int idx) {
    int i = 0;
    while (i < 8) { ow_arom[i] = ow_sw_rom[idx][i]; i = i + 1; }
}

// Find alias for ROM in ow_arom. Copies name to dst, returns 1 if found.
int ow_get_alias(char dst[]) {
    int e = 0;
    while (e < OW_MAX_ALIAS) {
        int aoff = e * OW_ALEN;
        if (ow_alias[aoff] != 0) {
            int match = 1;
            int b = 0;
            while (b < 8) {
                if ((ow_alias[aoff + b] & 0xFF) != (ow_arom[b] & 0xFF)) {
                    match = 0;
                    break;
                }
                b = b + 1;
            }
            if (match) {
                // copy name byte-by-byte to global buffer
                int c = 0;
                while (c < OW_NLEN) {
                    ow_aname[c] = ow_alias[aoff + 8 + c];
                    c = c + 1;
                }
                strcpy(dst, ow_aname);
                return 1;
            }
        }
        e = e + 1;
    }
    return 0;
}

// Store alias for ROM in ow_arom
void ow_set_alias(char name[], int nlen) {
    char nm[16];
    strcpy(nm, name);
    int found = -1;
    int empty = -1;
    int e = 0;
    while (e < OW_MAX_ALIAS) {
        int aoff = e * OW_ALEN;
        if (ow_alias[aoff] == 0) {
            if (empty < 0) empty = e;
        } else {
            int match = 1;
            int b = 0;
            while (b < 8) {
                if ((ow_alias[aoff + b] & 0xFF) != (ow_arom[b] & 0xFF)) {
                    match = 0;
                    break;
                }
                b = b + 1;
            }
            if (match) { found = e; break; }
        }
        e = e + 1;
    }
    int slot = found;
    if (slot < 0) slot = empty;
    if (slot < 0) return;
    int aoff = slot * OW_ALEN;
    int b = 0;
    while (b < 8) { ow_alias[aoff + b] = ow_arom[b]; b = b + 1; }
    b = 0;
    while (b < OW_NLEN) {
        if (b < nlen) { ow_alias[aoff + 8 + b] = nm[b]; }
        else { ow_alias[aoff + 8 + b] = 0; }
        b = b + 1;
    }
}

// Clear alias for ROM in ow_arom. Returns 1 if found and cleared.
int ow_clear_alias() {
    int e = 0;
    while (e < OW_MAX_ALIAS) {
        int aoff = e * OW_ALEN;
        if (ow_alias[aoff] != 0) {
            int match = 1;
            int b = 0;
            while (b < 8) {
                if ((ow_alias[aoff + b] & 0xFF) != (ow_arom[b] & 0xFF)) {
                    match = 0;
                    break;
                }
                b = b + 1;
            }
            if (match) {
                b = 0;
                while (b < OW_ALEN) { ow_alias[aoff + b] = 0; b = b + 1; }
                return 1;
            }
        }
        e = e + 1;
    }
    return 0;
}

void ow_temp_label(char dst[], int idx) {
    ow_copy_trom(idx);
    if (ow_get_alias(dst)) return;
    strcpy(dst, "DS18x20-");
    char hx[4];
    sprintf(hx, "%02X", ow_rom[idx][6] & 0xFF);
    strcat(dst, hx);
}

void ow_sw_label(char dst[], int idx) {
    ow_copy_srom(idx);
    if (ow_get_alias(dst)) return;
    if (ow_sw_type[idx] == FAM_DS2413) {
        sprintf(dst, "DS2413-%d", idx);
    } else if (ow_sw_type[idx] == FAM_DS2408) {
        sprintf(dst, "DS2408-%d", idx);
    } else {
        sprintf(dst, "DS2406-%d", idx);
    }
}

// ---- WebCall: show sensor values on main page ----

void ow_web_sensor(int idx) {
    char vt[64];
    if (!ow_valid[idx]) return;

    char name[16];
    ow_temp_label(name, idx);

    LGetString(0, ow_lbl);  // 0 = Temperature
    sprintf(vt, "{s}%s %s{m}", name, ow_lbl);
    webSend(vt);
    sprintf(vt, "%.1f °C{e}", ow_temp[idx]);
    webSend(vt);
}

void ow_web_switch(int idx) {
    char vt[64];
    if (!ow_sw_valid[idx]) return;

    char name[16];
    ow_sw_label(name, idx);

    if (ow_is_2ch(idx) || ow_is_1ch(idx)) {
        int a_on = 0;
        int b_on = 0;
        if ((ow_sw_input[idx] & 0x01) == 0) a_on = 1;
        if ((ow_sw_input[idx] & 0x04) == 0) b_on = 1;

        sprintf(vt, "{s}%s A{m}", name);
        webSend(vt);
        if (a_on) {
            webSend("ON{e}");
        } else {
            webSend("OFF{e}");
        }

        if (ow_is_2ch(idx)) {
            sprintf(vt, "{s}%s B{m}", name);
            webSend(vt);
            if (b_on) {
                webSend("ON{e}");
            } else {
                webSend("OFF{e}");
            }
        }
    } else {
        // DS2408: show hex port state
        sprintf(vt, "{s}%s{m}", name);
        webSend(vt);
        sprintf(vt, "0x%02X{e}", ow_sw_input[idx] & 0xFF);
        webSend(vt);
    }
}

void WebCall() {
    if (!ow_ok) {
        webSend("{s}1-Wire{m}no devices{e}");
        return;
    }
    int i = 0;
    while (i < ow_cnt) {
        ow_web_sensor(i);
        i = i + 1;
    }
    i = 0;
    while (i < ow_sw_cnt) {
        ow_web_switch(i);
        i = i + 1;
    }
}

// ---- JsonCall: MQTT sensor output ----

void JsonCall() {
    if (!ow_ok) return;
    char buf[64];

    // temperature sensors
    int i = 0;
    char name[16];
    while (i < ow_cnt) {
        if (ow_valid[i]) {
            ow_temp_label(name, i);
            sprintf(buf, ",\"%s\":{", name);
            responseAppend(buf);
            sprintf(buf, "\"Temperature\":%.1f}", ow_temp[i]);
            responseAppend(buf);
        }
        i = i + 1;
    }

    // switch devices
    i = 0;
    while (i < ow_sw_cnt) {
        if (ow_sw_valid[i]) {
            ow_sw_label(name, i);
            sprintf(buf, ",\"%s\":{", name);
            responseAppend(buf);
            if (ow_is_2ch(i) || ow_is_1ch(i)) {
                int a_on = 0;
                int b_on = 0;
                if ((ow_sw_input[i] & 0x01) == 0) a_on = 1;
                if ((ow_sw_input[i] & 0x04) == 0) b_on = 1;
                if (ow_is_1ch(i)) {
                    sprintf(buf, "\"PIO_A\":%d}", a_on);
                    responseAppend(buf);
                } else {
                    sprintf(buf, "\"PIO_A\":%d,", a_on);
                    responseAppend(buf);
                    sprintf(buf, "\"PIO_B\":%d}", b_on);
                    responseAppend(buf);
                }
            } else {
                sprintf(buf, "\"State\":%d,", ow_sw_state[i] & 0xFF);
                responseAppend(buf);
                sprintf(buf, "\"Input\":%d}", ow_sw_input[i] & 0xFF);
                responseAppend(buf);
            }
        }
        i = i + 1;
    }
}

// ---- WebUI: configuration page ----

void WebUI() {
    // Backend is auto-detected (DS2484 → DS2480B → GPIO). Show what won
    // and expose the pin/bus config for all three so the user can move
    // the wiring at will and re-scan picks it up.
    char status[80];
    if      (ow_mode == 2) sprintf(status, "<div><b>Backend:</b> DS2484 (I2C bus %d)</div>", ow_i2c_bus);
    else if (ow_mode == 1) sprintf(status, "<div><b>Backend:</b> DS2480B (serial rx=%d tx=%d)</div>", ow_rxpin, ow_txpin);
    else if (ow_mode == 0) sprintf(status, "<div><b>Backend:</b> GPIO bit-bang (pin %d)</div>", ow_pin);
    else                   strcpy (status, "<div><b>Backend:</b> none — no bus available</div>");
    webSend(status);
    webPulldown(ow_pin,   "GPIO Pin",   "@getfreepins");
    webPulldown(ow_rxpin, "DS2480B RX", "@getfreepins");
    webPulldown(ow_txpin, "DS2480B TX", "@getfreepins");
    // DS2484 I2C bus is auto-probed (both 0 and 1) on each reinit, so no
    // pulldown needed — the status line above shows where it was found.

    // switch controls
    if (ow_sw_cnt > 0) {
        if (ow_is_2ch(0)) {
            webButton(ow_sw0a, "SW0 PIO-A");
            webButton(ow_sw0b, "SW0 PIO-B");
        } else if (ow_is_1ch(0)) {
            webButton(ow_sw0a, "SW0 PIO-A");
        } else {
            webNumber(ow_port0, 0, 255, "SW0 Port");
        }
    }
    if (ow_sw_cnt > 1) {
        if (ow_is_2ch(1)) {
            webButton(ow_sw1a, "SW1 PIO-A");
            webButton(ow_sw1b, "SW1 PIO-B");
        } else if (ow_is_1ch(1)) {
            webButton(ow_sw1a, "SW1 PIO-A");
        } else {
            webNumber(ow_port1, 0, 255, "SW1 Port");
        }
    }
}

// ========================================
// Command handler: OW <dev> [channel] <state>
//   OW              → show status
//   OW 0 1          → 1-ch/2-ch: set PIO-A on
//   OW 0 A 1        → set device 0 PIO-A on
//   OW 0 B 0        → set device 0 PIO-B off
//   OW 0 255        → DS2408: set port byte
//   OW NAME T 0 Kitchen → set temp sensor 0 alias
//   OW NAME S 1 Relay   → set switch 1 alias
//   OW NAME T 0         → clear temp sensor 0 alias
// ========================================

// Handle NAME subcommand — set/clear alias for a device
void ow_cmd_name(char cmd[], int pos, int len) {
    char cs[64];
    strcpy(cs, cmd);

    while (pos < len && cs[pos] == ' ') pos = pos + 1;

    if (pos >= len) {
        responseCmnd("OW NAME T/S idx [name]");
        return;
    }

    int is_temp = 0;
    if (cs[pos] == 'T') {
        is_temp = 1;
    } else if (cs[pos] != 'S') {
        responseCmnd("Use T or S");
        return;
    }
    pos = pos + 1;
    while (pos < len && cs[pos] == ' ') pos = pos + 1;

    if (pos >= len) {
        responseCmnd("Missing index");
        return;
    }

    char arg[4];
    strSub(arg, cs, pos, 1);
    int idx = atoi(arg);
    pos = pos + 1;
    while (pos < len && cs[pos] == ' ') pos = pos + 1;

    if (is_temp) {
        if (idx < 0 || idx >= ow_cnt) { responseCmnd("Bad index"); return; }
        ow_copy_trom(idx);
    } else {
        if (idx < 0 || idx >= ow_sw_cnt) { responseCmnd("Bad index"); return; }
        ow_copy_srom(idx);
    }

    if (pos >= len) {
        // no name → clear alias
        if (ow_clear_alias()) {
            responseCmnd("Alias cleared");
        } else {
            responseCmnd("No alias set");
        }
        return;
    }

    // extract name (max OW_NLEN-1 chars)
    char name[16];
    int nlen = len - pos;
    if (nlen >= OW_NLEN) nlen = OW_NLEN - 1;
    strSub(name, cs, pos, nlen);
    name[nlen] = 0;

    ow_set_alias(name, nlen);

    char resp[32];
    strcpy(resp, "Alias: ");
    strcat(resp, name);
    responseCmnd(resp);
}

void ow_cmd_status(char resp[]) {
    int i = 0;
    strcpy(resp, "{");
    char tmp[32];
    char name[16];
    while (i < ow_sw_cnt) {
        if (i > 0) strcat(resp, ",");
        ow_sw_label(name, i);
        sprintf(tmp, "\"%s\":{", name);
        strcat(resp, tmp);
        if (ow_is_2ch(i) || ow_is_1ch(i)) {
            int a_on = 0;
            int b_on = 0;
            if ((ow_sw_input[i] & 0x01) == 0) a_on = 1;
            if ((ow_sw_input[i] & 0x04) == 0) b_on = 1;
            sprintf(tmp, "\"A\":%d", a_on);
            strcat(resp, tmp);
            if (ow_is_2ch(i)) {
                sprintf(tmp, ",\"B\":%d", b_on);
                strcat(resp, tmp);
            }
            strcat(resp, "}");
        } else {
            sprintf(tmp, "\"State\":%d}", ow_sw_input[i] & 0xFF);
            strcat(resp, tmp);
        }
        i = i + 1;
    }
    strcat(resp, "}");
}

void Command(char cmd[]) {
    char resp[192];
    char arg[16];
    char cs[64];
    strcpy(cs, cmd);
    int len = strlen(cs);

    // skip leading space
    int pos = 0;
    while (pos < len && cs[pos] == ' ') pos = pos + 1;

    // no args → show status
    if (pos >= len) {
        ow_cmd_status(resp);
        responseCmnd(resp);
        return;
    }

    // check for NAME subcommand
    if (len - pos >= 4 && cs[pos] == 'N' && cs[pos+1] == 'A' && cs[pos+2] == 'M' && cs[pos+3] == 'E') {
        ow_cmd_name(cs, pos + 4, len);
        return;
    }

    // check for SCAN subcommand
    if (len - pos >= 4 && cs[pos] == 'S' && cs[pos+1] == 'C' && cs[pos+2] == 'A' && cs[pos+3] == 'N') {
        ow_reinit();
        char dt[48];
        char dt2[16];
        sprintf(dt, "%d temp, ", ow_cnt);
        sprintf(dt2, "%d switch", ow_sw_cnt);
        strcat(dt, dt2);
        responseCmnd(dt);
        return;
    }

    // parse device index
    strSub(arg, cs, pos, 1);
    int dev = atoi(arg);
    pos = pos + 1;

    if (dev < 0 || dev >= ow_sw_cnt) {
        responseCmnd("Invalid device");
        return;
    }

    // skip space
    while (pos < len && cs[pos] == ' ') pos = pos + 1;

    if (pos >= len) {
        // just "OW 0" → show single device status
        ow_cmd_status(resp);
        responseCmnd(resp);
        return;
    }

    // check for A/B channel specifier
    int ch = cs[pos];
    int val = 0;

    if (ch == 'A' || ch == 'a' || ch == 'B' || ch == 'b') {
        // channel letter
        pos = pos + 1;
        while (pos < len && cs[pos] == ' ') pos = pos + 1;
        if (pos < len) {
            strSub(arg, cs, pos, 0);
            val = atoi(arg);
        }

        if (ch == 'A' || ch == 'a') {
            // set PIO-A
            if (ow_is_2ch(dev) || ow_is_1ch(dev)) {
                if (dev == 0) { ow_sw0a = val; }
                if (dev == 1) { ow_sw1a = val; }
            }
        } else {
            // set PIO-B
            if (ow_is_2ch(dev)) {
                if (dev == 0) { ow_sw0b = val; }
                if (dev == 1) { ow_sw1b = val; }
            } else {
                responseCmnd("No PIO-B on this device");
                return;
            }
        }
    } else {
        // numeric value
        strSub(arg, cs, pos, 0);
        val = atoi(arg);

        if (ow_sw_type[dev] == FAM_DS2408) {
            // DS2408: set full port byte
            if (dev == 0) ow_port0 = val;
            if (dev == 1) ow_port1 = val;
        } else {
            // 1-ch or 2-ch: treat as PIO-A
            if (dev == 0) { ow_sw0a = val; }
            if (dev == 1) { ow_sw1a = val; }
        }
    }

    // trigger write
    ow_ui_to_state();
    ow_sw_write(dev);

    // sync prev to avoid double-write from ow_check_ui_changes
    ow_sw0a_prev = ow_sw0a;
    ow_sw0b_prev = ow_sw0b;
    ow_sw1a_prev = ow_sw1a;
    ow_sw1b_prev = ow_sw1b;
    ow_port0_prev = ow_port0;
    ow_port1_prev = ow_port1;

    // respond with current state
    ow_cmd_status(resp);
    responseCmnd(resp);
}

void OnExit() {
    if (ow_serial >= 0) {
        serialClose(ow_serial);
    }
}

int main() {
    // first-run defaults (persist vars start at 0)
    if (ow_pin == 0 && ow_rxpin == 0 && ow_txpin == 0) {
        ow_pin = 4;
        ow_rxpin = 6;
        ow_txpin = 5;
    }

    // Since 1.3.36 the runtime defers autoexec main() until Tasmota's
    // uptime ≥ 3 s, so serialBegin / I2C are race-free here — no manual
    // boot delay needed.
    //
    // WebUI pulldowns live in WebUI() — calling them here writes via
    // WSContentSend_P with no active web-request context and corrupts
    // the heap. Persist handles storage; WebUI() renders on each hit.

    ow_reinit();
    addCommand("OW");
    return 0;
}