#define _DEFAULT_SOURCE
#define _POSIX_C_SOURCE 200809L

#include <mosquitto.h>

#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <poll.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>

enum {
    PACKS = 16,
    CELLS = 16,
    PAYLOAD_SIZE = 300,
    RESPONSE_SIZE = 310,
    RX_SIZE = 8192,
    STATUS_SIZE = 1400
};

static volatile sig_atomic_t stopping = 0;

typedef struct {
    uint16_t cell_mv[CELLS];
    uint16_t wire_mohm[CELLS];
    uint32_t present_mask;
    uint16_t reported_max_mv, delta_mv, min_mv, max_mv;

    int16_t mos_temp_decic, temp_decic[4];
    uint32_t wire_status, pack_mv, power_mw;
    int32_t current_ma;
    uint32_t alarms;
    int16_t balance_ma;
    uint8_t balance_state, soc, soh;
    int32_t remaining_mah;
    uint32_t capacity_mah, cycles, cycle_capacity_mah, runtime_s;
    uint8_t precharge, charge_fet, discharge_fet;
    uint8_t temp_sensor_mask, heating;
    uint16_t heater_ma;
} Status;

typedef struct {
    uint32_t uv_mv, ov_mv, rcv_mv, float_mv;
    uint32_t charge_oc_ma, discharge_oc_ma;
    uint32_t cells, capacity_mah;
} Settings;

typedef struct {
    bool have_status, have_settings, published_online;
    int64_t last_status_ms;
    Settings settings;
    char status_json[STATUS_SIZE];
} Pack;

typedef struct {
    struct mosquitto *mqtt;
    Pack pack[PACKS];

    bool connected, announcing, bridge_online;
    int timeout_s;
    int64_t next_mqtt_retry_ms, next_announce_ms;
    int shutdown_mid;
    bool shutdown_acked;

    uint64_t serial_bytes, payloads_ok, bad_checksums;
    uint64_t bad_acks, statuses, configs, ignored_frames;
    int64_t next_diagnostics_ms;
} App;

typedef struct {
    uint8_t data[RX_SIZE];
    size_t used;
    int64_t candidate_since_ms;

    /* Last complete, CRC-checked Modbus trigger seen on this bus. */
    bool request_valid;
    uint8_t request_address;
    uint16_t request_type;
    int64_t request_ms;
} Receiver;

static void signal_handler(int signum)
{
    (void)signum;
    stopping = 1;
}

static int64_t now_ms(void)
{
    struct timespec ts;
    if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0)
        return 0;
    return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}

static uint16_t u16le(const uint8_t *p)
{
    return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
}

static uint32_t u32le(const uint8_t *p)
{
    return (uint32_t)p[0]
         | ((uint32_t)p[1] << 8)
         | ((uint32_t)p[2] << 16)
         | ((uint32_t)p[3] << 24);
}

static int16_t i16le(const uint8_t *p)
{
    uint16_t n = u16le(p);
    return (int16_t)(n <= INT16_MAX
                   ? (int32_t)n : (int32_t)n - 65536);
}

static int32_t i32le(const uint8_t *p)
{
    uint32_t n = u32le(p);
    return (int32_t)(n <= INT32_MAX
                   ? (int64_t)n : (int64_t)n - INT64_C(4294967296));
}

/* JK's special encoding for some temperatures below zero. */
static int temperature_decic(int16_t raw)
{
    return raw >= 990 ? 1000 - raw : raw;
}

static uint16_t crc16_modbus(const uint8_t *p, size_t length)
{
    uint16_t crc = UINT16_C(0xffff);

    for (size_t i = 0; i < length; ++i) {
        crc ^= p[i];
        for (unsigned bit = 0; bit < 8; ++bit)
            crc = (crc & 1u)
                ? (uint16_t)((crc >> 1) ^ UINT16_C(0xa001))
                : (uint16_t)(crc >> 1);
    }
    return crc;
}

static bool header_ok(const uint8_t *p)
{
    return p[0] == 0x55 && p[1] == 0xaa &&
           p[2] == 0xeb && p[3] == 0x90;
}

static bool payload_checksum_ok(const uint8_t *p)
{
    unsigned sum = 0;
    for (size_t i = 0; i < PAYLOAD_SIZE - 1; ++i)
        sum = (sum + p[i]) & 255u;
    return sum == p[PAYLOAD_SIZE - 1];
}


/*
 * Request:
 *   ADDR 10 16 20 00 01 02 00 00 CRClo CRChi   (status)
 *   ADDR 10 16 1E 00 01 02 00 00 CRClo CRChi   (config)
 */
