@fleetless/contracts 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/artifacts/constants.json +30 -4
  3. package/artifacts/openapi.json +580 -49
  4. package/artifacts/routes.json +93 -2
  5. package/artifacts/schema/apply-error.schema.json +2 -1
  6. package/artifacts/schema/asset-list-response.schema.json +77 -12
  7. package/artifacts/schema/asset-sync-status.schema.json +35 -8
  8. package/artifacts/schema/asset.schema.json +2 -3
  9. package/artifacts/schema/assets-clear-response.schema.json +23 -0
  10. package/artifacts/schema/bridge-asset-progress.schema.json +14 -8
  11. package/artifacts/schema/bridge-config-applied.schema.json +2 -1
  12. package/artifacts/schema/bridge-link-mode.schema.json +36 -0
  13. package/artifacts/schema/bridge-state.schema.json +6 -1
  14. package/artifacts/schema/client-robot-list-item.schema.json +6 -1
  15. package/artifacts/schema/client-robot-list-response.schema.json +6 -1
  16. package/artifacts/schema/cloud-config.schema.json +90 -5
  17. package/artifacts/schema/cloud-hello-ok.schema.json +45 -0
  18. package/artifacts/schema/cloud-ping.schema.json +27 -1
  19. package/artifacts/schema/config-draft-response.schema.json +90 -5
  20. package/artifacts/schema/config-state.schema.json +2 -1
  21. package/artifacts/schema/config-version-response.schema.json +90 -5
  22. package/artifacts/schema/datapoint-config.schema.json +5 -0
  23. package/artifacts/schema/datapoint-frame.schema.json +4 -0
  24. package/artifacts/schema/datapoint-list-response.schema.json +2 -2
  25. package/artifacts/schema/joint-state-put-request.schema.json +23 -0
  26. package/artifacts/schema/joint-state-put-response.schema.json +24 -0
  27. package/artifacts/schema/org-quota-usage-counts.schema.json +0 -5
  28. package/artifacts/schema/org-quota-usage.schema.json +1 -12
  29. package/artifacts/schema/org-quotas.schema.json +1 -7
  30. package/artifacts/schema/robot-config-doc.schema.json +90 -5
  31. package/artifacts/schema/robot-deletion-summary.schema.json +2 -1
  32. package/artifacts/schema/robot-detail-response.schema.json +63 -2
  33. package/artifacts/schema/robot-list-item.schema.json +15 -1
  34. package/artifacts/schema/robot-list-response.schema.json +15 -1
  35. package/artifacts/schema/robot-token-rotate-response.schema.json +15 -0
  36. package/artifacts/schema-outgoing/bridge-asset-progress.schema.json +14 -8
  37. package/artifacts/schema-outgoing/bridge-config-applied.schema.json +2 -1
  38. package/artifacts/schema-outgoing/bridge-link-mode.schema.json +37 -0
  39. package/artifacts/schema-outgoing/datapoint-frame.schema.json +4 -0
  40. package/dist/assets.d.ts +85 -50
  41. package/dist/assets.js +152 -62
  42. package/dist/audit.d.ts +1 -1
  43. package/dist/audit.js +1 -1
  44. package/dist/client-robots.d.ts +2 -0
  45. package/dist/common.d.ts +10 -0
  46. package/dist/common.js +16 -1
  47. package/dist/config.d.ts +69 -1
  48. package/dist/config.js +86 -6
  49. package/dist/errors.d.ts +1 -1
  50. package/dist/errors.js +1 -8
  51. package/dist/index.d.ts +8 -8
  52. package/dist/index.js +4 -4
  53. package/dist/protocol.d.ts +150 -71
  54. package/dist/protocol.js +144 -87
  55. package/dist/rest.d.ts +137 -35
  56. package/dist/rest.js +98 -66
  57. package/dist/routes.js +57 -8
  58. package/package.json +1 -1
  59. package/artifacts/schema/bridge-pressure.schema.json +0 -292
@@ -5022,6 +5022,112 @@
5022
5022
  }
5023
5023
  }
5024
5024
  },
