322 lines
7.3 KiB
Markdown
322 lines
7.3 KiB
Markdown
# WebDev API Contract — CLAUDE.md
|
|
|
|
Parent: [../CLAUDE.md](../CLAUDE.md)
|
|
|
|
## Overview
|
|
|
|
All external interaction with Ignition tags goes through WebDev endpoints.
|
|
No external tool should read or write tag files directly at runtime.
|
|
The test runner, CLI tools, and any future integrations use these endpoints.
|
|
|
|
## Base URL
|
|
|
|
```
|
|
http://<gateway>:8088/system/webdev/<project-name>
|
|
```
|
|
|
|
Default: `http://localhost:8088/system/webdev/framework`
|
|
|
|
## Endpoints
|
|
|
|
### POST /tagWrite
|
|
|
|
Write one or more tag values.
|
|
|
|
**Request:**
|
|
```json
|
|
{
|
|
"writes": [
|
|
{
|
|
"path": "[default]Devices/Pumps/P-101/Command/Start",
|
|
"value": true
|
|
},
|
|
{
|
|
"path": "[default]Devices/Pumps/P-101/Command/SpeedSetpoint",
|
|
"value": 60.0
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Response (200):**
|
|
```json
|
|
{
|
|
"status": "ok",
|
|
"results": [
|
|
{
|
|
"path": "[default]Devices/Pumps/P-101/Command/Start",
|
|
"success": true
|
|
},
|
|
{
|
|
"path": "[default]Devices/Pumps/P-101/Command/SpeedSetpoint",
|
|
"success": true
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Response (400 — bad request):**
|
|
```json
|
|
{
|
|
"status": "error",
|
|
"message": "Invalid tag path: [default]Devices/Pumps/P-999/Command/Start"
|
|
}
|
|
```
|
|
|
|
**Handler (Jython 2.7):**
|
|
```python
|
|
# WebDev endpoint: tagWrite
|
|
# Method: POST
|
|
# Jython 2.7 — no f-strings, no type hints
|
|
|
|
import json
|
|
|
|
def doPost(request, session):
|
|
logger = system.util.getLogger("webdev.tagWrite")
|
|
try:
|
|
payload = json.loads(request["data"])
|
|
writes = payload.get("writes", [])
|
|
|
|
if not writes:
|
|
return {"status": "error", "message": "No writes provided"}
|
|
|
|
paths = [w["path"] for w in writes]
|
|
values = [w["value"] for w in writes]
|
|
|
|
results = system.tag.writeBlocking(paths, values)
|
|
|
|
response_results = []
|
|
for i, qv in enumerate(results):
|
|
response_results.append({
|
|
"path": paths[i],
|
|
"success": qv.isGood()
|
|
})
|
|
|
|
return {"status": "ok", "results": response_results}
|
|
|
|
except Exception as e:
|
|
logger.error("tagWrite error: %s" % str(e))
|
|
return {"status": "error", "message": str(e)}
|
|
```
|
|
|
|
### GET /tagRead
|
|
|
|
Read one or more tag values.
|
|
|
|
**Request:**
|
|
```
|
|
GET /tagRead?paths=[default]Devices/Pumps/P-101/Status/Running,[default]Devices/Pumps/P-101/Status/Speed
|
|
```
|
|
|
|
Multiple paths are comma-separated in the query string.
|
|
|
|
**Response (200):**
|
|
```json
|
|
{
|
|
"status": "ok",
|
|
"results": [
|
|
{
|
|
"path": "[default]Devices/Pumps/P-101/Status/Running",
|
|
"value": true,
|
|
"quality": "Good",
|
|
"timestamp": "2026-03-14T10:30:00Z"
|
|
},
|
|
{
|
|
"path": "[default]Devices/Pumps/P-101/Status/Speed",
|
|
"value": 59.8,
|
|
"quality": "Good",
|
|
"timestamp": "2026-03-14T10:30:00Z"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Handler (Jython 2.7):**
|
|
```python
|
|
# WebDev endpoint: tagRead
|
|
# Method: GET
|
|
# Jython 2.7 — no f-strings, no type hints
|
|
|
|
def doGet(request, session):
|
|
logger = system.util.getLogger("webdev.tagRead")
|
|
try:
|
|
paths_param = request["params"].get("paths", "")
|
|
if not paths_param:
|
|
return {"status": "error", "message": "No paths provided"}
|
|
|
|
paths = [p.strip() for p in paths_param.split(",")]
|
|
qvs = system.tag.readBlocking(paths)
|
|
|
|
results = []
|
|
for i, qv in enumerate(qvs):
|
|
results.append({
|
|
"path": paths[i],
|
|
"value": qv.value,
|
|
"quality": str(qv.quality),
|
|
"timestamp": str(qv.timestamp)
|
|
})
|
|
|
|
return {"status": "ok", "results": results}
|
|
|
|
except Exception as e:
|
|
logger.error("tagRead error: %s" % str(e))
|
|
return {"status": "error", "message": str(e)}
|
|
```
|
|
|
|
### GET /tagBrowse
|
|
|
|
Browse the tag tree from a root path.
|
|
|
|
**Request:**
|
|
```
|
|
GET /tagBrowse?root=[default]Devices/Pumps
|
|
```
|
|
|
|
**Response (200):**
|
|
```json
|
|
{
|
|
"status": "ok",
|
|
"root": "[default]Devices/Pumps",
|
|
"children": [
|
|
{
|
|
"name": "P-101",
|
|
"path": "[default]Devices/Pumps/P-101",
|
|
"type": "UdtInstance",
|
|
"has_children": true
|
|
},
|
|
{
|
|
"name": "P-102",
|
|
"path": "[default]Devices/Pumps/P-102",
|
|
"type": "UdtInstance",
|
|
"has_children": true
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Handler (Jython 2.7):**
|
|
```python
|
|
# WebDev endpoint: tagBrowse
|
|
# Method: GET
|
|
# Jython 2.7 — no f-strings, no type hints
|
|
|
|
def doGet(request, session):
|
|
logger = system.util.getLogger("webdev.tagBrowse")
|
|
try:
|
|
root = request["params"].get("root", "")
|
|
browse_results = system.tag.browse(root)
|
|
|
|
children = []
|
|
for result in browse_results.getResults():
|
|
children.append({
|
|
"name": result["name"],
|
|
"path": str(result["fullPath"]),
|
|
"type": str(result["tagType"]),
|
|
"has_children": result["hasChildren"]
|
|
})
|
|
|
|
return {"status": "ok", "root": root, "children": children}
|
|
|
|
except Exception as e:
|
|
logger.error("tagBrowse error: %s" % str(e))
|
|
return {"status": "error", "message": str(e)}
|
|
```
|
|
|
|
### GET /health
|
|
|
|
Check gateway and connection status.
|
|
|
|
**Request:**
|
|
```
|
|
GET /health
|
|
```
|
|
|
|
**Response (200):**
|
|
```json
|
|
{
|
|
"status": "ok",
|
|
"gateway": {
|
|
"state": "RUNNING",
|
|
"uptime_ms": 3600000
|
|
},
|
|
"connections": {
|
|
"plc": {
|
|
"ControlLogix": {
|
|
"connected": true,
|
|
"status": "Connected"
|
|
}
|
|
},
|
|
"database": {
|
|
"postgres": {
|
|
"connected": true,
|
|
"status": "Valid"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Rules
|
|
|
|
1. **All external I/O goes through these endpoints.** No exceptions at runtime.
|
|
2. **Never bypass WebDev** to edit tag JSON files while the gateway is running.
|
|
3. **Validate inputs** in every handler — check for missing fields, bad paths.
|
|
4. **Log errors** with `system.util.getLogger()` in every handler.
|
|
5. **Return consistent JSON** — always include `status` field ("ok" or "error").
|
|
6. **Jython 2.7 only** in handler code — no f-strings, no type hints.
|
|
|
|
## Error Handling Convention
|
|
|
|
All endpoints return this structure on error:
|
|
|
|
```json
|
|
{
|
|
"status": "error",
|
|
"message": "Human-readable error description"
|
|
}
|
|
```
|
|
|
|
HTTP status codes:
|
|
- `200` — success (status: "ok")
|
|
- `400` — bad request (missing params, invalid paths)
|
|
- `500` — server error (unhandled exceptions)
|
|
|
|
## Verification Commands
|
|
|
|
Quick checks from the host or test runner:
|
|
|
|
```bash
|
|
# Health check
|
|
curl -s http://localhost:8088/system/webdev/framework/health | python3 -m json.tool
|
|
|
|
# Read a tag
|
|
curl -s "http://localhost:8088/system/webdev/framework/tagRead?paths=[default]Devices/Pumps/P-101/Status/Running" | python3 -m json.tool
|
|
|
|
# Write a tag
|
|
curl -s -X POST http://localhost:8088/system/webdev/framework/tagWrite \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"writes":[{"path":"[default]Devices/Pumps/P-101/Command/Start","value":true}]}' | python3 -m json.tool
|
|
|
|
# Browse tags
|
|
curl -s "http://localhost:8088/system/webdev/framework/tagBrowse?root=[default]Devices" | python3 -m json.tool
|
|
```
|
|
|
|
## File Structure
|
|
|
|
```
|
|
webdev/
|
|
├── CLAUDE.md ← you are here
|
|
└── endpoints/
|
|
├── tagWrite/
|
|
│ ├── code.py ← handler source (Jython 2.7)
|
|
│ └── resource.json
|
|
├── tagRead/
|
|
│ ├── code.py
|
|
│ └── resource.json
|
|
├── tagBrowse/
|
|
│ ├── code.py
|
|
│ └── resource.json
|
|
└── health/
|
|
├── code.py
|
|
└── resource.json
|
|
``` |