static bool trigger_request(const uint8_t *p, uint16_t *type)
{
    if (p[0] >=PACKS ||
        p[1] != 0x10 || p[2] != 0x16 ||
        (p[3] != 0x20 && p[3] != 0x1e) ||
        p[4] != 0x00 || p[5] != 0x01 ||
        p[6] != 0x02 || p[7] != 0x00 || p[8] != 0x00)
        return false;

    uint16_t crc = crc16_modbus(p, 9);
    if (p[9] != (uint8_t)crc ||
        p[10] != (uint8_t)(crc >> 8))
        return false;

    *type = p[3] == 0x20 ? 2 : 1;
    return true;
}

/*
 * Return:
 *   1..16  valid, addressed ACK
 *   -1     ACK-shaped bytes, but invalid CRC/address
 *   0      no ACK at this offset (or not enough bytes yet)
 *
 * Unlike the previous implementation, `p` points AT the address byte;
 * there is no unconditional padding byte before it.
 */

static int ack_at(const uint8_t *p, size_t available, uint16_t type)
{
    if (available < 8)
        return -2;

    if (p[1] != 0x10 || p[2] != 0x16 ||
        p[3] != (type == 2 ? 0x20 : 0x1e) ||
        p[4] != 0x00 || p[5] != 0x01)
        return -2;

    uint16_t crc = crc16_modbus(p, 6);

    if (p[0] >= PACKS ||
        p[6] != (uint8_t)crc ||
        p[7] != (uint8_t)(crc >> 8))
        return -1;

    return p[0];                 /* Zero is a VALID pack address. */
}

static void decode_status(const uint8_t *p, Status *s)
{
    memset(s, 0, sizeof *s);

    s->present_mask = u32le(p + 70);
    s->min_mv = UINT16_MAX;

    for (unsigned i = 0; i < CELLS; ++i) {
        s->cell_mv[i] = u16le(p + 6 + 2 * i);
        s->wire_mohm[i] = u16le(p + 80 + 2 * i);

        if (s->present_mask & (UINT32_C(1) << i)) {
            if (s->cell_mv[i] < s->min_mv)
                s->min_mv = s->cell_mv[i];
            if (s->cell_mv[i] > s->max_mv)
                s->max_mv = s->cell_mv[i];
        }
    }
    if (s->min_mv == UINT16_MAX)
        s->min_mv = 0;

    s->reported_max_mv   = u16le(p + 74);
    s->delta_mv          = u16le(p + 76);
    s->mos_temp_decic    = temperature_decic(i16le(p + 144));
    s->wire_status       = u32le(p + 146);
    s->pack_mv           = u32le(p + 150);
    s->power_mw          = u32le(p + 154);
    s->current_ma        = i32le(p + 158);
    s->temp_decic[0]     = temperature_decic(i16le(p + 162));
    s->temp_decic[1]     = temperature_decic(i16le(p + 164));
    s->alarms            = u32le(p + 166);
    s->balance_ma        = i16le(p + 170);
    s->balance_state     = p[172];
    s->soc               = p[173];
    s->remaining_mah     = i32le(p + 174);
    s->capacity_mah      = u32le(p + 178);
    s->cycles            = u32le(p + 182);
    s->cycle_capacity_mah = u32le(p + 186);
    s->soh               = p[190];
    s->precharge         = p[191];
    s->runtime_s         = u32le(p + 194);
    s->charge_fet        = p[198];
    s->discharge_fet     = p[199];
    s->temp_sensor_mask  = p[214];
    s->heating           = p[215];
    s->heater_ma         = u16le(p + 236);
    s->temp_decic[2]     = temperature_decic(i16le(p + 256));
    s->temp_decic[3]     = temperature_decic(i16le(p + 258));
}

static void decode_settings(const uint8_t *p, Settings *s)
{
    *s = (Settings){
        .uv_mv           = u32le(p + 10),
        .ov_mv           = u32le(p + 18),
        .rcv_mv          = u32le(p + 38),
        .float_mv        = u32le(p + 42),
        .charge_oc_ma    = u32le(p + 50),
        .discharge_oc_ma = u32le(p + 62),
        .cells           = u32le(p + 114),
        .capacity_mah    = u32le(p + 130)
    };
}

