{
  "openapi": "3.1.0",
  "info": {
    "title": "tiny camera worker",
    "version": "0.1.0",
    "description": "Eyes for a tiny. One REST surface over a camera host (Nicla Vision, rover cam, arm eye): stills, short clips, FOMO detections, ToF distance, IMU. Cameras are addressed by ROSTER NAME, never by device index — indices slide when a device is unplugged, names do not. Every image comes back as a hosted https URL the agent can see."
  },
  "servers": [
    { "url": "https://{host}", "variables": { "host": { "default": "camera.cagatay.my", "description": "Camera host (tunnel or LAN name)" } } }
  ],
  "security": [ { "bearer": [] } ],
  "tags": [
    { "name": "camera", "description": "See" },
    { "name": "sensors", "description": "Feel (ToF, IMU)" },
    { "name": "system", "description": "Health" }
  ],
  "paths": {
    "/health": {
      "get": {
        "operationId": "camera_health",
        "tags": ["system"],
        "summary": "Is the camera host alive, which cameras are up, battery/thermal",
        "security": [],
        "responses": { "200": { "description": "ok", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Health" } } } } }
      }
    },
    "/cameras": {
      "get": {
        "operationId": "camera_list",
        "tags": ["camera"],
        "summary": "Roster of cameras by name, with liveness and identity-drift flags",
        "description": "Call this before choosing a camera. `identity_drift=true` means the configured index now answers with a DIFFERENT physical camera — refuse to trust its frames until re-bound.",
        "responses": { "200": { "description": "roster", "content": { "application/json": { "schema": { "type": "object", "properties": { "cameras": { "type": "array", "items": { "$ref": "#/components/schemas/Camera" } } }, "required": ["cameras"] } } } } }
      }
    },
    "/cameras/{name}/snapshot": {
      "post": {
        "operationId": "camera_snapshot",
        "tags": ["camera"],
        "summary": "Take ONE still from the named camera and return a hosted image URL",
        "parameters": [ { "$ref": "#/components/parameters/CameraName" } ],
        "requestBody": { "required": false, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SnapshotRequest" } } } },
        "responses": {
          "200": { "description": "still", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Frame" } } } },
          "404": { "$ref": "#/components/responses/NoSuchCamera" },
          "409": { "$ref": "#/components/responses/Refused" },
          "503": { "$ref": "#/components/responses/CameraDead" }
        }
      }
    },
    "/cameras/{name}/clip": {
      "post": {
        "operationId": "camera_clip",
        "tags": ["camera"],
        "summary": "Record a short clip (1-30 s) and return a hosted video/GIF URL plus sampled keyframes",
        "description": "Blocks for the whole recording. Keyframes are what the agent can SEE; the clip URL is for the human.",
        "parameters": [ { "$ref": "#/components/parameters/CameraName" } ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "seconds": { "type": "integer", "minimum": 1, "maximum": 30, "default": 5 }, "keyframes": { "type": "integer", "minimum": 1, "maximum": 8, "default": 4, "description": "How many stills to sample from the clip" }, "reason": { "type": "string", "maxLength": 200 } } } } } },
        "responses": {
          "200": { "description": "clip", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Clip" } } } },
          "404": { "$ref": "#/components/responses/NoSuchCamera" },
          "409": { "$ref": "#/components/responses/Refused" },
          "503": { "$ref": "#/components/responses/CameraDead" }
        }
      }
    },
    "/cameras/{name}/detect": {
      "post": {
        "operationId": "camera_detect",
        "tags": ["camera"],
        "summary": "Run the on-device detector (FOMO / classifier) on a fresh frame — boxes, labels, confidences",
        "parameters": [ { "$ref": "#/components/parameters/CameraName" } ],
        "requestBody": { "required": false, "content": { "application/json": { "schema": { "type": "object", "properties": { "min_confidence": { "type": "number", "minimum": 0, "maximum": 1, "default": 0.5 }, "labels": { "type": "array", "items": { "type": "string" }, "description": "Only report these labels (empty = all)" }, "annotate": { "type": "boolean", "default": true, "description": "Also return the frame with boxes drawn" } } } } } },
        "responses": {
          "200": { "description": "detections", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Detections" } } } },
          "404": { "$ref": "#/components/responses/NoSuchCamera" },
          "503": { "$ref": "#/components/responses/CameraDead" }
        }
      }
    },
    "/cameras/{name}/stream": {
      "get": {
        "operationId": "camera_stream_info",
        "tags": ["camera"],
        "summary": "Where the live MJPEG/WebRTC stream is (URL, fps, watchdog state) — for a human viewer, not the agent",
        "parameters": [ { "$ref": "#/components/parameters/CameraName" } ],
        "responses": { "200": { "description": "stream info", "content": { "application/json": { "schema": { "type": "object", "properties": { "mjpeg_url": { "type": "string", "format": "uri" }, "webrtc_url": { "type": "string", "format": "uri" }, "fps": { "type": "number" }, "last_frame_age_ms": { "type": "integer" }, "state": { "type": "string", "enum": ["live", "stale", "down"] } }, "required": ["state"] } } } } }
      }
    },
    "/cameras/{name}/settings": {
      "get": {
        "operationId": "camera_settings_get",
        "tags": ["camera"],
        "summary": "Current resolution / fps / exposure / flip / lamp",
        "parameters": [ { "$ref": "#/components/parameters/CameraName" } ],
        "responses": { "200": { "description": "settings", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Settings" } } } } }
      },
      "patch": {
        "operationId": "camera_settings_set",
        "tags": ["camera"],
        "summary": "Change resolution / fps / exposure / flip / lamp (partial update)",
        "parameters": [ { "$ref": "#/components/parameters/CameraName" } ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Settings" } } } },
        "responses": { "200": { "description": "applied settings", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Settings" } } } }, "422": { "description": "unsupported value (e.g. resolution the sensor cannot do)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }
      }
    },
    "/cameras/{name}/bind": {
      "post": {
        "operationId": "camera_bind",
        "tags": ["camera"],
        "summary": "Re-bind a roster name to the physical camera currently answering (clears identity_drift). Human-confirmed action.",
        "parameters": [ { "$ref": "#/components/parameters/CameraName" } ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "confirm": { "type": "boolean", "const": true }, "device_id": { "type": "string", "description": "Stable hardware id (USB serial / MAC) to pin, from camera_list" } }, "required": ["confirm", "device_id"] } } } },
        "responses": { "200": { "description": "bound", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Camera" } } } } }
      }
    },
    "/sensors/distance": {
      "get": {
        "operationId": "sensor_distance",
        "tags": ["sensors"],
        "summary": "Time-of-flight distance in front of the camera (mm)",
        "parameters": [ { "name": "camera", "in": "query", "schema": { "type": "string" }, "description": "Which camera's ToF (default: the one flagged primary)" } ],
        "responses": { "200": { "description": "distance", "content": { "application/json": { "schema": { "type": "object", "properties": { "mm": { "type": "integer" }, "valid": { "type": "boolean", "description": "false = out of range / no return" }, "at": { "type": "string", "format": "date-time" } }, "required": ["mm", "valid", "at"] } } } } }
      }
    },
    "/sensors/imu": {
      "get": {
        "operationId": "sensor_imu",
        "tags": ["sensors"],
        "summary": "Orientation (roll/pitch/yaw) and motion flag from the board IMU",
        "responses": { "200": { "description": "imu", "content": { "application/json": { "schema": { "type": "object", "properties": { "roll": { "type": "number" }, "pitch": { "type": "number" }, "yaw": { "type": "number" }, "moving": { "type": "boolean" }, "accel_g": { "type": "array", "items": { "type": "number" }, "minItems": 3, "maxItems": 3 }, "at": { "type": "string", "format": "date-time" } }, "required": ["roll", "pitch", "moving", "at"] } } } } }
      }
    }
  },
  "components": {
    "securitySchemes": { "bearer": { "type": "http", "scheme": "bearer", "description": "CAMERA_WORKER_TOKEN" } },
    "parameters": {
      "CameraName": { "name": "name", "in": "path", "required": true, "schema": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]{0,31}$" }, "description": "Roster name from camera_list, e.g. nicla-eye, rover-front, arm-wrist" }
    },
    "responses": {
      "NoSuchCamera": { "description": "name not in roster", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "CameraDead": { "description": "camera listed but not delivering frames (frame age > watchdog)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Refused": { "description": "refused: identity_drift, privacy shutter, or thermal limit — the `reason` sentence says which and which override flag (if any) exists", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
    },
    "schemas": {
      "Error": { "type": "object", "properties": { "error": { "type": "string" }, "reason": { "type": "string" }, "override_flag": { "type": "string", "enum": ["ignore_identity_drift", "ignore_thermal"] } }, "required": ["error"] },
      "Health": { "type": "object", "properties": { "ok": { "type": "boolean" }, "host": { "type": "string" }, "uptime_s": { "type": "integer" }, "cameras_live": { "type": "integer" }, "cameras_total": { "type": "integer" }, "battery_pct": { "type": ["integer", "null"] }, "thermal": { "type": "string", "enum": ["nominal", "warm", "hot", "critical"] }, "version": { "type": "string" } }, "required": ["ok", "cameras_live", "cameras_total"] },
      "Camera": { "type": "object", "properties": { "name": { "type": "string" }, "kind": { "type": "string", "enum": ["nicla-vision", "usb", "csi", "ip", "phone"] }, "device_id": { "type": "string", "description": "Stable hardware id the name is bound to" }, "index": { "type": ["integer", "null"], "description": "Current OS index — informational only, may slide" }, "primary": { "type": "boolean" }, "state": { "type": "string", "enum": ["live", "stale", "missing", "drift"] }, "identity_drift": { "type": "boolean" }, "last_frame_age_ms": { "type": ["integer", "null"] }, "resolution": { "type": "string", "example": "320x240" }, "has_tof": { "type": "boolean" }, "has_lamp": { "type": "boolean" }, "detector": { "type": ["string", "null"], "description": "Loaded model name, e.g. fomo-cups-v3" } }, "required": ["name", "kind", "state", "identity_drift"] },
      "SnapshotRequest": { "type": "object", "properties": { "reason": { "type": "string", "maxLength": 200, "description": "Shown on the host's LED/log while capturing" }, "lamp": { "type": "boolean", "description": "Flash the lamp for this shot if the camera has one" }, "max_width": { "type": "integer", "minimum": 64, "maximum": 1920, "default": 768 }, "ignore_identity_drift": { "type": "boolean", "default": false, "description": "Explicit admission: capture even though a different camera answers this name" } } },
      "Frame": { "type": "object", "properties": { "camera": { "type": "string" }, "image_url": { "type": "string", "format": "uri", "description": "Hosted https JPEG — the agent can SEE this" }, "width": { "type": "integer" }, "height": { "type": "integer" }, "bytes": { "type": "integer" }, "captured_at": { "type": "string", "format": "date-time" }, "distance_mm": { "type": ["integer", "null"], "description": "ToF reading at capture, when the camera has one" }, "frame_index": { "type": ["integer", "null"], "description": "Position in the host's recording stream (ECoT sidecar binding)" } }, "required": ["camera", "image_url", "width", "height", "captured_at"] },
      "Clip": { "type": "object", "properties": { "camera": { "type": "string" }, "clip_url": { "type": "string", "format": "uri" }, "mime": { "type": "string", "enum": ["video/mp4", "image/gif"] }, "seconds": { "type": "number" }, "fps": { "type": "number" }, "keyframes": { "type": "array", "items": { "$ref": "#/components/schemas/Frame" } }, "started_at": { "type": "string", "format": "date-time" } }, "required": ["camera", "clip_url", "mime", "seconds", "keyframes"] },
      "Detection": { "type": "object", "properties": { "label": { "type": "string" }, "confidence": { "type": "number" }, "box": { "type": "object", "description": "Normalized 0-1, origin top-left", "properties": { "x": { "type": "number" }, "y": { "type": "number" }, "w": { "type": "number" }, "h": { "type": "number" } }, "required": ["x", "y", "w", "h"] }, "centroid": { "type": "array", "items": { "type": "number" }, "minItems": 2, "maxItems": 2 } }, "required": ["label", "confidence", "box"] },
      "Detections": { "type": "object", "properties": { "camera": { "type": "string" }, "model": { "type": "string" }, "inference_ms": { "type": "integer" }, "detections": { "type": "array", "items": { "$ref": "#/components/schemas/Detection" } }, "frame": { "$ref": "#/components/schemas/Frame" }, "annotated_url": { "type": ["string", "null"], "format": "uri" } }, "required": ["camera", "model", "detections", "frame"] },
      "Settings": { "type": "object", "properties": { "resolution": { "type": "string", "enum": ["160x120", "320x240", "640x480", "1280x720"] }, "fps": { "type": "integer", "minimum": 1, "maximum": 30 }, "exposure": { "type": "string", "enum": ["auto", "low", "mid", "high"] }, "flip": { "type": "string", "enum": ["none", "h", "v", "hv"] }, "lamp": { "type": "boolean" }, "privacy_shutter": { "type": "boolean", "description": "true = refuse all captures until cleared by a human" } } }
    }
  }
}