5025
+ "/api/robots/{id}/token/rotate": {
5026
+ "post": {
5027
+ "operationId": "post_api_robots_id_token_rotate",
5028
+ "summary": "Mints a new bridge token for the robot and invalidates the old one.",
5029
+ "tags": [
5030
+ "robots"
5031
+ ],
5032
+ "security": [
5033
+ {
5034
+ "developerSession": []
5035
+ }
5036
+ ],
5037
+ "parameters": [
5038
+ {
5039
+ "name": "id",
5040
+ "in": "path",
5041
+ "required": true,
5042
+ "description": "The robot's uuid, as returned by `POST /api/robots` or listed by `GET /api/robots`.",
5043
+ "schema": {
5044
+ "type": "string"
5045
+ }
5046
+ }
5047
+ ],
5048
+ "responses": {
5049
+ "201": {
5050
+ "description": "Success.",
5051
+ "content": {
5052
+ "application/json": {
5053
+ "schema": {
5054
+ "$ref": "#/components/schemas/robot-token-rotate-response"
5055
+ }
5056
+ }
5057
+ }
5058
+ },
5059
+ "default": {
5060
+ "description": "An error envelope. Codes this route is known to answer: `unauthorized`, `token_expired`, `token_revoked`, `tier_required`, `invalid_uuid`, `not_found`.",
5061
+ "content": {
5062
+ "application/json": {
5063
+ "schema": {
5064
+ "$ref": "#/components/schemas/api-error"
5065
+ }
5066
+ }
5067
+ }
5068
+ }
5069
+ },
5070
+ "description": "Owner tier, behind the org-scoped lookup, so a developer-tier admin sees the `404` a stranger would for a robot outside their org rather than a tier refusal that confirms the id exists. `token` is the only moment the new secret exists outside the caller's hands — the cloud stores a hash — so a caller who loses it rotates again. Audited as `robot.token_rotated`, with no `details`: the one interesting value here is the token. \n\n**It stops the bridge that is connected right now.** The old secret is gone the instant the hash is replaced, so the cloud closes that socket with `CLOSE_TOKEN_ROTATED` rather than leaving a bridge speaking on a credential nothing would accept again. A bridge that does not know the code reconnects and is refused at hello as `invalid_token`, which is the honest answer and ends the same way. **The robot is offline until somebody puts the new token on it** — this is a deliberate interruption, not a background rekey, and a fleet cannot be rotated without a visit to each robot."
5071
+ }
5072
+ },
5073
+ "/api/robots/{id}/urdf/joint-state": {
5074
+ "put": {
5075
+ "operationId": "put_api_robots_id_urdf_joint_state",
5076
+ "summary": "Chooses the datapoint whose joint positions move the robot's URDF, or clears it.",
5077
+ "tags": [
5078
+ "robots"
5079
+ ],
5080
+ "security": [
5081
+ {
5082
+ "developerSession": []
5083
+ }
5084
+ ],
5085
+ "parameters": [
5086
+ {
5087
+ "name": "id",
5088
+ "in": "path",
5089
+ "required": true,
5090
+ "description": "The robot's uuid, as returned by `POST /api/robots` or listed by `GET /api/robots`.",
5091
+ "schema": {
5092
+ "type": "string"
5093
+ }
5094
+ }
5095
+ ],
5096
+ "responses": {
5097
+ "200": {
5098
+ "description": "Success.",
5099
+ "content": {
5100
+ "application/json": {
5101
+ "schema": {
5102
+ "$ref": "#/components/schemas/joint-state-put-response"
5103
+ }
5104
+ }
5105
+ }
5106
+ },
5107
+ "default": {
5108
+ "description": "An error envelope. Codes this route is known to answer: `unauthorized`, `token_expired`, `token_revoked`, `invalid_uuid`, `not_found`, `validation_error`.",
5109
+ "content": {
5110
+ "application/json": {
5111
+ "schema": {
5112
+ "$ref": "#/components/schemas/api-error"
5113
+ }
5114
+ }
5115
+ }
5116
+ }
5117
+ },
5118
+ "description": "**What qualifies**: a datapoint of the **published** configuration whose ROS type is `sensor_msgs/msg/JointState` and which carries no `field` — the whole message, because positions and names arrive together and a single extracted field is half of a pose. Anything else is a `validation_error` naming that rule rather than a stored mapping that renders a battery reading as a robot. `{ \"slug\": null }` clears it, which is why the field is required and nullable rather than optional. \n\n**The mapping cannot outlive what it points at.** Every successful publish re-checks it against the new document and clears it when it no longer qualifies, recording `robot.joint_state_cleared` with the version that did it; a slug rename rewrites it like every other reference the editor already rewrites; deleting the robot takes it along. Every write through this route — a slug or `null` — is on the record too, as `robot.joint_state_set` with the actor and the slug, so a clear a person made is never mistaken for one a publish made. The stored value reads back on `GET /api/robots/:id/assets` as `joint_state_slug`, so a renderer fetches the URDF, the meshes and the mapping from one place.",
5119
+ "requestBody": {
5120
+ "required": true,
5121
+ "content": {
5122
+ "application/json": {
5123
+ "schema": {
5124
+ "$ref": "#/components/schemas/joint-state-put-request"
5125
+ }
5126
+ }
5127
+ }
5128
+ }
5129
+ }
5130
+ },
5025
5131
  "/api/robots/{id}": {
5026
5132
  "get": {
5027
5133
  "operationId": "get_api_robots_id",
@@ -7282,6 +7388,58 @@
7282
7388
  }
7283
7389
  },
7284
7390
  "description": "Needs the `assets` capability, refused as `403 capability_required` rather than a bare `forbidden`: the code says a capability is missing and the message says which, so a developer who switched the wrong toggle on is told what to switch. The capability is checked before existence, so a denied robot and an absent one read alike to a caller with no right to tell them apart. `urdf` reports whether a URDF is present and which of its mesh references have no stored asset."
7391
+ },
7392
+ "delete": {
7393
+ "operationId": "delete_api_robots_id_assets",
7394
+ "summary": "Empties a robot's asset store: every URDF, mesh and texture, gone at once.",
7395
+ "tags": [
7396
+ "assets"
7397
+ ],
7398
+ "security": [
7399
+ {
7400
+ "developerSession": []
7401
+ },
7402
+ {
7403
+ "clientToken": []
7404
+ },
7405
+ {
7406
+ "serverKey": []
7407
+ }
7408
+ ],
7409
+ "parameters": [
7410
+ {
7411
+ "name": "id",
7412
+ "in": "path",
7413
+ "required": true,
7414
+ "description": "The robot's uuid, as returned by `POST /api/robots` or listed by `GET /api/robots`.",
7415
+ "schema": {
7416
+ "type": "string"
7417
+ }
7418
+ }
7419
+ ],
7420
+ "responses": {
7421
+ "200": {
7422
+ "description": "Success.",
7423
+ "content": {
7424
+ "application/json": {
7425
+ "schema": {
7426
+ "$ref": "#/components/schemas/assets-clear-response"
7427
+ }
7428
+ }
7429
+ }
7430
+ },
7431
+ "default": {
7432
+ "description": "An error envelope. Codes this route is known to answer: `unauthorized`, `token_expired`, `token_revoked`, `forbidden`, `tier_required`, `invalid_uuid`, `not_found`, `busy`.",
7433
+ "content": {
7434
+ "application/json": {
7435
+ "schema": {
7436
+ "$ref": "#/components/schemas/api-error"
7437
+ }
7438
+ }
7439
+ }
7440
+ }
7441
+ },
7442
+ "description": "The store's escape hatch: a full store is never a dead end, and this is the blunt third of the three answers to it — the URDF upload is exempt from the gate, reconcile after a sync already frees what the new URDF stopped referencing, and this route lets an Owner clear the robot outright. Owner tier, unconditionally, like starting a sync. Removes every asset of the robot and resets its store to `0`; the next sync fills it again. It does not touch the bridge's availability report — `urdf_available` still answers from the connected robot, unrelated to what this cloud happens to have stored. A clear while a sync is running is `409 busy` naming that sync's details, the same refusal starting a second sync gets, because deleting under a running upload would leave the store counter wrong."
7285
7443
  }
7286
7444
  },
7287
7445
  "/api/robots/{id}/assets/{assetId}": {
@@ -9244,10 +9402,9 @@
9244
9402
  "enum": [
9245
9403
  "urdf",
9246
9404
  "mesh",
9247
- "texture",
9248
- "other"
9405
+ "texture"
9249
9406
  ],
9250
- "description": "What the file is: the `urdf` itself, a `mesh` it references, a `texture` a mesh or the URDF paints with, or `other`. A renderer decides from this alone, before fetching anything, what to pre-fetch."
9407
+ "description": "What the file is: the `urdf` itself, a `mesh` it references, or a `texture` a mesh or the URDF paints with. A renderer decides from this alone, before fetching anything, what to pre-fetch."
9251
9408
  },