static bool format_status(char *dst, size_t size, const Status *s)
{
    int n = snprintf(dst, size,
        "{\"v\":%" PRIu32 ",\"i\":%" PRId32
        ",\"p\":%" PRIu32 ",\"soc\":%u,\"soh\":%u,"
        "\"dv\":%u,\"lo\":%u,\"hi\":%u,\"reported_hi\":%u,"
        "\"tm\":%d,\"t\":[%d,%d,%d,%d],"
        "\"alm\":%" PRIu32 ",\"ws\":%" PRIu32
        ",\"bal\":%d,\"bs\":%u,"
        "\"rem\":%" PRId32 ",\"cap\":%" PRIu32
        ",\"cy\":%" PRIu32 ",\"cycap\":%" PRIu32
        ",\"run\":%" PRIu32
        ",\"cf\":%u,\"df\":%u,\"pre\":%u,"
        "\"heat\":%u,\"heater\":%u,\"ts\":%u,"
        "\"present\":%" PRIu32 ",\"c\":[",
        s->pack_mv, s->current_ma, s->power_mw,
        s->soc, s->soh, s->delta_mv, s->min_mv, s->max_mv,
        s->reported_max_mv, s->mos_temp_decic,
        s->temp_decic[0], s->temp_decic[1],
        s->temp_decic[2], s->temp_decic[3],
        s->alarms, s->wire_status, s->balance_ma,
        s->balance_state, s->remaining_mah, s->capacity_mah,
        s->cycles, s->cycle_capacity_mah, s->runtime_s,
        s->charge_fet, s->discharge_fet, s->precharge,
        s->heating, s->heater_ma, s->temp_sensor_mask,
        s->present_mask);

    if (n < 0 || (size_t)n >= size)
        return false;
    size_t used = (size_t)n;

    for (unsigned i = 0; i < CELLS; ++i) {
        n = snprintf(dst + used, size - used,
                     "%s%u", i ? "," : "", s->cell_mv[i]);
        if (n < 0 || (size_t)n >= size - used)
            return false;
        used += (size_t)n;
    }

    n = snprintf(dst + used, size - used, "],\"r\":[");
    if (n < 0 || (size_t)n >= size - used)
        return false;
    used += (size_t)n;

    for (unsigned i = 0; i < CELLS; ++i) {
        n = snprintf(dst + used, size - used,
                     "%s%u", i ? "," : "", s->wire_mohm[i]);
        if (n < 0 || (size_t)n >= size - used)
            return false;
        used += (size_t)n;
    }

    n = snprintf(dst + used, size - used, "]}");
    return n == 2;
}

static void pack_topic(char *dst, size_t size, int k,
                       const char *suffix)
{
    snprintf(dst, size, "battery_string/%d/%s", k, suffix);
}

static bool publish(App *a, const char *topic, const char *text,
                    bool retained, int *mid)
{
    int rc = mosquitto_publish(a->mqtt, mid, topic,
                               (int)strlen(text), text, 1, retained);
    if (rc != MOSQ_ERR_SUCCESS)
        fprintf(stderr, "MQTT publish %s failed: %s\n",
                topic, mosquitto_strerror(rc));
    return rc == MOSQ_ERR_SUCCESS;
}

static bool publish_availability(App *a, int k, bool online)
{
    char topic[80];
    pack_topic(topic, sizeof topic, k, "availability");

    if (!publish(a, topic, online ? "online" : "offline",
                 true, NULL))
        return false;

    a->pack[k].published_online = online;
    fprintf(stderr, "MQTT battery %d availability = %s\n",
            k, online ? "online" : "offline");
    return true;
}

static bool publish_config(App *a, int k)
{
    char topic[80], json[512];
    const Pack *p = &a->pack[k];

    pack_topic(topic, sizeof topic, k, "config");

    int n = snprintf(json, sizeof json,
        "{\"max_charge_ma\":48000,\"max_discharge_ma\":48000,"
        "\"rcv_mv\":3450,\"max_cell_mv\":3650");

    if (n < 0 || (size_t)n >= sizeof json)
        return false;

    size_t used = (size_t)n;

    if (p->have_settings) {
        const Settings *s = &p->settings;

        n = snprintf(json + used, sizeof json - used,
            ",\"bms\":{\"uv_mv\":%" PRIu32
            ",\"ov_mv\":%" PRIu32
            ",\"rcv_mv\":%" PRIu32
            ",\"float_mv\":%" PRIu32
            ",\"charge_oc_ma\":%" PRIu32
            ",\"discharge_oc_ma\":%" PRIu32
            ",\"cells\":%" PRIu32
            ",\"capacity_mah\":%" PRIu32 "}",
            s->uv_mv, s->ov_mv, s->rcv_mv, s->float_mv,
            s->charge_oc_ma, s->discharge_oc_ma,
            s->cells, s->capacity_mah);

        if (n < 0 || (size_t)n >= sizeof json - used)
            return false;
        used += (size_t)n;
    }

    if (used + 2 > sizeof json)
        return false;
    json[used++] = '}';
    json[used] = '\0';

    return publish(a, topic, json, true, NULL);
}

typedef struct {
    const char *key, *name, *expression;
    const char *unit, *device_class, *state_class;
} Sensor;

