{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "raviden-config-v1",
  "title": "RAVidEn configuration",
  "description": "Single versioned config document. DRAFT — Phase 0. The Rust binary, the web UI, and import/export all validate against this.",
  "type": "object",
  "additionalProperties": false,
  "required": ["version", "connections", "tags", "overlay", "streamEncode", "settings"],
  "properties": {
    "version": { "const": 1 },
    "connections": {
      "type": "array",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["id", "name", "protocol", "pollIntervalMs"],
        "properties": {
          "id": { "$ref": "#/$defs/id" },
          "name": { "type": "string", "minLength": 1, "maxLength": 64 },
          "protocol": { "enum": ["ab-eip", "demo", "diag", "camera", "abb-rws"], "description": "ab-eip: Allen-Bradley via libplctag; demo: simulated signals; diag: camera/app diagnostics; camera: Axis event system (AOA/VMD/tampering/IO…) — tag address = Topic0/Topic1[/…][#field]; abb-rws: ABB robot via Robot Web Services (IRC5/OmniCore auto-detected) — tag address = catalog (panel.…, exec.…, motion.…, elog.…) or rapid:Task/Module/Var[#comp] or io:Network/Device/Signal" },
          "plcType": { "enum": ["controllogix", "compactlogix", "micrologix", "micro800", "slc500", "plc5"], "default": "controllogix" },
          "gateway": { "type": "string", "description": "PLC/ENBT IP or hostname (ab-eip only)" },
          "cipPath": { "type": "string", "description": "libplctag path, e.g. \"1,0\". Empty for PLCs without a backplane path.", "default": "1,0" },
          "pollIntervalMs": { "type": "integer", "minimum": 100, "maximum": 60000 },
          "timeoutMs": { "type": "integer", "minimum": 100, "maximum": 30000, "default": 2000 },
          "enabled": { "type": "boolean", "default": true },
          "host": { "type": "string", "description": "abb-rws: robot controller IP or hostname" },
          "port": { "type": "integer", "minimum": 1, "maximum": 65535, "description": "abb-rws: optional port override (default 80 http / 443 https, auto-probed)" },
          "username": { "type": "string", "maxLength": 64, "default": "Default User", "description": "abb-rws: controller user" },
          "password": { "type": "string", "maxLength": 128, "description": "abb-rws: write-only through the API — GET returns it empty, empty on PUT keeps the stored value" },
          "verifyTls": { "type": "boolean", "default": false, "description": "abb-rws: verify the controller's TLS cert (OmniCore ships self-signed — default off)" }
        }
      }
    },
    "tags": {
      "type": "array",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["id", "name"],
        "properties": {
          "id": { "$ref": "#/$defs/id" },
          "name": { "type": "string", "minLength": 1, "maxLength": 32, "description": "Display name; also the record name in data-in-stream (<=16 bytes used there)" },
          "connectionId": { "type": "string", "pattern": "^[a-zA-Z0-9_-]{0,40}$", "description": "Empty for derived tags" },
          "address": { "type": "string", "description": "Controller tag name (Logix) or data-table address (SLC/PLC5); empty for derived tags" },
          "dataType": { "enum": ["bool", "sint", "int", "dint", "lint", "real", "lreal", "string"] },
          "scale": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "factor": { "type": "number", "default": 1 },
              "offset": { "type": "number", "default": 0 }
            }
          },
          "format": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "decimals": { "type": "integer", "minimum": 0, "maximum": 6 },
              "units": { "type": "string", "maxLength": 16 },
              "template": { "type": "string", "maxLength": 64, "description": "e.g. \"{value} {units}\"; {name}, {value}, {units} substituted" }
            }
          },
          "kind": { "enum": ["measurement", "state"], "default": "measurement", "description": "state tags carry a state map and are stored as transitions" },
          "derived": {
            "type": "object",
            "additionalProperties": false,
            "required": ["rules"],
            "description": "Derived state: first matching rule over other tags sets the state value; no polling",
            "properties": {
              "rules": { "type": "array", "maxItems": 32, "items": { "type": "object", "additionalProperties": false, "required": ["tagId", "op", "value", "state"],
                "properties": { "tagId": { "$ref": "#/$defs/id" }, "op": { "enum": ["eq", "ne", "lt", "le", "gt", "ge"] }, "value": { "type": "number" }, "state": { "type": "number" } } } },
              "elseState": { "type": "number", "default": 0 }
            }
          },
          "states": { "type": "array", "maxItems": 128, "items": { "$ref": "#/$defs/stateDef" } },
          "defaultState": { "$ref": "#/$defs/stateDef", "description": "Label/color for values not in the map" },
          "stateCategories": { "type": "array", "maxItems": 16, "items": { "$ref": "#/$defs/stateCategory" }, "description": "Per-tag category list: overrides the global categories for this tag (empty array = none); omit to follow settings" },
          "learn": { "type": "boolean", "default": false, "description": "Learning mode: the camera appends observed unmapped values to the state map (~1 min sweep) with auto-assigned colors" }
        }
      }
    },
    "overlay": {
      "type": "object",
      "additionalProperties": false,
      "required": ["elements"],
      "properties": {
        "maxFps": { "type": "number", "minimum": 0.5, "maximum": 10, "default": 5 },
        "motion": { "type": "boolean", "default": false, "description": "Ease gauge/bar values toward new readings (5 fps redraws while animating)" },
        "watermark": {
          "type": "object",
          "additionalProperties": false,
          "description": "RAVidEn logo watermark on the feed, kept inside the data border's safe zone (4-block ring)",
          "properties": {
            "enabled": { "type": "boolean", "default": true },
            "corner": { "enum": ["tl", "tr", "bl", "br"], "default": "br" },
            "opacity": { "type": "number", "minimum": 0, "maximum": 1, "default": 0.6 },
            "size": { "type": "number", "minimum": 32, "maximum": 512, "default": 160, "description": "px at 1080p" }
          }
        },
        "theme": {
          "type": "object",
          "additionalProperties": false,
          "description": "Defaults for element boxes; elements may override radius/border",
          "properties": {
            "radius": { "type": "number", "minimum": 0, "maximum": 2, "default": 0.3, "description": "Corner radius in em (relative to font size)" },
            "bg": { "$ref": "#/$defs/color", "default": "#00000099" },
            "fg": { "$ref": "#/$defs/color", "default": "#ffffff" },
            "border": { "$ref": "#/$defs/color", "description": "Box border color; omit for none" },
            "borderWidth": { "type": "number", "minimum": 0, "maximum": 8, "default": 1, "description": "px at 1080p" }
          }
        },
        "elements": {
          "type": "array",
          "items": {
            "type": "object",
            "required": ["id", "type", "position"],
            "properties": {
              "id": { "$ref": "#/$defs/id" },
              "type": { "enum": ["staticText", "tagValue", "indicator", "tagTable", "tickChart", "stateStrip", "gauge", "metricTile", "stateTile", "stateDonut", "stateTable", "stateBars", "robotPanel", "robotFigure"] },
              "position": { "$ref": "#/$defs/point", "description": "Normalized 0..1 of frame width/height, top-left anchor" },
              "size": { "$ref": "#/$defs/point", "description": "Normalized. Charts, indicator, robotFigure: the box. tagTable (optional; omit = auto-size to the content) and robotPanel: x = width only, y is stored as 0 — the height follows rows × fontSize and long text is trimmed with an ellipsis" },
              "fontSize": { "type": "integer", "minimum": 8, "maximum": 128, "default": 24, "description": "Pixels at 1080p reference; scaled per stream. robotPanel: the base font — the panel height is fontSize × the detail level's row count (0.11.0; older configs with a non-zero size.y are migrated on load)" },
              "fg": { "$ref": "#/$defs/color" },
              "bg": { "$ref": "#/$defs/color", "description": "Background box color incl. alpha; omit for none" },
              "radius": { "type": "number", "minimum": 0, "maximum": 2, "description": "Corner radius in em; omit to use the theme" },
              "window": { "type": "integer", "minimum": 60, "maximum": 604800, "description": "Chart window in seconds (rolling)" },
              "anchor": { "enum": ["rolling", "hour", "shift", "day"], "description": "Rolling window or anchored period that fills in" },
              "range": { "type": "object", "required": ["min", "max"], "properties": { "min": { "type": "number" }, "max": { "type": "number" } } },
              "chartColor": { "$ref": "#/$defs/color" },
              "stateMap": { "type": "array", "items": { "type": "object", "required": ["value", "label", "color"], "properties": { "value": { "type": "number" }, "label": { "type": "string", "maxLength": 24 }, "color": { "$ref": "#/$defs/color" } } } },
              "showLabel": { "type": "boolean" },
              "showAxis": { "type": "boolean" },
              "showStateValue": { "type": "boolean", "default": false, "description": "State charts: prefix state labels with the raw value (\"15 - Waiting…\")" },
              "timeAxis": { "enum": ["clock", "offset"], "default": "clock", "description": "Time legend: wall-clock endpoints, or offsets from now (Now, -15m, -30m, …)" },
              "gaugeStyle": { "enum": ["bar", "vbar", "arc"] },
              "defaultLabel": { "type": "string", "maxLength": 24, "description": "stateStrip: label for values not in the state map" },
              "tickPx": { "type": "number", "minimum": 1, "maximum": 40 },
              "orientation": { "enum": ["horizontal", "vertical"], "description": "stateStrip direction" },
              "border": { "$ref": "#/$defs/color", "description": "Border color; omit to use the theme" },
              "visibleWhen": { "$ref": "#/$defs/rule", "description": "Omit = always visible" },
              "connectionId": { "$ref": "#/$defs/id", "description": "robotPanel/robotFigure: the abb-rws connection this element shows (no tags needed — the driver polls the standard catalog)" },
              "detail": { "enum": ["chip", "compact", "full"], "default": "compact", "description": "robotPanel detail level" },
              "figure": { "enum": ["none", "side", "iso"], "default": "iso", "description": "robotFigure: pose style (posed from live joint data). Legacy on robotPanel, where it is now ignored — the figure is its own element" },
              "showAlarm": { "type": "boolean", "default": true, "description": "robotPanel full: latest-alarm row inside the panel" }
            },
            "allOf": [
              {
                "if": { "properties": { "type": { "const": "staticText" } } },
                "then": { "required": ["text"], "properties": { "text": { "type": "string", "maxLength": 128 } } }
              },
              {
                "if": { "properties": { "type": { "const": "tagValue" } } },
                "then": {
                  "required": ["tagId"],
                  "properties": {
                    "tagId": { "$ref": "#/$defs/id" },
                    "prefix": { "type": "string", "maxLength": 32, "description": "Text before the value" },
                    "suffix": { "type": "string", "maxLength": 32, "description": "Text after the value" }
                  }
                }
              },
              {
                "if": { "properties": { "type": { "enum": ["tickChart", "stateStrip", "gauge", "metricTile", "stateTile", "stateDonut", "stateTable", "stateBars"] } } },
                "then": { "required": ["tagId", "size"] }
              },
              {
                "if": { "properties": { "type": { "const": "tagTable" } } },
                "then": {
                  "required": ["tagIds"],
                  "properties": {
                    "tagIds": { "type": "array", "minItems": 1, "maxItems": 20, "items": { "$ref": "#/$defs/id" }, "description": "Rows: tag name | value" }
                  }
                }
              },
              {
                "if": { "properties": { "type": { "const": "robotPanel" } } },
                "then": { "required": ["connectionId", "size"] }
              },
              {
                "if": { "properties": { "type": { "const": "robotFigure" } } },
                "then": { "required": ["connectionId", "size"] }
              },
              {
                "if": { "properties": { "type": { "const": "indicator" } } },
                "then": {
                  "required": ["tagId", "rules"],
                  "properties": {
                    "tagId": { "$ref": "#/$defs/id" },
                    "rules": {
                      "type": "array",
                      "minItems": 1,
                      "description": "First matching rule wins; defaultColor if none match",
                      "items": {
                        "type": "object",
                        "required": ["op", "value", "color"],
                        "properties": {
                          "op": { "enum": ["eq", "ne", "lt", "le", "gt", "ge"] },
                          "value": { "type": "number" },
                          "color": { "$ref": "#/$defs/color" }
                        }
                      }
                    },
                    "defaultColor": { "$ref": "#/$defs/color" }
                  }
                }
              }
            ]
          }
        }
      }
    },
    "streamEncode": {
      "type": "object",
      "additionalProperties": false,
      "required": ["enabled"],
      "properties": {
        "enabled": { "type": "boolean", "default": false },
        "syncOverlay": { "type": "boolean", "default": true, "description": "Keep one tag-bound record per tag the overlay uses (manual rows are kept)" },
        "tagIds": { "type": "array", "items": { "$ref": "#/$defs/id" }, "maxItems": 15, "description": "Legacy: tags to embed by data type; used only when records is empty" },
        "records": {
          "type": "array",
          "maxItems": 255,
          "description": "Explicit payload records. v1: first 14 encoded (2 system records prepended, 16 total). v2: up to 255 dictionary keys, snapshot bursts spread them across frames.",
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": ["name", "type"],
            "properties": {
              "name": { "type": "string", "minLength": 1, "maxLength": 16 },
              "type": { "enum": ["bit", "bitarr", "bcd4", "uint", "int", "chararr", "real"] },
              "tagId": { "$ref": "#/$defs/id", "description": "Live value source; omit for manual/auto" },
              "mode": { "enum": ["manual", "hour", "minute", "second", "millis"], "default": "manual" },
              "value": { "type": "string", "maxLength": 64, "description": "Manual value (parsed per type)" },
              "size": { "type": "integer", "minimum": 1, "maximum": 200, "description": "Array size for bitarr/chararr" },
              "pinned": { "type": "boolean", "default": false, "description": "v2: include in every update (not just snapshots/changes) so keyframe-only scans see it" },
              "synced": { "type": "boolean", "default": false, "description": "Row was created by overlay sync; it is removed when the overlay stops using the tag. Rows the user added are never auto-removed." }
            }
          }
        },
        "updateFps": { "type": "number", "minimum": 1, "maximum": 30, "default": 10 },
        "formatVersion": { "enum": [1, 2], "default": 2, "description": "1: legacy named records; 2: dictionary keys + carousel + signing (docs/ring-format-spec.md)" },
        "thickness": { "enum": [1, 2, 3, 4, 5, 6], "default": 3, "description": "v2 only: ring thickness in blocks (173/242/484/695/865/1034 payload bytes; v2.4 capped-corner geometry for t=5/6). t<=4 survives 1 Mbit/s archive re-encodes at 480p+; t=5/6 need 540p+ at 1 Mbit/s or 2 Mbit/s at 720p (docs/ring-tsweep-capacity-test.md)" },
        "signing": { "type": "boolean", "default": true, "description": "v2 only: hash-chain links + periodic Ed25519 signatures" },
        "snapshotPeriodMs": { "type": "integer", "minimum": 500, "maximum": 30000, "default": 2000, "description": "v2 only: full-snapshot burst period; deltas + pinned records in between" },
        "historyInStream": { "type": "boolean", "default": true, "description": "v2 only: carry the last 24 h of state runs for the selected state tags in the carousel (L2)" },
        "historyTagIds": { "type": "array", "maxItems": 8, "items": { "$ref": "#/$defs/id" }, "description": "v2 only: which state tags ride the L2 history carousel; omit = every state tag among the records" },
        "packed": { "type": "boolean", "default": true, "description": "v2.3: packed fast section (LEB128 values, type-in-L0, bit groups, elidable chain link) + multi-fragment carousel. Off = v2.2 wire layout for old decoders" },
        "profile": { "enum": ["history", "snapshot"], "default": "history", "description": "v2.3: history = tier-2 carousel + L5 remainder (clip length maps to history depth); snapshot = every update self-contained (full dictionary inline — single-frame extraction; config must fit)" },
        "fullHistoryInStream": { "type": "boolean", "default": true, "description": "v2.3: cycle the camera's full bounded history in the stream (L5, chunked + DEFLATE, newest-first). history profile only" },
        "robots": {
          "type": "array",
          "maxItems": 16,
          "description": "v2 only: per-robot-connection standard-field records (dictionary names r{idx}.{field}) + alarm burst groups",
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": ["connectionId"],
            "properties": {
              "connectionId": { "$ref": "#/$defs/id" },
              "fields": { "type": "array", "maxItems": 20, "items": { "enum": ["state", "opmode", "exec", "speed", "rollup", "routine", "line", "stmt", "j1", "j2", "j3", "j4", "j5", "j6", "tcpx", "tcpy", "tcpz"] }, "description": "Live records; motion fields are opt-in (churn — see encoding policy). line/stmt follow the program pointer, so they churn while running: stmt text rides carousel L4, capped at 64 bytes per entry" },
              "onAlarm": { "type": "boolean", "default": true, "description": "Encode the alarm context group (code/seq/sev/text/routine/line/live) and force a snapshot burst when a new alarm lands" }
            }
          }
        }
      }
    },
    "settings": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "logLevel": { "enum": ["error", "warn", "info", "debug"], "default": "info" },
        "stateCategories": {
          "type": "array",
          "description": "Configurable state categories (empty = built-in defaults: productive, idle, blocked, blocked-upstream, blocked-downstream, down, maintenance)",
          "items": { "$ref": "#/$defs/stateCategory" }
        },
        "stateCategoriesEnabled": { "type": "boolean", "default": true, "description": "Off = no global categories (tags may still carry their own)" },
        "shifts": {
          "type": "array",
          "items": { "type": "object", "required": ["name", "start"], "additionalProperties": false,
            "properties": { "name": { "type": "string", "maxLength": 24 }, "start": { "type": "string", "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$" } } }
        }
      }
    }
  },
  "$defs": {
    "id": { "type": "string", "pattern": "^[a-zA-Z0-9_-]{1,40}$" },
    "stateCategory": { "type": "object", "required": ["id", "label", "color"], "additionalProperties": false,
      "properties": { "id": { "$ref": "#/$defs/id" }, "label": { "type": "string", "maxLength": 32 }, "color": { "$ref": "#/$defs/color" }, "productive": { "type": "boolean", "default": false } } },
    "stateDef": {
      "type": "object",
      "additionalProperties": false,
      "required": ["label"],
      "properties": {
        "value": { "type": "number" },
        "label": { "type": "string", "maxLength": 128, "description": "Shown on overlays (ellipsized to fit); the border payload dictionary carries only the first 24 bytes" },
        "color": { "$ref": "#/$defs/color" },
        "category": { "$ref": "#/$defs/id" }
      }
    },
    "point": {
      "type": "object",
      "additionalProperties": false,
      "required": ["x", "y"],
      "properties": {
        "x": { "type": "number", "minimum": 0, "maximum": 1 },
        "y": { "type": "number", "minimum": 0, "maximum": 1 }
      }
    },
    "color": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$", "description": "#RRGGBB or #RRGGBBAA" },
    "rule": {
      "type": "object",
      "additionalProperties": false,
      "required": ["tagId", "op", "value"],
      "properties": {
        "tagId": { "$ref": "#/$defs/id" },
        "op": { "enum": ["eq", "ne", "lt", "le", "gt", "ge"] },
        "value": { "type": "number" }
      }
    }
  }
}