9252
9409
  "name": {
9253
9410
  "type": "string",
@@ -9348,32 +9505,38 @@
9348
9505
  "enum": [
9349
9506
  "unresolvable",
9350
9507
  "upload_failed",
9351
- "refused",
9352
- "too_large"
9508
+ "refused"
9353
9509
  ],
9354
- "description": "Why it failed. `unresolvable` means the reference names nothing the producer can find or may read, and is **permanent** — the only kind reconciliation may treat as gone. `upload_failed` means the bytes exist and the transfer did not succeed, `refused` means it was never attempted because a producer-side ceiling was hit, and `too_large` means it exceeds the upload limit and carries both numbers in `details`."
9510
+ "description": "Why it failed. `unresolvable` means the reference names nothing the producer can find or may read, and is **permanent** — the only kind reconciliation may treat as gone. `upload_failed` means the bytes exist and the transfer did not succeed, and `refused` means it was never attempted, either because the robot's asset store had no room — then `details` carries the three numbers — or because a producer-side ceiling was hit."
9355
9511
  },
9356
9512
  "details": {
9357
- "description": "The two numbers behind a `too_large` failure, and absent for every other kind — a forced `null` on every `unresolvable` entry buys nothing. The pairing is enforced, not merely described.",
9513
+ "description": "The three numbers behind a `refused` entry the robot's store had no room for, and absent for every other kind — a forced `null` on every `unresolvable` entry buys nothing. A `refused` entry may also carry no details: the producer's own ceiling is the other half of that kind, and no store number describes it.",
9358
9514
  "anyOf": [
9359
9515
  {
9360
9516
  "type": "object",
9361
9517
  "properties": {
9362
- "limit_bytes": {
9518
+ "store_bytes": {
9363
9519
  "type": "integer",
9364
9520
  "exclusiveMinimum": 0,
9365
9521
  "maximum": 9007199254740991,
9366
- "description": "The upload ceiling, in bytes."
9522
+ "description": "The robot's store, in bytes."
9523
+ },
9524
+ "used_bytes": {
9525
+ "type": "integer",
9526
+ "minimum": 0,
9527
+ "maximum": 9007199254740991,
9528
+ "description": "Bytes the robot's assets occupy before this upload."
9367
9529
  },
9368
9530
  "size_bytes": {
9369
9531
  "type": "integer",
9370
9532
  "exclusiveMinimum": 0,
9371
9533
  "maximum": 9007199254740991,
9372
- "description": "How large the refused file is, in bytes. With `limit_bytes` beside it a developer can tell whether to shrink the mesh or raise the limit; \"too large\" alone answers neither."
9534
+ "description": "The refused upload, in bytes."
9373
9535
  }
9374
9536
  },
9375
9537
  "required": [
9376
- "limit_bytes",
9538
+ "store_bytes",
9539
+ "used_bytes",
9377
9540
  "size_bytes"
9378
9541
  ],
9379
9542
  "additionalProperties": false
@@ -9404,6 +9567,25 @@
9404
9567
  ],
9405
9568
  "description": "Why the sync ended as it did, when that is not a per-reference fact. `null` when `failed` already says everything there is to say."
9406
9569
  },
9570
+ "stored": {
9571
+ "anyOf": [
9572
+ {
9573
+ "type": "integer",
9574
+ "minimum": 0,
9575
+ "maximum": 9007199254740991
9576
+ },
9577
+ {
9578
+ "type": "null"
9579
+ }
9580
+ ],
9581
+ "description": "How many of the announced files the cloud's store actually holds. Counted once, after the robot reports the sync done, and `null` until then — nobody has looked yet. Read it against `announced`: `state` is what the robot reported, this is what arrived."
9582
+ },
9583
+ "announced": {
9584
+ "type": "integer",
9585
+ "minimum": 0,
9586
+ "maximum": 9007199254740991,
9587
+ "description": "How many files the robot announced for this sync — the URDF, if it has one, plus every mesh URI its description references. `0` when the robot announced nothing, and also `0` until it has answered at all: read it beside `stored`, which stays `null` until the terminal frame."
9588
+ },
9407
9589
  "started_at": {
9408
9590
  "type": "string",
9409
9591
  "format": "date-time",
@@ -9425,6 +9607,8 @@
9425
9607
  "total",
9426
9608
  "failed",
9427
9609
  "reason",
9610
+ "stored",
9611
+ "announced",
9428
9612
  "started_at",
9429
9613
  "updated_at"
9430
9614
  ],
@@ -9496,13 +9680,52 @@
9496
9680
  }
9497
9681
  ],
9498
9682
  "description": "What the connected bridge says it *could* transfer — deliberately separate from what has been transferred. `null` when no bridge is connected, distinct from `false`: \"no robot is online to ask\" and \"the robot has no URDF\" send a developer to different places. After a publisher is killed rather than shut down this can read `true` for some seconds, on the underlying DDS liveliness timeout rather than on any check made here."
9683
+ },
9684
+ "store": {
9685
+ "type": "object",
9686
+ "properties": {
9687
+ "bytes": {
9688
+ "type": "integer",
9689
+ "exclusiveMinimum": 0,
9690
+ "maximum": 9007199254740991,
9691
+ "description": "The robot's asset store, `ROBOT_ASSET_STORE_BYTES`."
9692
+ },
9693
+ "used_bytes": {
9694
+ "type": "integer",
9695
+ "minimum": 0,
9696
+ "maximum": 9007199254740991,
9697
+ "description": "Bytes its assets occupy."
9698
+ }
9699
+ },
9700
+ "required": [
9701
+ "bytes",
9702
+ "used_bytes"
9703
+ ],
9704
+ "additionalProperties": false,
9705
+ "description": "How full this robot's store is."
9706
+ },
9707
+ "joint_state_slug": {
9708
+ "anyOf": [
9709
+ {
9710
+ "type": "string",
9711
+ "minLength": 2,
9712
+ "maxLength": 63,
9713
+ "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$"
9714
+ },
9715
+ {
9716
+ "type": "null"
9717
+ }
9718
+ ],
9719
+ "description": "The whole-message `sensor_msgs/msg/JointState` datapoint that drives the console's URDF viewer; null when none is chosen or a publish removed it. Set through `PUT /api/robots/:id/urdf/joint-state`."
9499
9720
  }