static const Sensor sensors[] = {
    {"voltage", "Voltage", "value_json.v | float / 1000",
     "V", "voltage", "measurement"},
    {"current", "Current", "value_json.i | float / 1000",
     "A", "current", "measurement"},
    {"power", "Power", "value_json.p | float / 1000",
     "W", "power", "measurement"},
    {"soc", "State of charge", "value_json.soc | int",
     "%", "battery", "measurement"},
    {"soh", "State of health", "value_json.soh | int",
     "%", "", "measurement"},
    {"cell_delta", "Cell delta", "value_json.dv | float / 1000",
     "V", "voltage", "measurement"},
    {"cell_min", "Minimum cell voltage",
     "value_json.lo | float / 1000",
     "V", "voltage", "measurement"},
    {"cell_max", "Maximum cell voltage",
     "value_json.hi | float / 1000",
     "V", "voltage", "measurement"},
    {"mos_temp", "MOS temperature",
     "value_json.tm | float / 10",
     "°C", "temperature", "measurement"},
    {"balance_current", "Balance current",
     "value_json.bal | float / 1000",
     "A", "current", "measurement"},
    {"remaining", "Remaining capacity",
     "value_json.rem | float / 1000",
     "Ah", "", "measurement"},
    {"cycles", "Cycles", "value_json.cy | int",
     "", "", "total_increasing"}
};

static bool discover_sensor(App *a, int k,
                            const char *key, const char *name,
                            const char *expression, const char *unit,
                            const char *device_class,
                            const char *state_class)
{
    char topic[128], json[900];

    snprintf(topic, sizeof topic,
             "homeassistant/sensor/battery_string_%d_%s/config",
             k, key);

    int n = snprintf(json, sizeof json,
        "{\"name\":\"%s\","
        "\"unique_id\":\"battery_string_%d_%s\","
        "\"state_topic\":\"battery_string/%d/status\","
        "\"availability\":["
          "{\"topic\":\"battery_string/bridge/availability\"},"
          "{\"topic\":\"battery_string/%d/availability\"}"
        "],"
        "\"availability_mode\":\"all\","
        "\"value_template\":\"{{ %s }}\","
        "\"device\":{\"identifiers\":[\"battery_string_%d\"],"
                    "\"name\":\"Battery string %d\"}",
        name, k, key, k, k, expression, k, k);

    if (n < 0 || (size_t)n >= sizeof json)
        return false;
    size_t used = (size_t)n;

#define ADD_PROPERTY(format, value) do {                           \
    n = snprintf(json + used, sizeof json - used, format, value);  \
    if (n < 0 || (size_t)n >= sizeof json - used) return false;    \
    used += (size_t)n;                                              \
} while (0)

    if (*unit)
        ADD_PROPERTY(",\"unit_of_measurement\":\"%s\"", unit);
    if (*device_class)
        ADD_PROPERTY(",\"device_class\":\"%s\"", device_class);
    if (*state_class)
        ADD_PROPERTY(",\"state_class\":\"%s\"", state_class);

     if (strncmp(key, "cell_", 5) == 0) {
    		ADD_PROPERTY(",\"suggested_display_precision\":%d", 3);
     }

#undef ADD_PROPERTY

    if (used + 2 > sizeof json)
        return false;
    json[used++] = '}';
    json[used] = '\0';

    return publish(a, topic, json, true, NULL);
}

static bool publish_discovery(App *a, int k)
{
    for (size_t i = 0;
         i < sizeof sensors / sizeof sensors[0]; ++i) {
        const Sensor *s = &sensors[i];

        if (!discover_sensor(a, k, s->key, s->name,
                             s->expression, s->unit,
                             s->device_class, s->state_class))
            return false;
    }

    for (unsigned i = 0; i < CELLS; ++i) {
        char key[32], name[48], expression[100];

        snprintf(key, sizeof key, "cell_%02u", i + 1);
        snprintf(name, sizeof name, "Cell %02u voltage", i + 1);
        snprintf(expression, sizeof expression,
                 "value_json.c[%u] | float / 1000", i);

        if (!discover_sensor(a, k, key, name, expression,
                             "V", "voltage", "measurement"))
            return false;
    }

    for (unsigned i = 0; i < 4; ++i) {
        char key[32], name[48], expression[100];

        snprintf(key, sizeof key, "temperature_%u", i + 1);
        snprintf(name, sizeof name, "Temperature %u", i + 1);
        snprintf(expression, sizeof expression,
                 "value_json.t[%u] | float / 10", i);

        if (!discover_sensor(a, k, key, name, expression,
                             "°C", "temperature", "measurement"))
            return false;
    }

    return true;
}

