From 1059b5dbc7319666b5a890ccf79bf83fb033d4ce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 24 Jul 2025 20:23:39 +0000 Subject: [PATCH 1/8] Initial plan From 33e6462d365595d0c57204da61a6968d708cb2cf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 24 Jul 2025 20:29:05 +0000 Subject: [PATCH 2/8] Initial setup: Create basic schema templates for card.triangulate API Co-authored-by: zakoverflow <215570245+zakoverflow@users.noreply.github.com> --- card.triangulate.req.notecard.api.json | 60 ++++++++++++++++++++++++++ card.triangulate.rsp.notecard.api.json | 20 +++++++++ notecard.api.json | 5 ++- tests/test_card_triangulate_req.py | 56 ++++++++++++++++++++++++ tests/test_card_triangulate_rsp.py | 40 +++++++++++++++++ 5 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 card.triangulate.req.notecard.api.json create mode 100644 card.triangulate.rsp.notecard.api.json create mode 100644 tests/test_card_triangulate_req.py create mode 100644 tests/test_card_triangulate_rsp.py diff --git a/card.triangulate.req.notecard.api.json b/card.triangulate.req.notecard.api.json new file mode 100644 index 0000000..1dbaac1 --- /dev/null +++ b/card.triangulate.req.notecard.api.json @@ -0,0 +1,60 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/blues/notecard-schema/master/card.triangulate.req.notecard.api.json", + "title": "card.triangulate Request Application Programming Interface (API) Schema", + "description": "Request schema for card.triangulate API command.", + "type": "object", + "skus": [ + "CELL", + "CELL+WIFI", + "WIFI", + "LORA" + ], + "version": "0.1.1", + "apiVersion": "9.1.1", + "properties": { + "cmd": { + "description": "Command for the Notecard (no response)", + "const": "card.triangulate" + }, + "req": { + "description": "Request for the Notecard (expects response)", + "const": "card.triangulate" + } + }, + "oneOf": [ + { + "required": [ + "req" + ], + "properties": { + "req": { + "const": "card.triangulate" + } + } + }, + { + "required": [ + "cmd" + ], + "properties": { + "cmd": { + "const": "card.triangulate" + } + } + } + ], + "additionalProperties": false, + "samples": [ + { + "description": "Basic card.triangulate request.", + "json": "{\"req\":\"card.triangulate\"}" + } + ], + "annotations": [ + { + "title": "note", + "description": "Placeholder annotation." + } + ] +} \ No newline at end of file diff --git a/card.triangulate.rsp.notecard.api.json b/card.triangulate.rsp.notecard.api.json new file mode 100644 index 0000000..d70449b --- /dev/null +++ b/card.triangulate.rsp.notecard.api.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/blues/notecard-schema/master/card.triangulate.rsp.notecard.api.json", + "title": "card.triangulate Response Application Programming Interface (API) Schema", + "type": "object", + "version": "0.1.1", + "apiVersion": "9.1.1", + "properties": { + "status": { + "description": "Status of the operation", + "type": "string" + } + }, + "samples": [ + { + "description": "Basic card.triangulate response.", + "json": "{\"status\": \"success\"}" + } + ] +} \ No newline at end of file diff --git a/notecard.api.json b/notecard.api.json index 9b8a729..acfa151 100644 --- a/notecard.api.json +++ b/notecard.api.json @@ -93,6 +93,9 @@ { "$ref": "https://raw.githubusercontent.com/blues/notecard-schema/master/card.transport.req.notecard.api.json" }, + { + "$ref": "https://raw.githubusercontent.com/blues/notecard-schema/master/card.triangulate.req.notecard.api.json" + }, { "$ref": "https://raw.githubusercontent.com/blues/notecard-schema/master/card.usage.get.req.notecard.api.json" }, @@ -220,4 +223,4 @@ "$ref": "https://raw.githubusercontent.com/blues/notecard-schema/master/web.req.notecard.api.json" } ] -} +} \ No newline at end of file diff --git a/tests/test_card_triangulate_req.py b/tests/test_card_triangulate_req.py new file mode 100644 index 0000000..77e9885 --- /dev/null +++ b/tests/test_card_triangulate_req.py @@ -0,0 +1,56 @@ +import pytest +import jsonschema +import json + +SCHEMA_FILE = "card.triangulate.req.notecard.api.json" + +def test_valid_req(schema): + """Tests a minimal valid request using 'req'.""" + instance = {"req": "card.triangulate"} + jsonschema.validate(instance=instance, schema=schema) + +def test_valid_cmd(schema): + """Tests a minimal valid request using 'cmd'.""" + instance = {"cmd": "card.triangulate"} + jsonschema.validate(instance=instance, schema=schema) + +def test_invalid_empty_object(schema): + """Tests invalid empty object (needs req or cmd).""" + instance = {} + with pytest.raises(jsonschema.ValidationError) as excinfo: + jsonschema.validate(instance=instance, schema=schema) + assert "is not valid under any of the given schemas" in str(excinfo.value) + +def test_invalid_both_req_and_cmd(schema): + """Tests invalid request having both req and cmd.""" + instance = {"req": "card.triangulate", "cmd": "card.triangulate"} + with pytest.raises(jsonschema.ValidationError) as excinfo: + jsonschema.validate(instance=instance, schema=schema) + assert "is valid under each of" in str(excinfo.value) + +def test_invalid_additional_property_with_req(schema): + """Tests invalid request with req and an additional property.""" + instance = {"req": "card.triangulate", "extra": "field"} + with pytest.raises(jsonschema.ValidationError) as excinfo: + jsonschema.validate(instance=instance, schema=schema) + assert "Additional properties are not allowed ('extra' was unexpected)" in str(excinfo.value) + +def test_invalid_additional_property_with_cmd(schema): + """Tests invalid request with cmd and an additional property.""" + instance = {"cmd": "card.triangulate", "extra": "field"} + with pytest.raises(jsonschema.ValidationError) as excinfo: + jsonschema.validate(instance=instance, schema=schema) + assert "Additional properties are not allowed ('extra' was unexpected)" in str(excinfo.value) + +def test_validate_samples_from_schema(schema, schema_samples): + """Tests that samples in the schema definition are valid.""" + for sample in schema_samples: + sample_json_str = sample.get("json") + if not sample_json_str: + pytest.fail(f"Sample missing 'json' field: {sample.get('description', 'Unnamed sample')}") + try: + instance = json.loads(sample_json_str) + except json.JSONDecodeError as e: + pytest.fail(f"Failed to parse sample JSON: {sample_json_str}\nError: {e}") + + jsonschema.validate(instance=instance, schema=schema) diff --git a/tests/test_card_triangulate_rsp.py b/tests/test_card_triangulate_rsp.py new file mode 100644 index 0000000..036cff9 --- /dev/null +++ b/tests/test_card_triangulate_rsp.py @@ -0,0 +1,40 @@ +import pytest +import jsonschema +import json + +SCHEMA_FILE = "card.triangulate.rsp.notecard.api.json" + +def test_minimal_valid_rsp(schema): + """Tests a minimal valid response (empty object).""" + instance = {} + jsonschema.validate(instance=instance, schema=schema) + +def test_valid_status(schema): + """Tests valid status field.""" + instance = {"status": "success"} + jsonschema.validate(instance=instance, schema=schema) + +def test_status_invalid_type(schema): + """Tests invalid type for status.""" + instance = {"status": 123} + with pytest.raises(jsonschema.ValidationError) as excinfo: + jsonschema.validate(instance=instance, schema=schema) + assert "123 is not of type 'string'" in str(excinfo.value) + +def test_valid_additional_property(schema): + """Tests valid response with an additional property.""" + instance = {"status": "success", "additional": "property"} + jsonschema.validate(instance=instance, schema=schema) + +def test_validate_samples_from_schema(schema, schema_samples): + """Tests that samples in the schema definition are valid.""" + for sample in schema_samples: + sample_json_str = sample.get("json") + if not sample_json_str: + pytest.fail(f"Sample missing 'json' field: {sample.get('description', 'Unnamed sample')}") + try: + instance = json.loads(sample_json_str) + except json.JSONDecodeError as e: + pytest.fail(f"Failed to parse sample JSON: {sample_json_str}\nError: {e}") + + jsonschema.validate(instance=instance, schema=schema) From e2147363972a1cebb2dc2a6fb6428453791a6dc6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 24 Jul 2025 20:34:09 +0000 Subject: [PATCH 3/8] Implement complete card.triangulate API schema with comprehensive tests Co-authored-by: zakoverflow <215570245+zakoverflow@users.noreply.github.com> --- card.triangulate.req.notecard.api.json | 24 +++- card.triangulate.rsp.notecard.api.json | 54 ++++++++- tests/test_card_triangulate_req.py | 28 +++++ tests/test_card_triangulate_rsp.py | 150 ++++++++++++++++++++++++- 4 files changed, 244 insertions(+), 12 deletions(-) diff --git a/card.triangulate.req.notecard.api.json b/card.triangulate.req.notecard.api.json index 1dbaac1..6fff3c1 100644 --- a/card.triangulate.req.notecard.api.json +++ b/card.triangulate.req.notecard.api.json @@ -2,17 +2,20 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://raw.githubusercontent.com/blues/notecard-schema/master/card.triangulate.req.notecard.api.json", "title": "card.triangulate Request Application Programming Interface (API) Schema", - "description": "Request schema for card.triangulate API command.", + "description": "Triangulate the location of the Notecard using cellular towers and Wi-Fi access points.", "type": "object", "skus": [ "CELL", "CELL+WIFI", - "WIFI", - "LORA" + "WIFI" ], "version": "0.1.1", "apiVersion": "9.1.1", "properties": { + "on": { + "description": "When `true`, enables triangulation mode. When `false`, disables triangulation mode.", + "type": "boolean" + }, "cmd": { "description": "Command for the Notecard (no response)", "const": "card.triangulate" @@ -47,14 +50,25 @@ "additionalProperties": false, "samples": [ { - "description": "Basic card.triangulate request.", + "title": "Get Current Triangulation Location", + "description": "Retrieve the current triangulated location using cellular towers and Wi-Fi access points.", "json": "{\"req\":\"card.triangulate\"}" + }, + { + "title": "Enable Triangulation", + "description": "Enable triangulation mode for location tracking when GPS is unavailable.", + "json": "{\"req\":\"card.triangulate\",\"on\":true}" + }, + { + "title": "Disable Triangulation", + "description": "Disable triangulation mode to conserve power.", + "json": "{\"req\":\"card.triangulate\",\"on\":false}" } ], "annotations": [ { "title": "note", - "description": "Placeholder annotation." + "description": "Triangulation uses cellular tower and Wi-Fi access point signal strengths to estimate location when GPS is unavailable or disabled. Accuracy is typically less precise than GPS but can provide useful location information in urban environments with multiple cell towers or Wi-Fi networks." } ] } \ No newline at end of file diff --git a/card.triangulate.rsp.notecard.api.json b/card.triangulate.rsp.notecard.api.json index d70449b..91452fa 100644 --- a/card.triangulate.rsp.notecard.api.json +++ b/card.triangulate.rsp.notecard.api.json @@ -2,19 +2,67 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://raw.githubusercontent.com/blues/notecard-schema/master/card.triangulate.rsp.notecard.api.json", "title": "card.triangulate Response Application Programming Interface (API) Schema", + "description": "Response containing triangulated location data and positioning information derived from cellular towers and Wi-Fi access points.", "type": "object", "version": "0.1.1", "apiVersion": "9.1.1", "properties": { "status": { - "description": "Status of the operation", + "description": "The current status of the triangulation operation", "type": "string" + }, + "lat": { + "description": "The estimated latitude in degrees from triangulation", + "type": "number", + "minimum": -90, + "maximum": 90 + }, + "lon": { + "description": "The estimated longitude in degrees from triangulation", + "type": "number", + "minimum": -180, + "maximum": 180 + }, + "time": { + "description": "Unix epoch time in seconds when the triangulation was calculated", + "type": "integer", + "minimum": 0 + }, + "accuracy": { + "description": "Estimated accuracy of the triangulated position in meters", + "type": "number", + "minimum": 0 + }, + "towers": { + "description": "Number of cellular towers used in the triangulation calculation", + "type": "integer", + "minimum": 0 + }, + "wifi": { + "description": "Number of Wi-Fi access points used in the triangulation calculation", + "type": "integer", + "minimum": 0 + }, + "on": { + "description": "Indicates whether triangulation mode is currently enabled", + "type": "boolean" } }, "samples": [ { - "description": "Basic card.triangulate response.", - "json": "{\"status\": \"success\"}" + "title": "Successful Triangulation", + "description": "Example response from a successful triangulation with location data", + "json": "{\"status\":\"triangulated\",\"lat\":37.7749,\"lon\":-122.4194,\"time\":1678886400,\"accuracy\":150.5,\"towers\":3,\"wifi\":5,\"on\":true}" + }, + { + "title": "Insufficient Data", + "description": "Example response when insufficient towers or access points are available", + "json": "{\"status\":\"insufficient-data\",\"towers\":1,\"wifi\":0,\"on\":true}" + }, + { + "title": "Triangulation Disabled", + "description": "Example response when triangulation mode is disabled", + "json": "{\"status\":\"disabled\",\"on\":false}" } ] } \ No newline at end of file diff --git a/tests/test_card_triangulate_req.py b/tests/test_card_triangulate_req.py index 77e9885..990dc78 100644 --- a/tests/test_card_triangulate_req.py +++ b/tests/test_card_triangulate_req.py @@ -42,6 +42,34 @@ def test_invalid_additional_property_with_cmd(schema): jsonschema.validate(instance=instance, schema=schema) assert "Additional properties are not allowed ('extra' was unexpected)" in str(excinfo.value) +def test_on_field_valid(schema): + """Tests valid 'on' field values.""" + # Valid boolean values + instance = {"req": "card.triangulate", "on": True} + jsonschema.validate(instance=instance, schema=schema) + + instance = {"req": "card.triangulate", "on": False} + jsonschema.validate(instance=instance, schema=schema) + +def test_on_field_invalid_type(schema): + """Tests invalid type for 'on' field.""" + instance = {"req": "card.triangulate", "on": "true"} + with pytest.raises(jsonschema.ValidationError) as excinfo: + jsonschema.validate(instance=instance, schema=schema) + assert "'true' is not of type 'boolean'" in str(excinfo.value) + +def test_on_field_invalid_number(schema): + """Tests invalid number type for 'on' field.""" + instance = {"req": "card.triangulate", "on": 1} + with pytest.raises(jsonschema.ValidationError) as excinfo: + jsonschema.validate(instance=instance, schema=schema) + assert "1 is not of type 'boolean'" in str(excinfo.value) + +def test_valid_request_with_cmd_and_on(schema): + """Tests valid request using 'cmd' with 'on' parameter.""" + instance = {"cmd": "card.triangulate", "on": True} + jsonschema.validate(instance=instance, schema=schema) + def test_validate_samples_from_schema(schema, schema_samples): """Tests that samples in the schema definition are valid.""" for sample in schema_samples: diff --git a/tests/test_card_triangulate_rsp.py b/tests/test_card_triangulate_rsp.py index 036cff9..99b22cd 100644 --- a/tests/test_card_triangulate_rsp.py +++ b/tests/test_card_triangulate_rsp.py @@ -11,7 +11,7 @@ def test_minimal_valid_rsp(schema): def test_valid_status(schema): """Tests valid status field.""" - instance = {"status": "success"} + instance = {"status": "triangulated"} jsonschema.validate(instance=instance, schema=schema) def test_status_invalid_type(schema): @@ -21,9 +21,151 @@ def test_status_invalid_type(schema): jsonschema.validate(instance=instance, schema=schema) assert "123 is not of type 'string'" in str(excinfo.value) -def test_valid_additional_property(schema): - """Tests valid response with an additional property.""" - instance = {"status": "success", "additional": "property"} +def test_valid_location_fields(schema): + """Tests valid latitude and longitude fields.""" + instance = { + "lat": 37.7749, + "lon": -122.4194 + } + jsonschema.validate(instance=instance, schema=schema) + +def test_lat_boundary_values(schema): + """Tests latitude boundary values.""" + # Valid boundary values + instance = {"lat": -90} + jsonschema.validate(instance=instance, schema=schema) + + instance = {"lat": 90} + jsonschema.validate(instance=instance, schema=schema) + + # Invalid boundary values + instance = {"lat": -90.1} + with pytest.raises(jsonschema.ValidationError) as excinfo: + jsonschema.validate(instance=instance, schema=schema) + assert "-90.1 is less than the minimum of -90" in str(excinfo.value) + + instance = {"lat": 90.1} + with pytest.raises(jsonschema.ValidationError) as excinfo: + jsonschema.validate(instance=instance, schema=schema) + assert "90.1 is greater than the maximum of 90" in str(excinfo.value) + +def test_lon_boundary_values(schema): + """Tests longitude boundary values.""" + # Valid boundary values + instance = {"lon": -180} + jsonschema.validate(instance=instance, schema=schema) + + instance = {"lon": 180} + jsonschema.validate(instance=instance, schema=schema) + + # Invalid boundary values + instance = {"lon": -180.1} + with pytest.raises(jsonschema.ValidationError) as excinfo: + jsonschema.validate(instance=instance, schema=schema) + assert "-180.1 is less than the minimum of -180" in str(excinfo.value) + + instance = {"lon": 180.1} + with pytest.raises(jsonschema.ValidationError) as excinfo: + jsonschema.validate(instance=instance, schema=schema) + assert "180.1 is greater than the maximum of 180" in str(excinfo.value) + +def test_time_field(schema): + """Tests valid time field.""" + instance = {"time": 1678886400} + jsonschema.validate(instance=instance, schema=schema) + + # Time cannot be negative + instance = {"time": -1} + with pytest.raises(jsonschema.ValidationError) as excinfo: + jsonschema.validate(instance=instance, schema=schema) + assert "-1 is less than the minimum of 0" in str(excinfo.value) + +def test_accuracy_field(schema): + """Tests valid accuracy field.""" + instance = {"accuracy": 150.5} + jsonschema.validate(instance=instance, schema=schema) + + instance = {"accuracy": 0} + jsonschema.validate(instance=instance, schema=schema) + + # Accuracy cannot be negative + instance = {"accuracy": -10} + with pytest.raises(jsonschema.ValidationError) as excinfo: + jsonschema.validate(instance=instance, schema=schema) + assert "-10 is less than the minimum of 0" in str(excinfo.value) + +def test_towers_field(schema): + """Tests valid towers field.""" + instance = {"towers": 3} + jsonschema.validate(instance=instance, schema=schema) + + instance = {"towers": 0} + jsonschema.validate(instance=instance, schema=schema) + + # Towers cannot be negative + instance = {"towers": -1} + with pytest.raises(jsonschema.ValidationError) as excinfo: + jsonschema.validate(instance=instance, schema=schema) + assert "-1 is less than the minimum of 0" in str(excinfo.value) + +def test_wifi_field(schema): + """Tests valid wifi field.""" + instance = {"wifi": 5} + jsonschema.validate(instance=instance, schema=schema) + + instance = {"wifi": 0} + jsonschema.validate(instance=instance, schema=schema) + + # WiFi count cannot be negative + instance = {"wifi": -1} + with pytest.raises(jsonschema.ValidationError) as excinfo: + jsonschema.validate(instance=instance, schema=schema) + assert "-1 is less than the minimum of 0" in str(excinfo.value) + +def test_on_field(schema): + """Tests valid on field.""" + instance = {"on": True} + jsonschema.validate(instance=instance, schema=schema) + + instance = {"on": False} + jsonschema.validate(instance=instance, schema=schema) + + # Invalid type + instance = {"on": 1} + with pytest.raises(jsonschema.ValidationError) as excinfo: + jsonschema.validate(instance=instance, schema=schema) + assert "1 is not of type 'boolean'" in str(excinfo.value) + +def test_complete_triangulation_response(schema): + """Tests a complete valid triangulation response.""" + instance = { + "status": "triangulated", + "lat": 37.7749, + "lon": -122.4194, + "time": 1678886400, + "accuracy": 150.5, + "towers": 3, + "wifi": 5, + "on": True + } + jsonschema.validate(instance=instance, schema=schema) + +def test_insufficient_data_response(schema): + """Tests valid response when insufficient data is available.""" + instance = { + "status": "insufficient-data", + "towers": 1, + "wifi": 0, + "on": True + } + jsonschema.validate(instance=instance, schema=schema) + +def test_disabled_response(schema): + """Tests valid response when triangulation is disabled.""" + instance = { + "status": "disabled", + "on": False + } jsonschema.validate(instance=instance, schema=schema) def test_validate_samples_from_schema(schema, schema_samples): From 6da08376642db739af1304923d2120cde335bdd2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 25 Jul 2025 15:57:21 +0000 Subject: [PATCH 4/8] Update card.triangulate schema descriptions with verbatim API documentation text Co-authored-by: zakoverflow <215570245+zakoverflow@users.noreply.github.com> --- card.triangulate.req.notecard.api.json | 50 ++++++++++++++----- card.triangulate.rsp.notecard.api.json | 67 +++++++++----------------- 2 files changed, 60 insertions(+), 57 deletions(-) diff --git a/card.triangulate.req.notecard.api.json b/card.triangulate.req.notecard.api.json index 6fff3c1..407e29e 100644 --- a/card.triangulate.req.notecard.api.json +++ b/card.triangulate.req.notecard.api.json @@ -2,20 +2,46 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://raw.githubusercontent.com/blues/notecard-schema/master/card.triangulate.req.notecard.api.json", "title": "card.triangulate Request Application Programming Interface (API) Schema", - "description": "Triangulate the location of the Notecard using cellular towers and Wi-Fi access points.", + "description": "Enables or disables a behavior by which the Notecard gathers information about surrounding cell towers and/or Wi-Fi access points with each new Notehub session.", "type": "object", "skus": [ "CELL", "CELL+WIFI", "WIFI" ], - "version": "0.1.1", + "version": "0.1.2", "apiVersion": "9.1.1", "properties": { + "mode": { + "description": "The triangulation approach to use for determining the Notecard location. The following keywords can be used separately or together in a comma-delimited list, in any order. `cell` enables cell tower scanning to determine the position of the Device. `wifi` enables the use of nearby Wi-Fi access points to determine the position of the Device. To leverage this feature, the host will need to provide access point information to the Notecard via the `text` argument in subsequent requests. `-` to clear the currently-set triangulation mode.", + "type": "string" + }, "on": { - "description": "When `true`, enables triangulation mode. When `false`, disables triangulation mode.", + "description": "`true` to instruct the Notecard to triangulate even if the module has not moved. Only takes effect when `set` is `true`.", + "type": "boolean" + }, + "usb": { + "description": "`true` to use perform triangulation only when the Notecard is connected to USB power.", + "type": "boolean" + }, + "set": { + "description": "`true` to instruct the module to use the state of the `on` and `usb` arguments.", "type": "boolean" }, + "minutes": { + "description": "Minimum delay, in minutes, between triangulation attempts. Use `0` for no time-based suppression.", + "type": "integer", + "minimum": 0 + }, + "text": { + "description": "When using Wi-Fi triangulation, a newline-terminated list of Wi-Fi access points obtained by the external module. Format should follow the ESP32's AT+CWLAP command output.", + "type": "string" + }, + "time": { + "description": "When passed with `text`, records the time that the Wi-Fi access point scan was performed. If not provided, Notecard time is used.", + "type": "integer", + "minimum": 0 + }, "cmd": { "description": "Command for the Notecard (no response)", "const": "card.triangulate" @@ -50,25 +76,25 @@ "additionalProperties": false, "samples": [ { - "title": "Get Current Triangulation Location", - "description": "Retrieve the current triangulated location using cellular towers and Wi-Fi access points.", - "json": "{\"req\":\"card.triangulate\"}" + "title": "Single Mode", + "description": "Enable triangulation using cell towers only.", + "json": "{\"req\":\"card.triangulate\",\"mode\":\"cell\",\"on\":true,\"set\":true}" }, { - "title": "Enable Triangulation", - "description": "Enable triangulation mode for location tracking when GPS is unavailable.", - "json": "{\"req\":\"card.triangulate\",\"on\":true}" + "title": "Dual Mode", + "description": "Enable triangulation using both Wi-Fi and cell towers when connected to USB power.", + "json": "{\"req\":\"card.triangulate\",\"mode\":\"wifi,cell\",\"on\":true,\"usb\":true,\"set\":true}" }, { "title": "Disable Triangulation", - "description": "Disable triangulation mode to conserve power.", - "json": "{\"req\":\"card.triangulate\",\"on\":false}" + "description": "Disable triangulation mode.", + "json": "{\"req\":\"card.triangulate\",\"mode\":\"-\"}" } ], "annotations": [ { "title": "note", - "description": "Triangulation uses cellular tower and Wi-Fi access point signal strengths to estimate location when GPS is unavailable or disabled. Accuracy is typically less precise than GPS but can provide useful location information in urban environments with multiple cell towers or Wi-Fi networks." + "description": "See [Using Cell Tower & Wi-Fi Triangulation](https://dev.blues.io/guides-and-tutorials/collecting-sensor-data/notecard-guides/using-cell-tower-wi-fi-triangulation/) for more information." } ] } \ No newline at end of file diff --git a/card.triangulate.rsp.notecard.api.json b/card.triangulate.rsp.notecard.api.json index 91452fa..70c9184 100644 --- a/card.triangulate.rsp.notecard.api.json +++ b/card.triangulate.rsp.notecard.api.json @@ -2,67 +2,44 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://raw.githubusercontent.com/blues/notecard-schema/master/card.triangulate.rsp.notecard.api.json", "title": "card.triangulate Response Application Programming Interface (API) Schema", - "description": "Response containing triangulated location data and positioning information derived from cellular towers and Wi-Fi access points.", + "description": "Response containing triangulated location information and triangulation settings.", "type": "object", - "version": "0.1.1", + "version": "0.1.2", "apiVersion": "9.1.1", "properties": { - "status": { - "description": "The current status of the triangulation operation", - "type": "string" - }, - "lat": { - "description": "The estimated latitude in degrees from triangulation", - "type": "number", - "minimum": -90, - "maximum": 90 - }, - "lon": { - "description": "The estimated longitude in degrees from triangulation", - "type": "number", - "minimum": -180, - "maximum": 180 - }, - "time": { - "description": "Unix epoch time in seconds when the triangulation was calculated", + "motion": { + "description": "Time of last detected Notecard movement.", "type": "integer", "minimum": 0 }, - "accuracy": { - "description": "Estimated accuracy of the triangulated position in meters", - "type": "number", - "minimum": 0 - }, - "towers": { - "description": "Number of cellular towers used in the triangulation calculation", + "time": { + "description": "Time of last triangulation scan.", "type": "integer", "minimum": 0 }, - "wifi": { - "description": "Number of Wi-Fi access points used in the triangulation calculation", - "type": "integer", - "minimum": 0 + "mode": { + "description": "A comma-separated list indicating the active triangulation modes.", + "type": "string" }, "on": { - "description": "Indicates whether triangulation mode is currently enabled", + "description": "`true` if triangulation scans will be performed even if the device has not moved.", + "type": "boolean" + }, + "usb": { + "description": "`true` if triangulation scans will be performed only when the device is USB-powered.", "type": "boolean" + }, + "length": { + "description": "The length of the `text` buffer provided in the current or a previous request.", + "type": "integer", + "minimum": 0 } }, "samples": [ { - "title": "Successful Triangulation", - "description": "Example response from a successful triangulation with location data", - "json": "{\"status\":\"triangulated\",\"lat\":37.7749,\"lon\":-122.4194,\"time\":1678886400,\"accuracy\":150.5,\"towers\":3,\"wifi\":5,\"on\":true}" - }, - { - "title": "Insufficient Data", - "description": "Example response when insufficient towers or access points are available", - "json": "{\"status\":\"insufficient-data\",\"towers\":1,\"wifi\":0,\"on\":true}" - }, - { - "title": "Triangulation Disabled", - "description": "Example response when triangulation mode is disabled", - "json": "{\"status\":\"disabled\",\"on\":false}" + "title": "Example Response", + "description": "Response showing triangulation configuration and status.", + "json": "{\"usb\":true,\"mode\":\"wifi,cell\",\"length\":443,\"on\":true,\"time\":1606755042,\"motion\":1606757487}" } ] } \ No newline at end of file From f61ebceee83a179a9a4ea7becef08f93c5fb5e4f Mon Sep 17 00:00:00 2001 From: "Zachary J. Fields" Date: Fri, 25 Jul 2025 14:00:05 -0500 Subject: [PATCH 5/8] chore: Manually update card.triangulate --- card.triangulate.req.notecard.api.json | 65 +++++++++++++++++--------- 1 file changed, 44 insertions(+), 21 deletions(-) diff --git a/card.triangulate.req.notecard.api.json b/card.triangulate.req.notecard.api.json index 407e29e..7bd1885 100644 --- a/card.triangulate.req.notecard.api.json +++ b/card.triangulate.req.notecard.api.json @@ -9,46 +9,64 @@ "CELL+WIFI", "WIFI" ], - "version": "0.1.2", + "version": "0.2.1", "apiVersion": "9.1.1", "properties": { + "cmd": { + "description": "Command for the Notecard (no response)", + "const": "card.triangulate" + }, + "req": { + "description": "Request for the Notecard (expects response)", + "const": "card.triangulate" + }, "mode": { - "description": "The triangulation approach to use for determining the Notecard location. The following keywords can be used separately or together in a comma-delimited list, in any order. `cell` enables cell tower scanning to determine the position of the Device. `wifi` enables the use of nearby Wi-Fi access points to determine the position of the Device. To leverage this feature, the host will need to provide access point information to the Notecard via the `text` argument in subsequent requests. `-` to clear the currently-set triangulation mode.", - "type": "string" + "description": "The triangulation approach to use for determining the Notecard location. The following keywords can be used separately or together in a comma-delimited list, in any order.", + "type": "string", + "pattern": "^(cell(,wifi)?|wifi(,cell)?|-)$", + "sub-descriptions": [ + { + "const": "cell", + "description": "Enables cell tower scanning to determine the position of the Device." + }, + { + "const": "wifi", + "description": "Enables the use of nearby Wi-Fi access points to determine the position of the Device. To leverage this feature, the host will need to provide access point information to the Notecard via the `text` argument in subsequent requests." + }, + { + "const": "-", + "description": "Clear the currently-set triangulation mode." + } + ] }, "on": { "description": "`true` to instruct the Notecard to triangulate even if the module has not moved. Only takes effect when `set` is `true`.", - "type": "boolean" + "type": "boolean", + "default": false }, "usb": { - "description": "`true` to use perform triangulation only when the Notecard is connected to USB power.", - "type": "boolean" + "description": "`true` to use perform triangulation only when the Notecard is connected to USB power. Only takes effect when `set` is `true`.", + "type": "boolean", + "default": false }, "set": { "description": "`true` to instruct the module to use the state of the `on` and `usb` arguments.", - "type": "boolean" + "type": "boolean", + "default": false }, "minutes": { "description": "Minimum delay, in minutes, between triangulation attempts. Use `0` for no time-based suppression.", "type": "integer", - "minimum": 0 + "default": 0, + "minimum": -1 }, "text": { - "description": "When using Wi-Fi triangulation, a newline-terminated list of Wi-Fi access points obtained by the external module. Format should follow the ESP32's AT+CWLAP command output.", + "description": "When using Wi-Fi triangulation, a newline-terminated list of Wi-Fi access points obtained by the external module. Format should follow the ESP32's [AT+CWLAP command output](https://docs.espressif.com/projects/esp-at/en/latest/AT_Command_Set/Wi-Fi_AT_Commands.html#cmd-lap).", "type": "string" }, "time": { - "description": "When passed with `text`, records the time that the Wi-Fi access point scan was performed. If not provided, Notecard time is used.", - "type": "integer", - "minimum": 0 - }, - "cmd": { - "description": "Command for the Notecard (no response)", - "const": "card.triangulate" - }, - "req": { - "description": "Request for the Notecard (expects response)", - "const": "card.triangulate" + "description": "When passed with `text`, records the time that the Wi-Fi access point scan was performed. _If not provided, Notecard time is used._", + "type": "integer" } }, "oneOf": [ @@ -81,10 +99,15 @@ "json": "{\"req\":\"card.triangulate\",\"mode\":\"cell\",\"on\":true,\"set\":true}" }, { - "title": "Dual Mode", + "title": "Dual Mode", "description": "Enable triangulation using both Wi-Fi and cell towers when connected to USB power.", "json": "{\"req\":\"card.triangulate\",\"mode\":\"wifi,cell\",\"on\":true,\"usb\":true,\"set\":true}" }, + { + "title": "Send Wi-Fi AP Data", + "description": "Send a newline-terminated list of Wi-Fi access points to the Notecard for triangulation.", + "json": "{\"req\":\"card.triangulate\",\"text\":\"+CWLAP:(4,\\\"Blues\\\",-51,\\\"74:ac:b9:12:12:f8\\\",1)\\n+CWLAP:(3,\\\"AAAA-62DD\\\",-70,\\\"6c:55:e8:91:62:e1\\\",11)\\n+CWLAP:(4,\\\"Blues\\\",-81,\\\"74:ac:b9:11:12:23\\\",1)\\n+CWLAP:(4,\\\"Blues\\\",-82,\\\"74:ac:a9:12:19:48\\\",11)\\n+CWLAP:(4,\\\"Free Parking\\\",-83,\\\"02:18:4a:11:60:31\\\",6)\\n+CWLAP:(5,\\\"GO\\\",-84,\\\"01:13:6a:13:90:30\\\",6)\\n+CWLAP:(4,\\\"AAAA-5C62-2.4\\\",-85,\\\"d8:97:ba:7b:fd:60\\\",1)\\n+CWLAP:(3,\\\"DIRECT-a5-HP MLP50\\\",-86,\\\"fa:da:0c:1b:16:a5\\\",6)\\n+CWLAP:(3,\\\"DIRECT-c6-HP M182 LaserJet\\\",-88,\\\"da:12:65:44:31:c6\\\",6)\\n\\n\"}" + }, { "title": "Disable Triangulation", "description": "Disable triangulation mode.", From 0dd3dc547be2f1f7c2daeba55d16a54a32f0a8b0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 25 Jul 2025 21:29:26 +0000 Subject: [PATCH 6/8] fix: Update card.triangulate response tests to match manually updated schema Co-authored-by: zakoverflow <215570245+zakoverflow@users.noreply.github.com> --- tests/test_card_triangulate_rsp.py | 167 +++++++++++------------------ 1 file changed, 63 insertions(+), 104 deletions(-) diff --git a/tests/test_card_triangulate_rsp.py b/tests/test_card_triangulate_rsp.py index 99b22cd..b9ea1e5 100644 --- a/tests/test_card_triangulate_rsp.py +++ b/tests/test_card_triangulate_rsp.py @@ -9,69 +9,20 @@ def test_minimal_valid_rsp(schema): instance = {} jsonschema.validate(instance=instance, schema=schema) -def test_valid_status(schema): - """Tests valid status field.""" - instance = {"status": "triangulated"} +def test_motion_field(schema): + """Tests valid motion field.""" + instance = {"motion": 1606757487} jsonschema.validate(instance=instance, schema=schema) - -def test_status_invalid_type(schema): - """Tests invalid type for status.""" - instance = {"status": 123} - with pytest.raises(jsonschema.ValidationError) as excinfo: - jsonschema.validate(instance=instance, schema=schema) - assert "123 is not of type 'string'" in str(excinfo.value) - -def test_valid_location_fields(schema): - """Tests valid latitude and longitude fields.""" - instance = { - "lat": 37.7749, - "lon": -122.4194 - } - jsonschema.validate(instance=instance, schema=schema) - -def test_lat_boundary_values(schema): - """Tests latitude boundary values.""" - # Valid boundary values - instance = {"lat": -90} - jsonschema.validate(instance=instance, schema=schema) - - instance = {"lat": 90} - jsonschema.validate(instance=instance, schema=schema) - - # Invalid boundary values - instance = {"lat": -90.1} - with pytest.raises(jsonschema.ValidationError) as excinfo: - jsonschema.validate(instance=instance, schema=schema) - assert "-90.1 is less than the minimum of -90" in str(excinfo.value) - instance = {"lat": 90.1} + # Motion cannot be negative + instance = {"motion": -1} with pytest.raises(jsonschema.ValidationError) as excinfo: jsonschema.validate(instance=instance, schema=schema) - assert "90.1 is greater than the maximum of 90" in str(excinfo.value) - -def test_lon_boundary_values(schema): - """Tests longitude boundary values.""" - # Valid boundary values - instance = {"lon": -180} - jsonschema.validate(instance=instance, schema=schema) - - instance = {"lon": 180} - jsonschema.validate(instance=instance, schema=schema) - - # Invalid boundary values - instance = {"lon": -180.1} - with pytest.raises(jsonschema.ValidationError) as excinfo: - jsonschema.validate(instance=instance, schema=schema) - assert "-180.1 is less than the minimum of -180" in str(excinfo.value) - - instance = {"lon": 180.1} - with pytest.raises(jsonschema.ValidationError) as excinfo: - jsonschema.validate(instance=instance, schema=schema) - assert "180.1 is greater than the maximum of 180" in str(excinfo.value) + assert "-1 is less than the minimum of 0" in str(excinfo.value) def test_time_field(schema): """Tests valid time field.""" - instance = {"time": 1678886400} + instance = {"time": 1606755042} jsonschema.validate(instance=instance, schema=schema) # Time cannot be negative @@ -80,91 +31,99 @@ def test_time_field(schema): jsonschema.validate(instance=instance, schema=schema) assert "-1 is less than the minimum of 0" in str(excinfo.value) -def test_accuracy_field(schema): - """Tests valid accuracy field.""" - instance = {"accuracy": 150.5} +def test_mode_field(schema): + """Tests valid mode field.""" + instance = {"mode": "wifi,cell"} jsonschema.validate(instance=instance, schema=schema) - instance = {"accuracy": 0} + instance = {"mode": "cell"} jsonschema.validate(instance=instance, schema=schema) - # Accuracy cannot be negative - instance = {"accuracy": -10} + instance = {"mode": ""} + jsonschema.validate(instance=instance, schema=schema) + +def test_mode_invalid_type(schema): + """Tests invalid type for mode.""" + instance = {"mode": 123} with pytest.raises(jsonschema.ValidationError) as excinfo: jsonschema.validate(instance=instance, schema=schema) - assert "-10 is less than the minimum of 0" in str(excinfo.value) + assert "123 is not of type 'string'" in str(excinfo.value) -def test_towers_field(schema): - """Tests valid towers field.""" - instance = {"towers": 3} +def test_on_field(schema): + """Tests valid on field.""" + instance = {"on": True} jsonschema.validate(instance=instance, schema=schema) - instance = {"towers": 0} + instance = {"on": False} jsonschema.validate(instance=instance, schema=schema) - - # Towers cannot be negative - instance = {"towers": -1} + +def test_on_invalid_type(schema): + """Tests invalid type for on.""" + instance = {"on": 1} with pytest.raises(jsonschema.ValidationError) as excinfo: jsonschema.validate(instance=instance, schema=schema) - assert "-1 is less than the minimum of 0" in str(excinfo.value) + assert "1 is not of type 'boolean'" in str(excinfo.value) -def test_wifi_field(schema): - """Tests valid wifi field.""" - instance = {"wifi": 5} +def test_usb_field(schema): + """Tests valid usb field.""" + instance = {"usb": True} jsonschema.validate(instance=instance, schema=schema) - instance = {"wifi": 0} + instance = {"usb": False} jsonschema.validate(instance=instance, schema=schema) - - # WiFi count cannot be negative - instance = {"wifi": -1} + +def test_usb_invalid_type(schema): + """Tests invalid type for usb.""" + instance = {"usb": "yes"} with pytest.raises(jsonschema.ValidationError) as excinfo: jsonschema.validate(instance=instance, schema=schema) - assert "-1 is less than the minimum of 0" in str(excinfo.value) + assert "'yes' is not of type 'boolean'" in str(excinfo.value) -def test_on_field(schema): - """Tests valid on field.""" - instance = {"on": True} +def test_length_field(schema): + """Tests valid length field.""" + instance = {"length": 443} jsonschema.validate(instance=instance, schema=schema) - instance = {"on": False} + instance = {"length": 0} jsonschema.validate(instance=instance, schema=schema) - # Invalid type - instance = {"on": 1} + # Length cannot be negative + instance = {"length": -1} with pytest.raises(jsonschema.ValidationError) as excinfo: jsonschema.validate(instance=instance, schema=schema) - assert "1 is not of type 'boolean'" in str(excinfo.value) + assert "-1 is less than the minimum of 0" in str(excinfo.value) + +def test_length_invalid_type(schema): + """Tests invalid type for length.""" + instance = {"length": "443"} + with pytest.raises(jsonschema.ValidationError) as excinfo: + jsonschema.validate(instance=instance, schema=schema) + assert "'443' is not of type 'integer'" in str(excinfo.value) def test_complete_triangulation_response(schema): """Tests a complete valid triangulation response.""" instance = { - "status": "triangulated", - "lat": 37.7749, - "lon": -122.4194, - "time": 1678886400, - "accuracy": 150.5, - "towers": 3, - "wifi": 5, - "on": True + "usb": True, + "mode": "wifi,cell", + "length": 443, + "on": True, + "time": 1606755042, + "motion": 1606757487 } jsonschema.validate(instance=instance, schema=schema) -def test_insufficient_data_response(schema): - """Tests valid response when insufficient data is available.""" +def test_partial_response(schema): + """Tests valid partial response.""" instance = { - "status": "insufficient-data", - "towers": 1, - "wifi": 0, - "on": True + "on": False, + "usb": True } jsonschema.validate(instance=instance, schema=schema) -def test_disabled_response(schema): - """Tests valid response when triangulation is disabled.""" +def test_mode_only_response(schema): + """Tests valid response with mode only.""" instance = { - "status": "disabled", - "on": False + "mode": "cell" } jsonschema.validate(instance=instance, schema=schema) From df40eba4cd2ab24023bda33d7a207a463a80e574 Mon Sep 17 00:00:00 2001 From: "Zachary J. Fields" Date: Fri, 25 Jul 2025 14:00:05 -0500 Subject: [PATCH 7/8] chore: Manually update card.triangulate response --- card.triangulate.rsp.notecard.api.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/card.triangulate.rsp.notecard.api.json b/card.triangulate.rsp.notecard.api.json index 70c9184..9fe2607 100644 --- a/card.triangulate.rsp.notecard.api.json +++ b/card.triangulate.rsp.notecard.api.json @@ -8,12 +8,12 @@ "apiVersion": "9.1.1", "properties": { "motion": { - "description": "Time of last detected Notecard movement.", + "description": "UNIX Epoch time of last detected Notecard movement.", "type": "integer", "minimum": 0 }, "time": { - "description": "Time of last triangulation scan.", + "description": "UNIX Epoch time of last triangulation scan.", "type": "integer", "minimum": 0 }, From 8c544d3315316790105576f089be0f991fd61f63 Mon Sep 17 00:00:00 2001 From: "Zachary J. Fields (Copilot)" Date: Fri, 25 Jul 2025 16:53:38 -0500 Subject: [PATCH 8/8] Update card.triangulate.req.notecard.api.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- card.triangulate.req.notecard.api.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/card.triangulate.req.notecard.api.json b/card.triangulate.req.notecard.api.json index 7bd1885..768493e 100644 --- a/card.triangulate.req.notecard.api.json +++ b/card.triangulate.req.notecard.api.json @@ -45,7 +45,7 @@ "default": false }, "usb": { - "description": "`true` to use perform triangulation only when the Notecard is connected to USB power. Only takes effect when `set` is `true`.", + "description": "`true` to perform triangulation only when the Notecard is connected to USB power. Only takes effect when `set` is `true`.", "type": "boolean", "default": false },