9500
9721
  },
9501
9722
  "required": [
9502
9723
  "assets",
9503
9724
  "active_sync",
9504
9725
  "urdf",
9505
- "urdf_available"
9726
+ "urdf_available",
9727
+ "store",
9728
+ "joint_state_slug"
9506
9729
  ],
9507
9730
  "additionalProperties": false
9508
9731
  },
@@ -9590,32 +9813,38 @@
9590
9813
  "enum": [
9591
9814
  "unresolvable",
9592
9815
  "upload_failed",
9593
- "refused",
9594
- "too_large"
9816
+ "refused"
9595
9817
  ],
9596
- "description": "Why it failed. `unresolvable` means the reference names nothing the producer can find or may read, and is **permanent** — the only kind reconciliation may treat as gone. `upload_failed` means the bytes exist and the transfer did not succeed, `refused` means it was never attempted because a producer-side ceiling was hit, and `too_large` means it exceeds the upload limit and carries both numbers in `details`."
9818
+ "description": "Why it failed. `unresolvable` means the reference names nothing the producer can find or may read, and is **permanent** — the only kind reconciliation may treat as gone. `upload_failed` means the bytes exist and the transfer did not succeed, and `refused` means it was never attempted, either because the robot's asset store had no room — then `details` carries the three numbers — or because a producer-side ceiling was hit."
9597
9819
  },
9598
9820
  "details": {
9599
- "description": "The two numbers behind a `too_large` failure, and absent for every other kind — a forced `null` on every `unresolvable` entry buys nothing. The pairing is enforced, not merely described.",
9821
+ "description": "The three numbers behind a `refused` entry the robot's store had no room for, and absent for every other kind — a forced `null` on every `unresolvable` entry buys nothing. A `refused` entry may also carry no details: the producer's own ceiling is the other half of that kind, and no store number describes it.",
9600
9822
  "anyOf": [
9601
9823
  {
9602
9824
  "type": "object",
9603
9825
  "properties": {
9604
- "limit_bytes": {
9826
+ "store_bytes": {
9605
9827
  "type": "integer",
9606
9828
  "exclusiveMinimum": 0,
9607
9829
  "maximum": 9007199254740991,
9608
- "description": "The upload ceiling, in bytes."
9830
+ "description": "The robot's store, in bytes."
9831
+ },
9832
+ "used_bytes": {
9833
+ "type": "integer",
9834
+ "minimum": 0,
9835
+ "maximum": 9007199254740991,
9836
+ "description": "Bytes the robot's assets occupy before this upload."
9609
9837
  },
9610
9838
  "size_bytes": {
9611
9839
  "type": "integer",
9612
9840
  "exclusiveMinimum": 0,
9613
9841
  "maximum": 9007199254740991,
9614
- "description": "How large the refused file is, in bytes. With `limit_bytes` beside it a developer can tell whether to shrink the mesh or raise the limit; \"too large\" alone answers neither."
9842
+ "description": "The refused upload, in bytes."
9615
9843
  }
9616
9844
  },
9617
9845
  "required": [
9618
- "limit_bytes",
9846
+ "store_bytes",
9847
+ "used_bytes",
9619
9848
  "size_bytes"
9620
9849
  ],
9621
9850
  "additionalProperties": false
@@ -9646,6 +9875,25 @@
9646
9875
  ],
9647
9876
  "description": "Why the sync ended as it did, when that is not a per-reference fact. `null` when `failed` already says everything there is to say."
9648
9877
  },
9878
+ "stored": {
9879
+ "anyOf": [
9880
+ {
9881
+ "type": "integer",
9882
+ "minimum": 0,
9883
+ "maximum": 9007199254740991
9884
+ },
9885
+ {
9886
+ "type": "null"
9887
+ }
9888
+ ],
9889
+ "description": "How many of the announced files the cloud's store actually holds. Counted once, after the robot reports the sync done, and `null` until then — nobody has looked yet. Read it against `announced`: `state` is what the robot reported, this is what arrived."
9890
+ },
9891
+ "announced": {
9892
+ "type": "integer",
9893
+ "minimum": 0,
9894
+ "maximum": 9007199254740991,
9895
+ "description": "How many files the robot announced for this sync — the URDF, if it has one, plus every mesh URI its description references. `0` when the robot announced nothing, and also `0` until it has answered at all: read it beside `stored`, which stays `null` until the terminal frame."
9896
+ },
9649
9897
  "started_at": {
9650
9898
  "type": "string",
9651
9899
  "format": "date-time",
@@ -9667,11 +9915,35 @@
9667
9915
  "total",
9668
9916
  "failed",
9669
9917
  "reason",
9918
+ "stored",
9919
+ "announced",
9670
9920
  "started_at",
9671
9921
  "updated_at"
9672
9922
  ],
9673
9923
  "additionalProperties": false
9674
9924
  },
9925
+ "assets-clear-response": {
9926
+ "type": "object",
9927
+ "properties": {
9928
+ "deleted": {
9929
+ "type": "integer",
9930
+ "minimum": 0,
9931
+ "maximum": 9007199254740991,
9932
+ "description": "How many assets — URDF, meshes and textures together — were removed."
9933
+ },
9934
+ "bytes_freed": {
9935
+ "type": "integer",
9936
+ "minimum": 0,
9937
+ "maximum": 9007199254740991,
9938
+ "description": "The bytes the robot's store got back."
9939
+ }
9940
+ },
9941
+ "required": [
9942
+ "deleted",
9943
+ "bytes_freed"
9944
+ ],
9945
+ "additionalProperties": false
9946
+ },
9675
9947
  "audit-list-response": {
9676
9948
  "type": "object",
9677
9949
  "properties": {
@@ -10535,11 +10807,16 @@
10535
10807
  "type": "null"
10536
10808
  }
10537
10809
  ]
10810
+ },
10811
+ "low_bandwidth": {
10812
+ "type": "boolean",
10813
+ "description": "Whether the bridge is in its low-bandwidth mode: datapoints capped, cameras reduced or stopped. Bridge-reported."
10538
10814
  }