static void on_connect(struct mosquitto *m, void *userdata, int rc)
{
    (void)m;
    App *a = userdata;

    a->connected = rc == 0;
    a->bridge_online = false;
    a->announcing = true;

    if (rc == 0) {
        a->next_announce_ms = 0;
        fprintf(stderr, "MQTT connected; publishing discovery\n");
    } else {
        a->next_mqtt_retry_ms = now_ms() + 5000;
        fprintf(stderr, "MQTT connection refused: %d\n", rc);
    }
}

static void on_disconnect(struct mosquitto *m, void *userdata, int rc)
{
    (void)m;
    App *a = userdata;

    a->connected = false;
    a->bridge_online = false;
    a->announcing = true;
    a->next_mqtt_retry_ms = now_ms() + 2000;
    fprintf(stderr, "MQTT disconnected: %d\n", rc);
}

static void on_publish(struct mosquitto *m, void *userdata, int mid)
{
    (void)m;
    App *a = userdata;
    if (mid == a->shutdown_mid)
        a->shutdown_acked = true;
}

static bool fresh(const App *a, int k, int64_t now)
{
    const Pack *p = &a->pack[k];

    return p->have_status &&
           now >= p->last_status_ms &&
           now - p->last_status_ms <
               (int64_t)a->timeout_s * 1000;
}

static bool announce_all(App *a, int64_t now)
{
    if (!publish(a, "battery_string/bridge/availability",
                 "offline", true, NULL))
        return false;

    for (int k = 0; k < PACKS; ++k) {
        Pack *p = &a->pack[k];

        if (!publish_config(a, k) ||
            !publish_discovery(a, k))
            return false;

        if (fresh(a, k, now)) {
            char topic[80];
            pack_topic(topic, sizeof topic, k, "status");

            if (!publish(a, topic, p->status_json, true, NULL))
                return false;
        }

        if (!publish_availability(a, k, fresh(a, k, now)))
            return false;
    }

    if (!publish(a, "battery_string/bridge/availability",
                 "online", true, NULL))
        return false;

    a->bridge_online = true;
    a->announcing = false;
    fprintf(stderr, "MQTT bridge online\n");
    return true;
}

static void maintain_mqtt(App *a, int64_t now)
{
    if (!a->connected) {
        if (now >= a->next_mqtt_retry_ms) {
            int rc = mosquitto_reconnect_async(a->mqtt);
            if (rc != MOSQ_ERR_SUCCESS)
                fprintf(stderr, "MQTT reconnect: %s\n",
                        mosquitto_strerror(rc));
            a->next_mqtt_retry_ms = now + 5000;
        }
        return;
    }

    if (a->announcing) {
        if (now >= a->next_announce_ms &&
            !announce_all(a, now))
            a->next_announce_ms = now + 5000;
        return;
    }

    for (int k = 0; k < PACKS; ++k) {
        bool online = fresh(a, k, now);
        if (online != a->pack[k].published_online)
            publish_availability(a, k, online);
    }
}

static void accept_frame(App *a, int k, uint16_t type,
                         const uint8_t *payload)
{
    Pack *p = &a->pack[k];

    if (type == 1) {
        decode_settings(payload, &p->settings);
        p->have_settings = true;
        ++a->configs;

        fprintf(stderr,
            "RX battery %d config: cells=%" PRIu32
            " RCV=%" PRIu32 "mV OV=%" PRIu32 "mV\n",
            k, p->settings.cells, p->settings.rcv_mv,
            p->settings.ov_mv);

        /* A settings frame does NOT refresh status availability. */
        if (a->connected && a->bridge_online)
            publish_config(a, k);
        return;
    }

    Status s;
    char json[STATUS_SIZE];

    decode_status(payload, &s);
    if (!format_status(json, sizeof json, &s)) {
        fprintf(stderr, "Battery %d: status JSON too large\n", k);
        return;
    }

    memcpy(p->status_json, json, strlen(json) + 1);
    p->have_status = true;
    p->last_status_ms = now_ms();
    ++a->statuses;

    fprintf(stderr,
        "RX battery %d status: %" PRIu32 "mV %"
        PRId32 "mA SOC=%u%% cells=%u\n",
        k, s.pack_mv, s.current_ma, s.soc,
        (unsigned)__builtin_popcount(s.present_mask & 0xffffu));

    if (a->connected && a->bridge_online) {
        char topic[80];
        pack_topic(topic, sizeof topic, k, "status");

        /* Retain the last measurement; availability guards freshness. */
        if (publish(a, topic, p->status_json, true, NULL) &&
            !p->published_online)
            publish_availability(a, k, true);
    }
}

static void discard(Receiver *r, size_t count)
{
    if (count >= r->used) {
        r->used = 0;
    } else {
        memmove(r->data, r->data + count, r->used - count);
        r->used -= count;
    }
    r->candidate_since_ms = 0;
}

