# The phyphox remote-interface API.
#
# This file is the source of truth for the REST API that both apps expose while
# an experiment is running. It was derived by reading the two server
# implementations, not the prose documentation:
#
#   Android  RemoteServer.java                 (phyphox-android b4564695, development)
#   iOS      ExperimentWebServer.swift         (phyphox-ios     a0643729, development)
#            WebServerUtilities.swift
#
# It has since been checked against running devices with tools/contract_test.py:
# Android 1.2.1-beta2 (build 1020102) and iOS 1.2.0 (build 17272), both serving
# linear_accelerometer.phyphox. That run corrected two things this file got wrong
# from reading alone - see the Meta schema - and turned up two divergences the
# source reading had missed.
#
# Where the two disagree, the divergence is recorded in ../../inconsistencies.yml
# and referenced from here with `x-phyphox-inconsistency`. The docs build fails
# if such a reference names an entry that does not exist.
#
# Do not resolve a divergence by picking a behavior and writing it down here.
# Which implementation is correct is the maintainer's decision; record the
# divergence with `status: open` and leave it visible. See
# docs/reference/known-inconsistencies.md for what the statuses mean.

openapi: 3.1.1

info:
  title: phyphox remote interface
  version: 1.1.0
  summary: Control a running phyphox experiment and stream its data over HTTP.
  description: |
    When remote access is enabled, phyphox runs a small webserver on the phone. It
    serves the bundled web interface and exposes this API for controlling the
    running experiment and reading its data.

    The API is not tied to the bundled web interface. It is meant to be usable
    from your own software — to combine several phones into one measurement, to
    drive phyphox from LabVIEW, MATLAB or a Python script, or to run it alongside
    other lab equipment.

    **There is no authentication.** Anyone who can reach the phone on the network
    can read the data and start, stop and clear the measurement. Remote access is
    off by default and has to be enabled per session by the user.

    Every response carries `Access-Control-Allow-Origin: *`, so a page served from
    anywhere may read it — which is what makes a browser-based client possible at
    all. See the inconsistencies noted on this page for where that is not yet
    true. That page must itself be served over plain HTTP, though: the app speaks
    HTTP only, and a browser blocks an `http://` request made from an `https://`
    page before CORS is ever consulted.

    ### Parameters, and POST bodies

    Every endpoint accepts **POST** as well as GET, and takes the same parameters
    either way. A POST may carry those parameters in its body in one of two
    formats, chosen by the request's `Content-Type`:

    - `application/json` — a flat JSON object, e.g.
      `{"cmd": "set", "buffer": "height", "value": 42}`. This is the natural form
      for a script.
    - `application/x-www-form-urlencoded` — the classic HTML form encoding, the
      same `name=value&…` you would put in a query string.

    Two rules keep this unambiguous:

    - **Every value is a string.** The whole API is string-valued — a `/get` value
      like `"3|t"` or `"full"`, a `set` value that is parsed as a number. A JSON
      scalar is coerced to its string form before it is used, so
      `{"value": 42}` and `{"value": "42"}` mean the same thing. A JSON body is a
      convenience, not a second type system; nesting is not part of the format.
      (A JSON body must still be valid JSON: a bare `Infinity`/`NaN` or a numeric
      literal too large to represent is a malformed body and answered with `400`.)
    - **Body parameters win over query parameters** of the same name, so
      `POST /control?cmd=stop` with a body of `{"cmd": "start"}` starts the
      measurement.

    A malformed request body (JSON that does not parse) is answered with `400`.
    Endpoints that take no parameters ignore a body. This holds for every
    endpoint — the static interface files (`/`, `/style.css`, `/logo`) accept
    POST too, though there is no reason to use it, and their POST operations are
    not spelled out below.

    **The server is single-threaded and slow.** The bundled web interface
    deliberately serialises its requests and delays the next one by 10 ms. Clients
    should do the same rather than issuing requests in parallel.
  license:
    name: GNU GPL v3
    identifier: GPL-3.0-only