10539
10815
  },
10540
10816
  "required": [
10541
10817
  "online",
10542
- "latency_ms"
10818
+ "latency_ms",
10819
+ "low_bandwidth"
10543
10820
  ],
10544
10821
  "additionalProperties": false,
10545
10822
  "description": "The built-in `bridge_state` datapoint as the cloud observes it right now: whether the bridge is connected, and its latency when it is."
@@ -10665,6 +10942,11 @@
10665
10942
  0.5
10666
10943
  ]
10667
10944
  },
10945
+ "low_bandwidth": {
10946
+ "description": "`keep` exempts this datapoint from the low-bandwidth rate cap; its backfill still pauses.",
10947
+ "type": "string",
10948
+ "const": "keep"
10949
+ },
10668
10950
  "description": {
10669
10951
  "description": "Prose about what this value is, for whoever meets it in the console. It changes nothing the robot does, so a publish that touches only it pushes no configuration at all — but it is carried verbatim into `robot_describe`, where a model that has never seen this robot reads it. The datapoint is offered whenever the role grants it; without one it is offered with `description: null` and the model has less to go on, as for actions, services, publishers and cameras. Omission is the only way to say nothing; an empty string is refused, here and on all five.",
10670
10952
  "examples": [
@@ -10862,7 +11144,7 @@
10862
11144
  ],
10863
11145
  "additionalProperties": false
10864
11146
  },
10865
- "description": "Values the robot publishes, each one field of one topic or a whole topic, and **never several topics**. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state`, `robot_details` and `bridge_pressure` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all four are refused when the document is validated."
11147
+ "description": "Values the robot publishes, each one field of one topic or a whole topic, and **never several topics**. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state` and `robot_details` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all three are refused when the document is validated."
10866
11148
  },
10867
11149
  "actions": {
10868
11150
  "type": "object",
@@ -11012,7 +11294,7 @@
11012
11294
  ],
11013
11295
  "additionalProperties": false
11014
11296
  },
11015
- "description": "Things the robot does on request that take time, each reported as a job with progress. **At most one job runs per action slug**: a second call is refused `busy`, and every observer of that slug watches the same job. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state`, `robot_details` and `bridge_pressure` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all four are refused when the document is validated."
11297
+ "description": "Things the robot does on request that take time, each reported as a job with progress. **At most one job runs per action slug**: a second call is refused `busy`, and every observer of that slug watches the same job. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state` and `robot_details` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all three are refused when the document is validated."
11016
11298
  },
11017
11299
  "services": {
11018
11300
  "type": "object",
@@ -11162,7 +11444,7 @@
11162
11444
  ],
11163
11445
  "additionalProperties": false
11164
11446
  },
11165
- "description": "ROS service calls the robot answers — one request, one reply. Unlike an action a service reports **no progress** and the call returns with its result already on the job, so there is nothing left to observe; a second concurrent call is still refused `busy`, exactly as for an action. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state`, `robot_details` and `bridge_pressure` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all four are refused when the document is validated."
11447
+ "description": "ROS service calls the robot answers — one request, one reply. Unlike an action a service reports **no progress** and the call returns with its result already on the job, so there is nothing left to observe; a second concurrent call is still refused `busy`, exactly as for an action. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state` and `robot_details` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all three are refused when the document is validated."
11166
11448
  },
11167
11449
  "publishers": {
11168
11450
  "type": "object",
@@ -11348,7 +11630,7 @@
11348
11630
  ],
11349
11631
  "additionalProperties": false
11350
11632
  },
11351
- "description": "Topics clients may send to, and where the format's whole safety story lives. The `message` template fixes every value a caller cannot change, and **`failsafe` is required**: once a client falls silent the bridge sends the failsafe message itself, so an operator whose window closed does not leave a robot driving. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state`, `robot_details` and `bridge_pressure` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all four are refused when the document is validated."
11633
+ "description": "Topics clients may send to, and where the format's whole safety story lives. The `message` template fixes every value a caller cannot change, and **`failsafe` is required**: once a client falls silent the bridge sends the failsafe message itself, so an operator whose window closed does not leave a robot driving. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state` and `robot_details` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all three are refused when the document is validated."
11352
11634
  },
11353
11635
  "cameras": {
11354
11636
  "type": "object",
@@ -11596,7 +11878,67 @@
11596
11878
  ],
11597
11879
  "additionalProperties": false
11598
11880
  },
11599
- "description": "Video the robot streams, and the still frames the cloud serves from it. `width`, `height`, `fps` and `bitrate_kbps` are what **the bridge produces before sending**, not what the camera captures — they live in the configuration rather than in a viewer's request precisely so that no viewer can make a robot send more. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state`, `robot_details` and `bridge_pressure` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all four are refused when the document is validated."
11881
+ "description": "Video the robot streams, and the still frames the cloud serves from it. `width`, `height`, `fps` and `bitrate_kbps` are what **the bridge produces before sending**, not what the camera captures — they live in the configuration rather than in a viewer's request precisely so that no viewer can make a robot send more. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state` and `robot_details` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all three are refused when the document is validated."
11882
+ },
11883
+ "low_bandwidth": {
11884
+ "description": "Overrides for the bridge's low-bandwidth mode; see the section schema.",
11885
+ "type": "object",
11886
+ "properties": {
11887
+ "mode": {
11888
+ "description": "`auto` decides from the measured lag; `on` and `off` force the mode, for tests and for an operator who knows the link.",
11889
+ "type": "string",
11890
+ "enum": [
11891
+ "auto",
11892
+ "on",
11893
+ "off"
11894
+ ]
11895
+ },
11896
+ "enter_lag_ms": {
11897
+ "description": "Lag or queue dwell above this enters the mode. Checked against exit_lag_ms only when both are in this document; a lone key composes with the bridge's parameter or the default on the robot, and a crossed pair is refused there when the configuration is applied, so name both when you change either.",
11898
+ "type": "integer",
11899
+ "minimum": 100,
11900
+ "maximum": 9007199254740991
11901
+ },
11902
+ "enter_after_s": {
11903
+ "description": "The entry condition must hold this long.",
11904
+ "type": "integer",
11905
+ "minimum": 1,
11906
+ "maximum": 9007199254740991
11907
+ },
11908
+ "exit_lag_ms": {
11909
+ "description": "Lag and dwell both at or below this leave the mode. Must be at or below enter_lag_ms: a crossed pair is a mode that leaves as it arrives. Checked here only when both keys are present; a lone key is checked on the robot against the parameter or default it composes with.",
11910
+ "type": "integer",
11911
+ "minimum": 0,
11912
+ "maximum": 9007199254740991
11913
+ },
11914
+ "exit_after_s": {
11915
+ "description": "The exit condition must hold this long.",
11916
+ "type": "integer",
11917
+ "minimum": 1,
11918
+ "maximum": 9007199254740991
11919
+ },
11920
+ "datapoint_max_hz": {
11921
+ "description": "The long-run rate for every datapoint in the mode, unless the datapoint says `low_bandwidth: keep`. It is an average, not a minimum gap: after a quiet spell two samples may go out close together, and over any longer window the rate holds.",
11922
+ "type": "number",
11923
+ "exclusiveMinimum": 0,
11924
+ "maximum": 20
11925
+ },
11926
+ "camera": {
11927
+ "description": "What happens to a running stream in the mode. New streams are refused either way.",
11928
+ "type": "string",
11929
+ "enum": [
11930
+ "reduce",
11931
+ "stop"
11932
+ ]
11933
+ },
11934
+ "camera_bitrate_kbps": {
11935
+ "description": "Bitrate applied to running streams under `reduce`.",
11936
+ "type": "integer",
11937
+ "minimum": 50,
11938
+ "maximum": 20000
11939
+ }
11940
+ },
11941
+ "additionalProperties": false
11600
11942
  }