static void parse_rx(Receiver *r, App *a)
{
    for (;;) {
        int64_t now = now_ms();

        /* A request from too long ago cannot identify this response. */
        if (r->request_valid &&
            now - r->request_ms > 1000)
            r->request_valid = false;

        if (r->used < 4)
            return;

        /*
         * Look for the preceding Modbus trigger BEFORE throwing away
         * non-55AA bytes. The old parser discarded this request.
         */
        if (r->data[0] >= 1 && r->data[0] <= PACKS &&
            r->data[1] == 0x10 &&
            r->data[2] == 0x16 &&
            (r->data[3] == 0x20 ||
             r->data[3] == 0x1e)) {

            if (r->used < 11)
                return; /* Complete request may arrive in next read(). */

            uint16_t request_type;
            if (trigger_request(r->data, &request_type)) {
                r->request_valid = true;
                r->request_address = r->data[0];
                r->request_type = request_type;
                r->request_ms = now;

                fprintf(stderr,
                    "RX Modbus request: battery %u %s "
                    "(CRC valid)\n",
                    r->request_address,
                    request_type == 2 ? "status" : "config");

                discard(r, 11);
                continue;
            }
        }

        if (!header_ok(r->data)) {
            discard(r, 1);
            continue;
        }

        if (!r->candidate_since_ms)
            r->candidate_since_ms = now;

        if (r->used < PAYLOAD_SIZE)
            return;

        uint16_t type = u16le(r->data + 4);

        if (type != 1 && type != 2) {
            ++a->ignored_frames;
            discard(r, 1);
            continue;
        }

        if (!payload_checksum_ok(r->data)) {
            ++a->bad_checksums;
            discard(r, 1);
            continue;
        }

        /*
         * The ACK on YOUR bus apparently begins at offset 300:
         *     04 10 16 20 00 01 CRClo CRChi
         *
         * Some captures have one preceding zero; try 301 too.
         * We need only 308 bytes for the first format.
	 *
         */
	int address = -1;
bool bad_ack = false;
size_t ack_padding = 0;

/* First try ACK immediately after the 300-byte payload. */
if (r->used >= PAYLOAD_SIZE + 8) {
    int result = ack_at(r->data + PAYLOAD_SIZE,
                        r->used - PAYLOAD_SIZE, type);
    if (result >= 0)
        address = result;
    else if (result == -1)
        bad_ack = true;
}

/* Some captures have one padding byte before the ACK. */
if (address < 0 && !bad_ack &&
    r->used >= PAYLOAD_SIZE + 9 &&
    r->data[PAYLOAD_SIZE] == 0x00) {
    int result = ack_at(r->data + PAYLOAD_SIZE + 1,
                        r->used - PAYLOAD_SIZE - 1, type);
    if (result >= 0) {
        address = result;
        ack_padding = 1;
    } else if (result == -1) {
        bad_ack = true;
    }
}

if (address >= 0) {
    if (r->request_valid &&
        (r->request_type != type ||
         r->request_address != address)) {
        fprintf(stderr,
            "RX %s: preceding request differs from CRC-checked "
            "ACK for battery %d; using ACK\n",
            type == 2 ? "status" : "config", address);
    }

    ++a->payloads_ok;
    accept_frame(a, address, type, r->data);
    r->request_valid = false;
    discard(r, PAYLOAD_SIZE + ack_padding + 8);
    continue;
}

        if (bad_ack) {
            ++a->bad_acks;
            fprintf(stderr,
                "RX %s: trailing ACK has bad CRC/address; "
                "NOT using preceding request as a workaround\n",
                type == 2 ? "status" : "config");

            r->request_valid = false;
            discard(r, 1);
            continue;
        }

        /*
         * Don't attribute the payload prematurely: serial read()
         * may simply not have delivered its ACK yet.
         *
         * A request-only fallback is allowed ONLY if precisely the
         * 300-byte payload was captured, it has waited 350 ms, and
         * the preceding request was CRC-checked and matches its type.
         */
        if (r->used == PAYLOAD_SIZE &&
            now - r->candidate_since_ms >= 350) {
            if (r->request_valid &&
                r->request_type == type &&
                now - r->request_ms <= 1000) {
                int k = r->request_address;

                ++a->payloads_ok;
                fprintf(stderr,
                    "RX %s: battery %d inferred from "
                    "preceding CRC-checked request "
                    "(no ACK captured)\n",
                    type == 2 ? "status" : "config", k);

                accept_frame(a, k, type, r->data);
                r->request_valid = false;
                discard(r, PAYLOAD_SIZE);
            } else {
                ++a->bad_acks;
                fprintf(stderr,
                    "RX valid %s payload, but no ACK or "
                    "matching preceding request; unattributed\n",
                    type == 2 ? "status" : "config");
                discard(r, 1);
            }
            continue;
        }

        /*
         * An ACK may be split across reads. If extra bytes arrived
         * but neither supported ACK format validates, don't guess.
         */
        if (now - r->candidate_since_ms >= 350) {
            ++a->bad_acks;
            fprintf(stderr,
                "RX valid %s payload, but no valid ACK "
                "(tail begins %02x %02x %02x %02x); "
                "unattributed\n",
                type == 2 ? "status" : "config",
                r->used > 300 ? r->data[300] : 0,
                r->used > 301 ? r->data[301] : 0,
                r->used > 302 ? r->data[302] : 0,
                r->used > 303 ? r->data[303] : 0);
            r->request_valid = false;
            discard(r, 1);
            continue;
        }

        return; /* Wait for the rest of the response. */
    }
}

