Files

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
```