11601
11943
  },
11602
11944
  "required": [
@@ -11762,6 +12104,11 @@
11762
12104
  0.5
11763
12105
  ]
11764
12106
  },
12107
+ "low_bandwidth": {
12108
+ "description": "`keep` exempts this datapoint from the low-bandwidth rate cap; its backfill still pauses.",
12109
+ "type": "string",
12110
+ "const": "keep"
12111
+ },
11765
12112
  "description": {
11766
12113
  "description": "Prose about what this value is, for whoever meets it in the console. It changes nothing the robot does, so a publish that touches only it pushes no configuration at all — but it is carried verbatim into `robot_describe`, where a model that has never seen this robot reads it. The datapoint is offered whenever the role grants it; without one it is offered with `description: null` and the model has less to go on, as for actions, services, publishers and cameras. Omission is the only way to say nothing; an empty string is refused, here and on all five.",
11767
12114
  "examples": [
@@ -11959,7 +12306,7 @@
11959
12306
  ],
11960
12307
  "additionalProperties": false
11961
12308
  },
11962
- "description": "Values the robot publishes, each one field of one topic or a whole topic, and **never several topics**. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state`, `robot_details` and `bridge_pressure` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all four are refused when the document is validated."
12309
+ "description": "Values the robot publishes, each one field of one topic or a whole topic, and **never several topics**. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state` and `robot_details` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all three are refused when the document is validated."
11963
12310
  },
11964
12311
  "actions": {
11965
12312
  "type": "object",
@@ -12109,7 +12456,7 @@
12109
12456
  ],
12110
12457
  "additionalProperties": false
12111
12458
  },
12112
- "description": "Things the robot does on request that take time, each reported as a job with progress. **At most one job runs per action slug**: a second call is refused `busy`, and every observer of that slug watches the same job. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state`, `robot_details` and `bridge_pressure` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all four are refused when the document is validated."
12459
+ "description": "Things the robot does on request that take time, each reported as a job with progress. **At most one job runs per action slug**: a second call is refused `busy`, and every observer of that slug watches the same job. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state` and `robot_details` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all three are refused when the document is validated."
12113
12460
  },
12114
12461
  "services": {
12115
12462
  "type": "object",
@@ -12259,7 +12606,7 @@
12259
12606
  ],
12260
12607
  "additionalProperties": false
12261
12608
  },
12262
- "description": "ROS service calls the robot answers — one request, one reply. Unlike an action a service reports **no progress** and the call returns with its result already on the job, so there is nothing left to observe; a second concurrent call is still refused `busy`, exactly as for an action. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state`, `robot_details` and `bridge_pressure` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all four are refused when the document is validated."
12609
+ "description": "ROS service calls the robot answers — one request, one reply. Unlike an action a service reports **no progress** and the call returns with its result already on the job, so there is nothing left to observe; a second concurrent call is still refused `busy`, exactly as for an action. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state` and `robot_details` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all three are refused when the document is validated."
12263
12610
  },
12264
12611
  "publishers": {
12265
12612
  "type": "object",
@@ -12445,7 +12792,7 @@
12445
12792
  ],
12446
12793
  "additionalProperties": false
12447
12794
  },
12448
- "description": "Topics clients may send to, and where the format's whole safety story lives. The `message` template fixes every value a caller cannot change, and **`failsafe` is required**: once a client falls silent the bridge sends the failsafe message itself, so an operator whose window closed does not leave a robot driving. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state`, `robot_details` and `bridge_pressure` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all four are refused when the document is validated."
12795
+ "description": "Topics clients may send to, and where the format's whole safety story lives. The `message` template fixes every value a caller cannot change, and **`failsafe` is required**: once a client falls silent the bridge sends the failsafe message itself, so an operator whose window closed does not leave a robot driving. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state` and `robot_details` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all three are refused when the document is validated."
12449
12796
  },
12450
12797
  "cameras": {
12451
12798
  "type": "object",
@@ -12693,7 +13040,67 @@
12693
13040
  ],
12694
13041
  "additionalProperties": false
12695
13042
  },