static void receive_bytes(Receiver *r, App *a,
                          const uint8_t *bytes, size_t count)
{
    a->serial_bytes += count;

    for (size_t i = 0; i < count; ++i) {
        if (r->used == sizeof r->data) {
            discard(r, 1);
            fprintf(stderr, "Serial receive buffer overflow\n");
        }
        r->data[r->used++] = bytes[i];
    }

    parse_rx(r, a);
}

static int open_serial(const char *path)
{
    int fd = open(path, O_RDONLY | O_NOCTTY | O_NONBLOCK);
    if (fd < 0)
        return -1;

    struct termios tty;
    if (tcgetattr(fd, &tty) != 0) {
        close(fd);
        return -1;
    }

    cfmakeraw(&tty);
    tty.c_cflag &= ~(CSIZE | PARENB | CSTOPB);
#ifdef CRTSCTS
    tty.c_cflag &= ~CRTSCTS;
#endif
    tty.c_cflag |= CS8 | CLOCAL | CREAD;
    tty.c_iflag &= ~(IXON | IXOFF | IXANY);
    tty.c_cc[VMIN] = 0;
    tty.c_cc[VTIME] = 0;

    if (cfsetispeed(&tty, B115200) != 0 ||
        cfsetospeed(&tty, B115200) != 0 ||
        tcsetattr(fd, TCSANOW, &tty) != 0 ||
        tcflush(fd, TCIFLUSH) != 0) {
        close(fd);
        return -1;
    }

    return fd;
}

static void serial_lost(App *a, Receiver *r, int *fd)
{
    if (*fd >= 0)
        close(*fd);
    *fd = -1;
    memset(r, 0, sizeof *r);

    for (int k = 0; k < PACKS; ++k) {
        a->pack[k].have_status = false;
        if (a->connected && a->bridge_online &&
            a->pack[k].published_online)
            publish_availability(a, k, false);
    }

    fprintf(stderr, "Serial port lost: all batteries offline\n");
}

static bool parse_int(const char *text, int minimum,
                      int maximum, int *result)
{
    char *end;
    errno = 0;
    long value = strtol(text, &end, 10);

    if (errno || end == text || *end ||
        value < minimum || value > maximum)
        return false;
    *result = (int)value;
    return true;
}