# Swagger UI renders neither of the descriptions below - not the server's and
# not its variables' - so none of this reaches a reader of the API reference
# page, and the block itself is hidden there (see the note in
# ../assets/stylesheets/swagger-ui-overrides.css). Keep it accurate anyway: it
# is part of the specification, and other consumers do read it. Documentation
# that readers must see belongs in `info.description` or on the page.
servers:
  - url: http://{host}:{port}
    description: A phone running phyphox with remote access enabled.
    variables:
      host:
        default: 192.168.0.42
        description: The address phyphox shows when remote access is enabled.
      port:
        default: "8080"
        description: |
          Android defaults to 8080, iOS to 80 — so on iOS the port can usually be
          omitted. If the default port is taken, the app silently walks through
          alternative ports (8080, 8081, …) and announces the one actually used;
          a custom port configured in the app settings (`remoteAccessPort`) is
          used exactly as configured.

tags:
  - name: interface
    description: The bundled web interface. Not needed to control phyphox from your own code.
  - name: data
    description: Reading measurement data and experiment status.
  - name: control
    description: Starting, stopping and feeding values into the experiment.
  - name: metadata
    description: What experiment is running, on what device, and against which clock.

paths:
  /:
    get:
      tags: [interface]
      summary: The bundled web interface
      description: |
        The remote interface itself: an HTML document with the JavaScript that
        drives every other endpoint on this page. It is generated per experiment,
        because it embeds the experiment's view layout.

        Not relevant if you are writing your own client.
      responses:
        "200":
          description: The web interface.
          content:
            text/html:
              schema:
                type: string

  /style.css:
    get:
      tags: [interface]
      summary: Stylesheet for the bundled web interface
      description: |
        Generated per session, because the icons are substituted into it as
        base64-encoded PNGs.
      responses:
        "200":
          description: The stylesheet.
          content:
            text/css:
              schema:
                type: string

  /logo:
    get:
      tags: [interface]
      summary: The phyphox logo
      responses:
        "200":
          description: A PNG image.
          content:
            image/png:
              schema:
                type: string
                format: binary

  /get:
    get:
      tags: [data]
      summary: Read buffer data and experiment status
      operationId: getData
      description: |
        Returns the contents of the requested buffers together with the current
        status of the experiment. This is the endpoint you will poll.

        **Buffers are named by the query parameters themselves.** There is no
        fixed parameter list: every query parameter is read as the name of a
        buffer, and its value says how much of that buffer to return.

        | Value           | Meaning                                                        |
        |-----------------|----------------------------------------------------------------|
        | *(empty)*       | Only the last value. `updateMode` comes back as `single`.       |
        | `full`          | The whole buffer. `updateMode` comes back as `full`.            |
        | *number*        | Every value after the first one exceeding this threshold.       |
        | *number*&#124;*ref* | As above, but the threshold applies to buffer *ref* instead. |

        So `/get?abc&def=full&ghi=42&jkl=23|mno` returns the last value of `abc`,
        all of `def`, everything in `ghi` after the first value above 42, and the
        values of `jkl` at the indices where `mno` exceeds 23. Note that `mno`
        itself is not returned.

        The threshold form is what makes polling cheap. Data usually has a
        monotonic axis — time — so a client that already holds data up to t = 3
        asks for `?time=3&value=3|time` and receives only what is new. Requesting
        `full` every time works but transfers the whole buffer each poll.

        Because values are transferred with about 8 significant digits, the
        server nudges the threshold up by one unit in the last place before
        comparing, so a value already delivered is not delivered again.

        > The `|` **must be percent-encoded as `%7C`.** It is written plainly here
        > for readability, but an unencoded `|` is not valid in a URL and not every
        > HTTP client will fix it for you.

        Buffer names that do not exist in the running experiment are skipped
        rather than reported. The `session` field in the status is how a client
        notices that the user switched experiments and its buffer names have gone
        stale.
      parameters:
        - name: buffers
          in: query
          required: true
          style: form
          explode: true
          description: |
            One parameter per requested buffer, named after the buffer. See the
            table above for the accepted values.
          schema:
            type: object
            additionalProperties:
              type: string
          examples:
            lastValueOnly:
              summary: Last value of two buffers
              value: {accX: "", accY: ""}
            everything:
              summary: Whole buffer
              value: {accX: "full"}
            incremental:
              summary: Only what is new, against a time axis
              value: {t: "3", accX: "3|t"}
      responses: &getResponses
        "200":
          description: Buffer contents and experiment status.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GetResponse"
        "400":
          description: |
            The request was malformed: a threshold that does not parse as a
            number, a threshold referencing a buffer that does not exist, or a
            request body that is not valid JSON. (An unknown buffer in the
            *requested* position is skipped, not rejected.) Carries a JSON
            error object, like every error response.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    post:
      tags: [data]
      summary: Read buffer data and experiment status (POST)
      operationId: getDataPost
      description: |
        Identical to `GET /get`, with the buffer requests in the request body:
        one key per buffer, same values as the query form. This is the natural
        way to poll many buffers at once, and JSON spares you percent-encoding
        the `|` of the threshold form.
      requestBody:
        $ref: "#/components/requestBodies/Parameters"
      responses: *getResponses

  /control:
    get:
      tags: [control]
      summary: Start, stop, clear, or write a value
      operationId: control
      description: |
        Sends a command to the running experiment. Always answers with
        `{"result": true}` or `{"result": false}`; the response says whether the
        command was accepted, not whether it has finished taking effect — except
        for `start`, where the result must reflect whether the measurement
        actually began (see `cmd=start` below).

        - `start` — begin measuring, or begin the countdown if timed run is on.
          An experiment can refuse to start — most commonly because a Bluetooth
          device it needs is not connected yet — and the result is then `false`.
          A client that must be certain can also read `status.measuring` from a
          subsequent `/get`.
        - `stop` — stop measuring.
        - `clear` — discard buffered data, stopping the measurement if it is running.
        - `set` — write a single value into a buffer. This is how the remote
          interface implements edit fields.
        - `trigger` — activate a view element by its index, as if the user had
          tapped it. Used for buttons. The index is the `index` field of the
          element in the view layout embedded in the web interface, counted across
          all views of the experiment.
      parameters:
        - name: cmd
          in: query
          required: true
          schema:
            type: string
            enum: [start, stop, clear, set, trigger]
        - name: buffer
          in: query
          required: false
          description: "`cmd=set`: the buffer to write to."
          schema:
            type: string
        - name: value
          in: query
          required: false
          description: |
            `cmd=set`: the value to write. Must parse as a number and must be
            finite; NaN and infinity are rejected. A buffer holds NaN when empty,
            so writing it explicitly is treated as a mistake, and `/get` reports
            any non-finite value as `null` so an infinity could not be read back.
          schema:
            type: number
        - name: element
          in: query
          required: false
          description: "`cmd=trigger`: index of the view element to activate."
          schema:
            type: integer
            minimum: 0
        - name: clearGroup1
          in: query
          required: false
          description: |
            `cmd=clear`: restrict clearing to named clear groups. Numbered from 1
            upward (`clearGroup1`, `clearGroup2`, …); the first gap ends the list.
            Omit entirely to clear everything.
          schema:
            type: string
      responses: &controlResponses
        "200":
          description: |
            Whether the command was accepted. Note that a rejected command also
            answers 200 — check the `result` field, not the status code.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ControlResult"
    post:
      tags: [control]
      summary: Start, stop, clear, or write a value (POST)
      operationId: controlPost
      description: |
        Identical to `GET /control`, with the command and its parameters in the
        request body, e.g. `{"cmd": "set", "buffer": "height", "value": 1.87}`.
      requestBody:
        $ref: "#/components/requestBodies/Parameters"
      responses: *controlResponses

  /set:
    post:
      tags: [control]
      summary: Write multiple buffers as JSON
      operationId: set
      description: |
        Writes values into one or more data containers in a single request.
        This is the bulk, array-valued counterpart of `/control?cmd=set`
        (which writes one finite value into one buffer): it can fill several
        buffers atomically and can write the non-finite values a buffer may
        legitimately hold.

        Unlike every other endpoint, `/set` takes **only a JSON body** — the
        payload is structured, so the flat query/form parameter convention
        does not apply. A request without a valid JSON body of the documented
        shape (including a GET, or a form-encoded body) is answered with
        `{"result": false}` and an error message; a body that is not valid
        JSON at all is answered with `400`, like everywhere else.

        Array entries may be JSON numbers, `null`, or strings in the file
        format's number lexical space (see the number-invalid-value rule of
        the format): `[1, 2.5, null, "nan", "Infinity", "-infinity"]`.
        `null` writes NaN — it is exactly the representation `/get` uses for
        every non-finite value, so `/get` output can be fed back into `/set`
        unchanged. The strings exist to write the *distinguishable* special
        values, which `null` cannot express; a string outside the lexical
        space (`"inf"`, a decimal comma, hex notation) rejects the request,
        exactly as it would reject an experiment file.

        The request is **atomic**: everything is validated first — every
        buffer name must exist and every entry must be well-formed — and on
        any error nothing is written and the response is
        `{"result": false}` with an `error` message. On success the values
        are appended in array order, after clearing the buffer first unless
        `mode` is `append`. Normal buffer semantics apply: a buffer keeps its
        newest `size` values, and a static buffer is written like any other —
        a caller names its target buffers deliberately and is expected to
        know the experiment's internals. A successful write marks the
        analysis as having new data, exactly like `/control?cmd=set`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [buffers]
              properties:
                buffers:
                  type: object
                  description: |
                    Container names mapped to the values to write. An empty
                    object is allowed and writes nothing.
                  additionalProperties:
                    type: array
                    items:
                      oneOf:
                        - type: number
                        - type: "null"
                        - type: string
                          pattern: '^([+-]?([0-9]+(\.[0-9]*)?|\.[0-9]+)([eE][+-]?[0-9]+)?|[nN][aA][nN]|[+-]?[iI][nN][fF][iI][nN][iI][tT][yY])$'
                mode:
                  type: string
                  enum: [replace, append]
                  default: replace
                  description: |
                    `replace` clears each named buffer before writing its
                    values; `append` keeps the existing contents.
      responses:
        "200":
          description: |
            Whether the write was applied. A rejected request also answers
            200 — check the `result` field, not the status code. Only a body
            that is not parseable JSON at all answers 400.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SetResult"

  /export:
    get:
      tags: [data]
      summary: Download all recorded data as a file
      operationId: export
      description: |
        Runs the experiment's own export and returns the resulting file, with a
        `Content-Disposition` header so a browser saves it under a sensible name.
        The export sets and their column labels are fixed by the experiment; see
        `/config`.

        Exporting a **running** experiment is allowed and returns a consistent
        snapshot: the data is collected under the same lock the analysis and
        sensor threads write through, once around all sets, so the sets agree
        with each other and no set can be caught mid-update. Which moment the
        snapshot represents is not specified - it is whatever the buffers held
        when the export was served. Both apps had to be corrected to guarantee
        this (2026-08-26): the unguarded version could return a set that was
        empty or short while the others were complete, and on Android it could
        fail the request outright.

        The number of rows a set exports is that of its longest column, with
        shorter columns padded with NaN - see the file format's export block.

        The format is selected by index into a fixed list, which is the same on
        both platforms:

        | Index | Format                          |
        |-------|---------------------------------|
        | 0     | Excel (`.xlsx`)                 |
        | 1     | CSV (comma, decimal point)      |
        | 2     | CSV (tabulator, decimal point)  |
        | 3     | CSV (semicolon, decimal point)  |
        | 4     | CSV (tabulator, decimal comma)  |
        | 5     | CSV (semicolon, decimal comma)  |
      parameters:
        - name: format
          in: query
          required: true
          description: Index into the format table above.
          schema:
            type: integer
            minimum: 0
            maximum: 5
      responses: &exportResponses
        "200":
          description: |
            The exported file. Note that an out-of-range or missing format also
            answers 200, with a JSON error object rather than a file.
          content:
            application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
              schema:
                type: string
                format: binary
            application/zip:
              schema:
                type: string
                format: binary
              # Formats 1-5 are a zip with one .csv per export set, never a
              # bare .csv: this endpoint runs the experiment's full export
              # (Android DataExport.getType(false) -> application/zip, iOS
              # runExport(singleSet: false)). The bare text/csv form is the
              # graph view's own export inside the app, which has no
              # remote-interface path.
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    post:
      tags: [data]
      summary: Download all recorded data as a file (POST)
      operationId: exportPost
      description: |
        Identical to `GET /export`, with `format` in the request body.
      requestBody:
        $ref: "#/components/requestBodies/Parameters"
      responses: *exportResponses

  /config:
    get:
      tags: [metadata]
      summary: What experiment is running
      operationId: config
      description: |
        Describes the experiment currently loaded: how to identify it, what
        buffers exist, where the data comes from and which buffers the author
        considered worth exporting.

        The `export` sets are the best starting point for a client. Every buffer
        in the experiment appears in `buffers`, but many hold intermediate results
        that are cleared or rewritten during analysis. The export sets were chosen
        by the experiment's author as the data worth keeping.

        Available since [phyphox 1.1.6](../reference/version-history/1.1.6.md).
      responses: &configResponses
        "200":
          description: The experiment configuration.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Config"
    post:
      tags: [metadata]
      summary: What experiment is running (POST)
      operationId: configPost
      description: |
        Identical to `GET /config`. The endpoint takes no parameters, so a
        request body is ignored.
      responses: *configResponses

  /meta:
    get:
      tags: [metadata]
      summary: Device and software information
      operationId: meta
      description: |
        Returns the device metadata, the same set an experiment can send through a
        network connection — see
        [network connections](../file-format/network-connections.md#metadata) —
        with one exception: `uniqueId` is never served here, because it is a
        per-user identifier and this endpoint has no authentication.

        Treat the key set as open: it grows with the app, and it is not the same
        on both platforms.

        **A value the platform cannot supply is not reported as an empty string.**
        Android omits the key entirely, iOS sends JSON `null` — so a client must
        handle a key being absent *and* being null, and must not assume that a key
        seen on one device exists on another.

        Available since [phyphox 1.1.8](../reference/version-history/1.1.8.md).
      responses: &metaResponses
        "200":
          description: Device metadata.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Meta"
      x-phyphox-inconsistency:
        - meta-sensors
    post:
      tags: [metadata]
      summary: Device and software information (POST)
      operationId: metaPost
      description: |
        Identical to `GET /meta`. The endpoint takes no parameters, so a
        request body is ignored.
      responses: *metaResponses
      x-phyphox-inconsistency:
        - meta-sensors

  /time:
    get:
      tags: [metadata]
      summary: Map experiment time onto the system clock
      operationId: time
      description: |
        Returns the start and pause events of the current measurement, each with
        both clocks, so that sensor timestamps can be placed on a wall clock.

        Sensor data is timestamped in *experiment time*: seconds since the
        measurement first started, with pauses skipped. That comes from the
        device's monotonic clock, which is not the system clock. On each event
        phyphox reads both clocks back to back, so each entry pairs them to within
        a few milliseconds — but the two clocks drift, and the system clock is
        adjusted in the background by the operating system, so interpolating
        between distant events is not reliable.

        Available since [phyphox 1.1.8](../reference/version-history/1.1.8.md).
      responses: &timeResponses
        "200":
          description: Time reference events, oldest first.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/TimeEvent"
    post:
      tags: [metadata]
      summary: Map experiment time onto the system clock (POST)
      operationId: timePost
      description: |
        Identical to `GET /time`. The endpoint takes no parameters, so a
        request body is ignored.
      responses: *timeResponses

  /res:
    get:
      tags: [interface]
      summary: Fetch a resource bundled with the experiment
      operationId: res
      description: |
        Serves a file that came with the experiment — in practice, an image named
        by an `image` view element. Only files the experiment actually references
        are served; anything else is refused.
      parameters:
        - name: src
          in: query
          required: true
          description: File name as written in the view element.
          schema:
            type: string
      responses: &resResponses
        "200":
          description: |
            The file. An unknown or missing name also answers 200, with a JSON
            error object.
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    post:
      tags: [interface]
      summary: Fetch a resource bundled with the experiment (POST)
      operationId: resPost
      description: |
        Identical to `GET /res`, with `src` in the request body.
      requestBody:
        $ref: "#/components/requestBodies/Parameters"
      responses: *resResponses

components:
  requestBodies:
    Parameters:
      description: |
        The endpoint's parameters as a request body: the same names and values
        the query string takes, in one of the two formats described under
        "Parameters, and POST bodies" at the top of this page. Parameters may
        still be given in the query as well; on a name collision the body wins.
        A JSON body that does not parse is answered with `400` and a
        JSON error object.
      required: false
      content:
        application/json:
          schema:
            type: object
            description: |
              A flat object. Values are coerced to strings before use, so
              `{"value": 42}` and `{"value": "42"}` are equivalent; nested
              objects and arrays are not part of the format.
            additionalProperties:
              type: [string, number, boolean]
        application/x-www-form-urlencoded:
          schema:
            type: object
            additionalProperties:
              type: string
  schemas:
    GetResponse:
      type: object
      required: [buffer, status]
      properties:
        buffer:
          type: object
          description: |
            One entry per requested buffer that exists. Buffers that do not exist
            are silently absent.
          additionalProperties:
            $ref: "#/components/schemas/Buffer"
        status:
          $ref: "#/components/schemas/Status"

    Buffer:
      type: object
      required: [size, updateMode, buffer]
      properties:
        size:
          type: integer
          description: |
            The buffer's declared capacity — neither how much of it is filled nor
            how many values this response carries.
        updateMode:
          type: string
          enum: [full, partial, single]
          description: |
            Whether this response carries the whole buffer, the part of it that
            passed the threshold, or only the last value.
        buffer:
          type: array
          description: |
            The values. `null` stands for a value that is NaN or infinite.

            JSON has no representation for either, so `null` is what the format
            offers, and the substitution is **lossy**: NaN, `+∞` and `−∞` all
            arrive as `null` and cannot be told apart. The bundled web interface
            converts every `null` back to NaN before plotting, which is the right
            reading for the case that actually occurs — a buffer position with no
            valid measurement.

            Non-finite values cannot simply be dropped, because a client pairing
            two buffers by index relies on the indices lining up.

            Note the asymmetry with `/control?cmd=set`: that value arrives as a
            query-string token rather than as JSON, so `Infinity` and `NaN` *can*
            be written there. Both are rejected, partly for this reason — a value
            the API cannot report back should not be accepted in the first place.
          items:
            type: [number, "null"]

    Status:
      type: object
      required: [session, measuring, timedRun, countDown]
      properties:
        session:
          type: string
          description: |
            Identifies the session. A change means the user switched experiments,
            so previously requested buffer names and any data held by the client
            should be discarded.
        measuring:
          type: boolean
          description: Whether the experiment is currently running.
        timedRun:
          type: boolean
          description: Whether the timed-run function is enabled.
        countDown:
          type: number
          description: |
            Milliseconds remaining on the timed run, or 0 when no countdown is
            active. If `measuring` is true the measurement stops when it elapses;
            otherwise it starts.

    ControlResult:
      type: object
      required: [result]
      properties:
        result:
          type: boolean
          description: |
            Whether the command was accepted. For `cmd=start` this means the
            measurement actually began: a start the experiment refuses answers
            `false`.

    SetResult:
      type: object
      required: [result]
      properties:
        result:
          type: boolean
          description: Whether the write was validated and applied.
        error:
          type: string
          description: |
            Present when `result` is false: what was wrong with the request.
            Wording is implementation-defined; scripts should branch on
            `result`, not on this text.

    Config:
      type: object
      required: [crc32, title, localTitle, category, localCategory, buffers, inputs, export]
      properties:
        crc32:
          type: string
          description: |
            CRC32 of the whole experiment file as lowercase hex, which identifies
            a specific configuration exactly. Not zero-padded.
        title:
          type: string
          description: Title in the experiment's base language.
        localTitle:
          type: string
          description: Title in the language currently displayed.
        category:
          type: string
          description: Category in the experiment's base language.
        localCategory:
          type: string
          description: Category in the language currently displayed.
        buffers:
          type: array
          description: |
            Every buffer in the experiment. Existing here is no promise of useful
            content — see `export` for the buffers the author picked out.

            **The order carries no meaning.** One platform happens to list them in
            the order the experiment declares them and the other in an unspecified
            order; both are valid, so do not write a client that depends on it.
          items:
            type: object
            required: [name, size]
            properties:
              name:
                type: string
                description: Name to use in `/get`.
              size:
                type: integer
                description: Declared capacity.
        inputs:
          type: array
          description: |
            The data sources feeding the experiment. Do not assume the named
            buffers are safe to poll — an experiment may clear them during
            analysis.
          items:
            $ref: "#/components/schemas/ConfigInput"
        export:
          type: array
          description: The export sets defined by the experiment.
          items:
            type: object
            required: [set, sources]
            properties:
              set:
                type: string
              sources:
                type: array
                items:
                  type: object
                  required: [label, buffer]
                  properties:
                    label:
                      type: string
                      description: Column title used in the exported file.
                    buffer:
                      type: string

    ConfigInput:
      type: object
      required: [source]
      properties:
        source:
          type: string
          description: |
            `audio` for the microphone, `location` for GPS, `bluetooth` for a BLE
            device, or a sensor name as used in the file format.
          examples: [audio, location, bluetooth, accelerometer, linear_acceleration,
                     gravity, gyroscope, magnetic_field, pressure, light, proximity,
                     temperature, humidity, attitude, custom]
        outputs:
          type: array
          description: |
            Which buffer each component of the source writes to, as one
            single-entry object per component. Absent for `bluetooth`, whose
            mapping is defined by the experiment rather than by the input.
          items:
            type: object
            additionalProperties:
              type: string

    Meta:
      type: object
      description: |
        Device metadata as string values. The key set is open and platform
        dependent; the keys below are the ones both platforms serve.
      additionalProperties:
        type: [string, "null"]
      properties:
        sensors:
          type: object
          description: |
            Per-sensor metadata, keyed by sensor name. Served by Android only —
            see the inconsistency noted on this endpoint. Note the capitalised
            keys, which differ from the lowercase spelling used elsewhere for
            these concepts; the flat identifiers a network connection sends
            (`accelerometerName` and so on) are capitalised the same way. On
            Android the meaning and accuracy of these values vary by device.
          additionalProperties:
            type: object
            additionalProperties:
              type: [string, "null"]
            properties:
              Name: {type: [string, "null"]}
              Vendor: {type: [string, "null"]}
              Range: {type: [string, "null"]}
              Resolution: {type: [string, "null"]}
              MinDelay: {type: [string, "null"]}
              MaxDelay: {type: [string, "null"]}
              Power: {type: [string, "null"]}
              Version: {type: [string, "null"]}
        version:
          type: [string, "null"]
          description: phyphox version.
        build:
          type: [string, "null"]
          description: phyphox build number.
        fileFormat:
          type: [string, "null"]
          description: Highest file-format version this app can read.
        deviceModel:
          type: [string, "null"]
        deviceBrand:
          type: [string, "null"]
        deviceBoard:
          type: [string, "null"]
        deviceManufacturer:
          type: [string, "null"]
        deviceBaseOS:
          type: [string, "null"]
        deviceCodename:
          type: [string, "null"]
        deviceRelease:
          type: [string, "null"]
        depthFrontSensor:
          type: [string, "null"]
        depthFrontResolution:
          type: [string, "null"]
        depthFrontRate:
          type: [string, "null"]
        depthBackSensor:
          type: [string, "null"]
        depthBackResolution:
          type: [string, "null"]
        depthBackRate:
          type: [string, "null"]

    TimeEvent:
      type: object
      required: [event, experimentTime, systemTime]
      properties:
        event:
          type: string
          enum: [START, PAUSE]
          description: |
            Whether the measurement started or paused. `CLEAR` exists internally
            but never reaches this endpoint, because clearing also empties this
            list.
        experimentTime:
          type: number
          description: Seconds since the measurement first started, excluding pauses.
        systemTime:
          type: number
          description: Seconds since the Unix epoch.

    Error:
      type: object
      required: [error]
      properties:
        error:
          type: string
          description: Human-readable reason. Not machine-readable; do not match on it.