12696
- "description": "Video the robot streams, and the still frames the cloud serves from it. `width`, `height`, `fps` and `bitrate_kbps` are what **the bridge produces before sending**, not what the camera captures — they live in the configuration rather than in a viewer's request precisely so that no viewer can make a robot send more. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state`, `robot_details` and `bridge_pressure` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all four are refused when the document is validated."
13043
+ "description": "Video the robot streams, and the still frames the cloud serves from it. `width`, `height`, `fps` and `bitrate_kbps` are what **the bridge produces before sending**, not what the camera captures — they live in the configuration rather than in a viewer's request precisely so that no viewer can make a robot send more. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state` and `robot_details` are built-in, and `history` is reserved because `GET …/jobs/history` would shadow an action of that name; all three are refused when the document is validated."
13044
+ },
13045
+ "low_bandwidth": {
13046
+ "description": "Overrides for the bridge's low-bandwidth mode; see the section schema.",
13047
+ "type": "object",
13048
+ "properties": {
13049
+ "mode": {
13050
+ "description": "`auto` decides from the measured lag; `on` and `off` force the mode, for tests and for an operator who knows the link.",
13051
+ "type": "string",
13052
+ "enum": [
13053
+ "auto",
13054
+ "on",
13055
+ "off"
13056
+ ]
13057
+ },
13058
+ "enter_lag_ms": {
13059
+ "description": "Lag or queue dwell above this enters the mode. Checked against exit_lag_ms only when both are in this document; a lone key composes with the bridge's parameter or the default on the robot, and a crossed pair is refused there when the configuration is applied, so name both when you change either.",
13060
+ "type": "integer",
13061
+ "minimum": 100,
13062
+ "maximum": 9007199254740991
13063
+ },
13064
+ "enter_after_s": {
13065
+ "description": "The entry condition must hold this long.",
13066
+ "type": "integer",
13067
+ "minimum": 1,
13068
+ "maximum": 9007199254740991
13069
+ },
13070
+ "exit_lag_ms": {
13071
+ "description": "Lag and dwell both at or below this leave the mode. Must be at or below enter_lag_ms: a crossed pair is a mode that leaves as it arrives. Checked here only when both keys are present; a lone key is checked on the robot against the parameter or default it composes with.",
13072
+ "type": "integer",
13073
+ "minimum": 0,
13074
+ "maximum": 9007199254740991
13075
+ },
13076
+ "exit_after_s": {
13077
+ "description": "The exit condition must hold this long.",
13078
+ "type": "integer",
13079
+ "minimum": 1,
13080
+ "maximum": 9007199254740991
13081
+ },
13082
+ "datapoint_max_hz": {
13083
+ "description": "The long-run rate for every datapoint in the mode, unless the datapoint says `low_bandwidth: keep`. It is an average, not a minimum gap: after a quiet spell two samples may go out close together, and over any longer window the rate holds.",
13084
+ "type": "number",
13085
+ "exclusiveMinimum": 0,
13086
+ "maximum": 20
13087
+ },
13088
+ "camera": {
13089
+ "description": "What happens to a running stream in the mode. New streams are refused either way.",
13090
+ "type": "string",
13091
+ "enum": [
13092
+ "reduce",
13093
+ "stop"
13094
+ ]
13095
+ },
13096
+ "camera_bitrate_kbps": {
13097
+ "description": "Bitrate applied to running streams under `reduce`.",
13098
+ "type": "integer",
13099
+ "minimum": 50,
13100
+ "maximum": 20000
13101
+ }
13102
+ },
13103
+ "additionalProperties": false
12697
13104
  }
12698
13105
  },
12699
13106
  "required": [
@@ -13102,7 +13509,7 @@
13102
13509
  },
13103
13510
  "builtin": {
13104
13511
  "type": "boolean",
13105
- "description": "`true` for the datapoints every robot has — `bridge_state`, `robot_details` and `bridge_pressure` — and `false` for everything the published configuration adds."
13512
+ "description": "`true` for the datapoints every robot has — `bridge_state` and `robot_details` — and `false` for everything the published configuration adds."
13106
13513
  },
13107
13514
  "unit": {
13108
13515
  "anyOf": [
@@ -13137,7 +13544,7 @@
13137
13544
  ],
13138
13545
  "additionalProperties": false
13139
13546
  },
13140
- "description": "Everything a client may read on this robot: the three built-ins, plus every datapoint the published configuration exposes and the caller's role grants."
13547
+ "description": "Everything a client may read on this robot: the two built-ins, plus every datapoint the published configuration exposes and the caller's role grants."
13141
13548
  }
13142
13549
  },
13143
13550
  "required": [
@@ -14452,6 +14859,51 @@
14452
14859
  ],
14453
14860
  "additionalProperties": false
14454
14861
  },
14862
+ "joint-state-put-request": {
14863
+ "type": "object",
14864
+ "properties": {
14865
+ "slug": {
14866
+ "anyOf": [
14867
+ {
14868
+ "type": "string",
14869
+ "minLength": 2,
14870
+ "maxLength": 63,
14871
+ "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$"
14872
+ },
14873
+ {
14874
+ "type": "null"
14875
+ }
14876
+ ],
14877
+ "description": "The datapoint to read joint positions from, or `null` to choose none. It must name a whole-message `sensor_msgs/msg/JointState` datapoint of the published configuration; anything else is a `validation_error` naming the rule."
14878
+ }
14879
+ },
14880
+ "required": [
14881
+ "slug"
14882
+ ]
14883
+ },
14884
+ "joint-state-put-response": {
14885
+ "type": "object",
14886
+ "properties": {
14887
+ "joint_state_slug": {
14888
+ "anyOf": [
14889
+ {
14890
+ "type": "string",
14891
+ "minLength": 2,
14892
+ "maxLength": 63,
14893
+ "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$"
14894
+ },
14895
+ {
14896
+ "type": "null"
14897
+ }
14898
+ ],
14899
+ "description": "The stored mapping after the call, `null` when none is chosen. The same value `assetListResponse.joint_state_slug` carries."
14900
+ }
14901
+ },
14902
+ "required": [
14903
+ "joint_state_slug"
14904
+ ],
14905
+ "additionalProperties": false
14906
+ },
14455
14907
  "live-session-response": {
14456
14908
  "type": "object",
14457
14909
  "properties": {
@@ -15329,11 +15781,6 @@
15329
15781
  "type": "integer",
15330
15782
  "exclusiveMinimum": 0,
15331
15783
  "maximum": 9007199254740991
15332
- },
15333
- "max_asset_storage_bytes": {
15334
- "type": "integer",
15335
- "minimum": 0,
15336
- "maximum": 9007199254740991
15337
15784
  }
15338
15785
  },
15339
15786
  "required": [
@@ -15342,8 +15789,7 @@
15342
15789
  "max_end_users",
15343
15790
  "max_retention_bytes",
15344
15791
  "max_retention_writes_per_minute",
15345
- "max_realtime_connections",
15346
- "max_asset_storage_bytes"
15792
+ "max_realtime_connections"
15347
15793
  ],
15348
15794
  "additionalProperties": false
15349
15795
  },