int main(int argc, char **argv)
{
    if (argc < 3 || argc > 6 || !*argv[2]) {
        fprintf(stderr,
            "Usage: %s BROKER CLIENT_ID "
            "[SERIAL=/dev/ttyUSB0] [PORT=1883] [TIMEOUT=30]\n",
            argv[0]);
        return 2;
    }

    const char *device = argc >= 4 ? argv[3] : "/dev/ttyUSB0";
    int port = 1883, timeout = 30;

    if (!*device ||
        (argc >= 5 &&
         !parse_int(argv[4], 1, 65535, &port)) ||
        (argc >= 6 &&
         !parse_int(argv[5], 1, 86400, &timeout))) {
        fprintf(stderr, "Invalid argument\n");
        return 2;
    }

    struct sigaction sa = {0};
    sa.sa_handler = signal_handler;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGINT, &sa, NULL);
    sigaction(SIGTERM, &sa, NULL);

    App a = {0};
    Receiver rx = {0};
    int serial_fd = -1;
    int64_t next_serial_retry_ms = 0;

    a.timeout_s = timeout;
    a.announcing = true;
    a.shutdown_mid = -1;
    a.next_diagnostics_ms = now_ms() + 10000;

    if (mosquitto_lib_init() != MOSQ_ERR_SUCCESS) {
        fprintf(stderr, "mosquitto_lib_init failed\n");
        return 1;
    }

    a.mqtt = mosquitto_new(argv[2], true, &a);
    if (!a.mqtt) {
        fprintf(stderr, "mosquitto_new failed\n");
        mosquitto_lib_cleanup();
        return 1;
    }

    mosquitto_connect_callback_set(a.mqtt, on_connect);
    mosquitto_disconnect_callback_set(a.mqtt, on_disconnect);
    mosquitto_publish_callback_set(a.mqtt, on_publish);

    int rc = mosquitto_will_set(
        a.mqtt, "battery_string/bridge/availability",
        7, "offline", 1, true);

    if (rc != MOSQ_ERR_SUCCESS) {
        fprintf(stderr, "MQTT will: %s\n", mosquitto_strerror(rc));
        mosquitto_destroy(a.mqtt);
        mosquitto_lib_cleanup();
        return 1;
    }

    rc = mosquitto_connect_async(a.mqtt, argv[1], port, 60);
    if (rc != MOSQ_ERR_SUCCESS)
        fprintf(stderr, "Initial MQTT connect: %s\n",
                mosquitto_strerror(rc));
    a.next_mqtt_retry_ms = now_ms() + 5000;

    while (!stopping) {
        rc = mosquitto_loop(a.mqtt, 0, 1);
        if (rc != MOSQ_ERR_SUCCESS && a.connected) {
            a.connected = false;
            a.bridge_online = false;
            a.announcing = true;
            a.next_mqtt_retry_ms = now_ms() + 2000;
        }

        int64_t now = now_ms();

        if (serial_fd < 0 && now >= next_serial_retry_ms) {
            serial_fd = open_serial(device);

            if (serial_fd < 0) {
                fprintf(stderr, "Cannot open %s: %s\n",
                        device, strerror(errno));
                next_serial_retry_ms = now + 5000;
            } else {
                memset(&rx, 0, sizeof rx);
                fprintf(stderr,
                    "Listening on %s, 115200 8N1, passive\n",
                    device);
            }
        }

	/* Let parse_rx resolve incomplete payloads or missing ACKs. */
if (rx.candidate_since_ms &&
    now - rx.candidate_since_ms >= 350)
    parse_rx(&rx, &a);

        maintain_mqtt(&a, now);

        if (now >= a.next_diagnostics_ms) {
            fprintf(stderr,
                "DIAG serial_bytes=%" PRIu64
                " valid_payloads=%" PRIu64
                " bad_checksums=%" PRIu64
                " missing_or_bad_ACK=%" PRIu64
                " status=%" PRIu64
                " config=%" PRIu64
                " ignored_type=%" PRIu64
                " mqtt=%s\n",
                a.serial_bytes, a.payloads_ok,
                a.bad_checksums, a.bad_acks,
                a.statuses, a.configs, a.ignored_frames,
                a.connected ? "connected" : "disconnected");
            a.next_diagnostics_ms = now + 10000;
        }

        if (serial_fd < 0) {
            (void)poll(NULL, 0, 100);
            continue;
        }

        struct pollfd pfd = {
            .fd = serial_fd,
            .events = POLLIN
        };

        int ready = poll(&pfd, 1, 100);

        if (ready < 0) {
            if (errno == EINTR)
                continue;
            perror("serial poll");
            serial_lost(&a, &rx, &serial_fd);
            next_serial_retry_ms = now_ms() + 5000;
            continue;
        }

        if (ready == 0)
            continue;

        if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) {
            serial_lost(&a, &rx, &serial_fd);
            next_serial_retry_ms = now_ms() + 5000;
            continue;
        }

        if (pfd.revents & POLLIN) {
            uint8_t chunk[1024];
            ssize_t n = read(serial_fd, chunk, sizeof chunk);

            if (n > 0) {
                receive_bytes(&rx, &a, chunk, (size_t)n);
            } else if (n < 0 && errno != EAGAIN &&
                       errno != EWOULDBLOCK && errno != EINTR) {
                perror("serial read");
                serial_lost(&a, &rx, &serial_fd);
                next_serial_retry_ms = now_ms() + 5000;
            }
        }
    }

    if (serial_fd >= 0)
        close(serial_fd);

    if (a.connected) {
        for (int k = 0; k < PACKS; ++k)
            publish_availability(&a, k, false);

        if (publish(&a, "battery_string/bridge/availability",
                    "offline", true, &a.shutdown_mid)) {
            int64_t deadline = now_ms() + 3000;
            while (!a.shutdown_acked && now_ms() < deadline) {
                if (mosquitto_loop(a.mqtt, 100, 1)
                    != MOSQ_ERR_SUCCESS)
                    break;
            }
        }
        mosquitto_disconnect(a.mqtt);
    }

    mosquitto_destroy(a.mqtt);
    mosquitto_lib_cleanup();
    return 0;
}