@@ -15370,11 +15816,6 @@
15370
15816
  "minimum": 0,
15371
15817
  "maximum": 9007199254740991
15372
15818
  },
15373
- "max_asset_storage_bytes": {
15374
- "type": "integer",
15375
- "minimum": 0,
15376
- "maximum": 9007199254740991
15377
- },
15378
15819
  "max_retention_writes_per_minute": {
15379
15820
  "type": "integer",
15380
15821
  "minimum": 0,
@@ -16268,7 +16709,8 @@
16268
16709
  "asset_bytes_freed": {
16269
16710
  "type": "integer",
16270
16711
  "minimum": 0,
16271
- "maximum": 9007199254740991
16712
+ "maximum": 9007199254740991,
16713
+ "description": "What the robot's store gives back: every distinct mesh or texture blob it holds, counted once, URDF excluded; a blob another robot also references stays in the object store but is still credited here, because each robot's counter carries it."
16272
16714
  },
16273
16715
  "job_run_count": {
16274
16716
  "type": "integer",
@@ -16332,11 +16774,16 @@
16332
16774
  "type": "null"
16333
16775
  }
16334
16776
  ]
16777
+ },
16778
+ "low_bandwidth": {
16779
+ "type": "boolean",
16780
+ "description": "Whether the bridge is in its low-bandwidth mode: datapoints capped, cameras reduced or stopped. Bridge-reported."
16335
16781
  }
16336
16782
  },
16337
16783
  "required": [
16338
16784
  "online",
16339
- "latency_ms"
16785
+ "latency_ms",
16786
+ "low_bandwidth"
16340
16787
  ],
16341
16788
  "additionalProperties": false
16342
16789
  },
@@ -16378,6 +16825,15 @@
16378
16825
  ],
16379
16826
  "additionalProperties": false
16380
16827
  },
16828
+ "protocol_status": {
16829
+ "description": "Where this robot's bridge stands against the protocol window: `current`, `deprecated` (still served, sunset date on the detail), or `refused` (its last hello was refused for its version; offline until upgraded). Absent from a cloud older than 0.21.0; read absence as `current`.",
16830
+ "type": "string",
16831
+ "enum": [
16832
+ "current",
16833
+ "deprecated",
16834
+ "refused"
16835
+ ]
16836
+ },
16381
16837
  "bridge_version": {
16382
16838
  "anyOf": [
16383
16839
  {
@@ -16389,6 +16845,52 @@
16389
16845
  }
16390
16846
  ]
16391
16847
  },
16848
+ "protocol_version": {
16849
+ "description": "The protocol version the bridge announced in its last accepted hello; `null` before the first. Absent from a cloud older than 0.21.0.",
16850
+ "anyOf": [
16851
+ {
16852
+ "type": "integer",
16853
+ "exclusiveMinimum": 0,
16854
+ "maximum": 9007199254740991
16855
+ },
16856
+ {
16857
+ "type": "null"
16858
+ }
16859
+ ]
16860
+ },
16861
+ "protocol": {
16862
+ "description": "The window verdict for `protocol_version`.",
16863
+ "type": "object",
16864
+ "properties": {
16865
+ "status": {
16866
+ "type": "string",
16867
+ "enum": [
16868
+ "current",
16869
+ "deprecated",
16870
+ "refused"
16871
+ ],
16872
+ "description": "Same values as `protocol_status`."
16873
+ },
16874
+ "sunset_at": {
16875
+ "anyOf": [
16876
+ {
16877
+ "type": "string",
16878
+ "format": "date",
16879
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$"
16880
+ },
16881
+ {
16882
+ "type": "null"
16883
+ }
16884
+ ],
16885
+ "description": "ISO date the announced version stops being served; `null` when current or unknown."
16886
+ }
16887
+ },
16888
+ "required": [
16889
+ "status",
16890
+ "sunset_at"
16891
+ ],
16892
+ "additionalProperties": false
16893
+ },
16392
16894
  "last_hello_error": {
16393
16895
  "anyOf": [
16394
16896
  {
@@ -16498,7 +17000,8 @@
16498
17000
  "action",
16499
17001
  "service",
16500
17002
  "publisher",
16501
- "camera"
17003
+ "camera",
17004
+ "low_bandwidth"
16502
17005
  ]
16503
17006
  },
16504
17007
  "code": {
@@ -16716,11 +17219,16 @@
16716
17219
  "type": "null"
16717
17220
  }
16718
17221
  ]
17222
+ },
17223
+ "low_bandwidth": {
17224
+ "type": "boolean",
17225
+ "description": "Whether the bridge is in its low-bandwidth mode: datapoints capped, cameras reduced or stopped. Bridge-reported."
16719
17226
  }
16720
17227
  },
16721
17228
  "required": [
16722
17229
  "online",
16723
- "latency_ms"
17230
+ "latency_ms",
17231
+ "low_bandwidth"
16724
17232
  ],
16725
17233
  "additionalProperties": false
16726
17234
  },
@@ -16761,6 +17269,15 @@
16761
17269
  "cameras"
16762
17270
  ],
16763
17271
  "additionalProperties": false
17272
+ },
17273
+ "protocol_status": {
17274
+ "description": "Where this robot's bridge stands against the protocol window: `current`, `deprecated` (still served, sunset date on the detail), or `refused` (its last hello was refused for its version; offline until upgraded). Absent from a cloud older than 0.21.0; read absence as `current`.",
17275
+ "type": "string",
17276
+ "enum": [
17277
+ "current",
17278
+ "deprecated",
17279
+ "refused"
17280
+ ]
16764
17281
  }
16765
17282
  },
16766
17283
  "required": [
@@ -16779,6 +17296,20 @@
16779
17296
  ],
16780
17297
  "additionalProperties": false
16781
17298
  },
17299
+ "robot-token-rotate-response": {
17300
+ "type": "object",
17301
+ "properties": {
17302
+ "token": {
17303
+ "type": "string",
17304
+ "pattern": "^frt_[0-9a-f]{32}$",
17305
+ "description": "The robot's new bridge token. Returned exactly once; the previous token stops working at the bridge's next hello."
17306
+ }
17307
+ },
17308
+ "required": [
17309
+ "token"
17310
+ ],
17311
+ "additionalProperties": false
17312
+ },
16782
17313
  "role": {
16783
17314
  "type": "object",
16784
17315
  "properties": {