Initial scaffold: CLAUDE.md modules, directory structure, .gitignore
This commit is contained in:
40
.gitignore
vendored
Normal file
40
.gitignore
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Secrets
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
|
||||||
|
# Backups (created by safety rules before overwrites)
|
||||||
|
*.bak
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
|
||||||
|
# Docker volumes (data managed by Docker, not Git)
|
||||||
|
docker/volumes/
|
||||||
|
|
||||||
|
# Ignition runtime data (not project resources)
|
||||||
|
ignition/data/
|
||||||
|
*.gwbk
|
||||||
|
|
||||||
|
# Test results (generated output, not source)
|
||||||
|
testing/results/
|
||||||
|
|
||||||
|
# IDE / Editor
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Node (if any tooling uses it)
|
||||||
|
node_modules/
|
||||||
127
CLAUDE.md
Normal file
127
CLAUDE.md
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
# Ignition + Docker Development Framework
|
||||||
|
|
||||||
|
## What This Is
|
||||||
|
|
||||||
|
Reusable scaffold for Ignition + Docker projects with Claude Code.
|
||||||
|
The PLC I/O Testing Platform is the first project built on this framework.
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- Ignition 8.3 (Git-native project storage, Perspective, WebDev, OPC-UA)
|
||||||
|
- Rockwell Allen-Bradley ControlLogix/CompactLogix, Studio 5000, PlantPAx
|
||||||
|
- Docker Compose on Ubuntu 24 (Ignition, PostgreSQL, Traefik, Modbus sims)
|
||||||
|
- Gitea at 192.168.3.59:3000 for source control
|
||||||
|
- Jython 2.7 inside Ignition; Python 3.10+ for external tooling
|
||||||
|
- Claude Code on the Linux host (VS Code Remote to Docker host)
|
||||||
|
|
||||||
|
## Language Rules
|
||||||
|
|
||||||
|
### Jython 2.7 (Inside Ignition)
|
||||||
|
|
||||||
|
All scripts in `ignition/project/` and gateway scripting run Jython 2.7.
|
||||||
|
Hard constraints — violating these causes runtime errors:
|
||||||
|
|
||||||
|
- NO f-strings → use `"text {}".format(val)` or `"text %s" % val`
|
||||||
|
- NO walrus operator `:=`
|
||||||
|
- NO `pathlib` → use `os.path`
|
||||||
|
- NO type hints → no `def foo(x: int) -> str:`
|
||||||
|
- NO `dataclasses`, `enum.auto()`, `asyncio`
|
||||||
|
- NO dictionary unpacking `{**d1, **d2}` → use `d1.update(d2)`
|
||||||
|
- NO `yield from` → use explicit loops
|
||||||
|
- Imports: `system.*` for Ignition API, standard Java/Jython libs only
|
||||||
|
|
||||||
|
### Python 3.10+ (External Tooling)
|
||||||
|
|
||||||
|
All scripts outside Ignition (test runners, utilities, CI/CD) use Python 3.10+.
|
||||||
|
Comment the target environment at the top of every file:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# env: python3.10+ (external tooling — NOT Ignition)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Safety Rules (Non-Negotiable)
|
||||||
|
|
||||||
|
1. **Validate JSON** before writing to any Ignition resource directory.
|
||||||
|
2. **Back up files** before overwriting: `cp file file.bak`
|
||||||
|
3. **Sorted-key JSON** for all JSON files (Git-friendly diffs).
|
||||||
|
4. **Never restart Ignition gateway** without explicit user confirmation.
|
||||||
|
5. **Never `docker compose down`** without confirmation (destroys unnamed volumes).
|
||||||
|
6. **Every change needs a verification step** — scenario run, curl, tag read.
|
||||||
|
|
||||||
|
## Canonical Patterns (Do Not Change Without Discussion)
|
||||||
|
|
||||||
|
### UDT / Linking Pattern
|
||||||
|
|
||||||
|
Linking/Link sub-UDT with `LinkPath` + `LinkData` for PLC signal binding.
|
||||||
|
Decouples simulation from PLC I/O structure. All device test tags follow this.
|
||||||
|
|
||||||
|
### JSON Test Scenarios
|
||||||
|
|
||||||
|
Five required sections:
|
||||||
|
- `identity` — scenario ID, version, description
|
||||||
|
- `device` — device type, UDT path, tag references
|
||||||
|
- `initial_state` — tag values to set before test begins
|
||||||
|
- `sequence` — ordered steps with `delay_ms` between actions
|
||||||
|
- `expected_outcomes` — pass/fail criteria with `tolerance`
|
||||||
|
|
||||||
|
See `testing/CLAUDE.md` for full schema.
|
||||||
|
|
||||||
|
### WebDev API
|
||||||
|
|
||||||
|
All external I/O goes through WebDev endpoints — never direct tag file edits at runtime:
|
||||||
|
- `tagWrite` — write values to tags
|
||||||
|
- `tagRead` — read tag values
|
||||||
|
- `tagBrowse` — browse tag tree
|
||||||
|
- `health` — gateway and connection status
|
||||||
|
|
||||||
|
See `webdev/CLAUDE.md` for endpoint contracts.
|
||||||
|
|
||||||
|
### Ignition as I/O Orchestrator
|
||||||
|
|
||||||
|
Ignition writes to Modbus containers and PLC I/O tags.
|
||||||
|
PLC runs real logic. Test runner talks to Ignition only.
|
||||||
|
Never bypass Ignition to write directly to Modbus or PLC.
|
||||||
|
|
||||||
|
## Directory Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
project-root/
|
||||||
|
├── CLAUDE.md ← you are here
|
||||||
|
├── docker/
|
||||||
|
│ ├── CLAUDE.md ← Docker Compose patterns
|
||||||
|
│ ├── docker-compose.yml
|
||||||
|
│ ├── docker-compose.override.yml
|
||||||
|
│ └── config/
|
||||||
|
│ ├── ignition/
|
||||||
|
│ ├── postgres/
|
||||||
|
│ └── traefik/
|
||||||
|
├── ignition/
|
||||||
|
│ ├── CLAUDE.md ← Ignition project structure
|
||||||
|
│ └── project/
|
||||||
|
│ ├── com.inductiveautomation.perspective/
|
||||||
|
│ ├── com.inductiveautomation.webdev/
|
||||||
|
│ └── ignition/
|
||||||
|
│ ├── named-query/
|
||||||
|
│ ├── script-python/
|
||||||
|
│ └── global-props/
|
||||||
|
├── testing/
|
||||||
|
│ ├── CLAUDE.md ← test scenario schema & runner
|
||||||
|
│ ├── scenarios/
|
||||||
|
│ ├── runner/
|
||||||
|
│ └── results/
|
||||||
|
├── webdev/
|
||||||
|
│ ├── CLAUDE.md ← WebDev API contract
|
||||||
|
│ └── endpoints/
|
||||||
|
├── tools/
|
||||||
|
│ └── (Python 3.10+ external utilities)
|
||||||
|
└── docs/
|
||||||
|
└── (project documentation)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Module CLAUDE.md Files
|
||||||
|
|
||||||
|
Each subdirectory has its own CLAUDE.md with domain-specific rules:
|
||||||
|
- `docker/CLAUDE.md` — container orchestration, volumes, networking
|
||||||
|
- `ignition/CLAUDE.md` — project structure, UDTs, scripting, resources
|
||||||
|
- `testing/CLAUDE.md` — scenario schema, test runner, results format
|
||||||
|
- `webdev/CLAUDE.md` — API endpoint contracts, request/response formats
|
||||||
38
README.md
Normal file
38
README.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Ignition + Docker Development Framework
|
||||||
|
|
||||||
|
Reusable scaffold for Ignition SCADA + Docker projects, designed to work with Claude Code as the AI development assistant.
|
||||||
|
|
||||||
|
## First Consumer
|
||||||
|
|
||||||
|
**PLC I/O Testing Platform** — automated PLC I/O validation using JSON-defined test scenarios.
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- Ignition 8.3 (Perspective, WebDev, OPC-UA, Git-native project storage)
|
||||||
|
- Docker Compose on Ubuntu 24
|
||||||
|
- Rockwell Allen-Bradley ControlLogix/CompactLogix
|
||||||
|
- PostgreSQL 16
|
||||||
|
- Traefik v3 (optional reverse proxy)
|
||||||
|
- Gitea for source control
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
See [CLAUDE.md](CLAUDE.md) for project directives, canonical patterns, and directory layout.
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
├── CLAUDE.md ← root directives (Claude Code reads this automatically)
|
||||||
|
├── docker/ ← Docker Compose stack and configs
|
||||||
|
├── ignition/ ← Ignition project (mounted into container)
|
||||||
|
├── testing/ ← test scenarios, runner, results
|
||||||
|
├── webdev/ ← WebDev API endpoint contracts
|
||||||
|
├── tools/ ← Python 3.10+ external utilities
|
||||||
|
└── docs/ ← project documentation
|
||||||
|
```
|
||||||
|
|
||||||
|
Each subdirectory contains its own `CLAUDE.md` with domain-specific rules.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Internal use only.
|
||||||
149
docker/CLAUDE.md
Normal file
149
docker/CLAUDE.md
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
# Docker Compose Patterns — CLAUDE.md
|
||||||
|
|
||||||
|
Parent: [../CLAUDE.md](../CLAUDE.md)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
All services run via Docker Compose on Ubuntu 24.
|
||||||
|
The base `docker-compose.yml` defines the core stack.
|
||||||
|
Project-specific overrides go in `docker-compose.override.yml`.
|
||||||
|
|
||||||
|
## Core Services
|
||||||
|
|
||||||
|
### Ignition Gateway
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
ignition:
|
||||||
|
image: inductiveautomation/ignition:8.3.X
|
||||||
|
ports:
|
||||||
|
- "8088:8088" # HTTP gateway
|
||||||
|
- "8043:8043" # HTTPS gateway
|
||||||
|
- "62541:62541" # OPC-UA
|
||||||
|
volumes:
|
||||||
|
- ignition-data:/usr/local/bin/ignition/data
|
||||||
|
- ./config/ignition/gateway.xml:/usr/local/bin/ignition/data/gateway.xml
|
||||||
|
- ../ignition/project:/usr/local/bin/ignition/data/projects/framework
|
||||||
|
environment:
|
||||||
|
ACCEPT_IGNITION_EULA: "Y"
|
||||||
|
GATEWAY_ADMIN_PASSWORD: "${IGNITION_ADMIN_PASSWORD}"
|
||||||
|
IGNITION_EDITION: standard
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
### PostgreSQL
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
|
- ./config/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ignition
|
||||||
|
POSTGRES_USER: ignition
|
||||||
|
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
### Traefik (Reverse Proxy)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik:
|
||||||
|
image: traefik:v3
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
- "8080:8080" # Traefik dashboard
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- ./config/traefik:/etc/traefik
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
### Modbus Simulator (Template)
|
||||||
|
|
||||||
|
One container per simulated device or device group.
|
||||||
|
Use `docker-compose.override.yml` to add project-specific simulators.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
modbus-sim-pumps:
|
||||||
|
image: oitc/modbus-server:latest
|
||||||
|
ports:
|
||||||
|
- "5020:5020"
|
||||||
|
volumes:
|
||||||
|
- ./config/modbus/pumps.json:/app/config.json
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
### Volume Management
|
||||||
|
|
||||||
|
- **Named volumes** (`ignition-data`, `postgres-data`) persist across restarts.
|
||||||
|
- **Never run `docker compose down -v`** — this destroys named volumes.
|
||||||
|
- **Never run `docker compose down`** without explicit user confirmation.
|
||||||
|
- Use `docker compose stop` to halt services without removing containers.
|
||||||
|
- Use `docker compose restart <service>` for individual service restarts.
|
||||||
|
|
||||||
|
### Networking
|
||||||
|
|
||||||
|
- All services share the default compose network.
|
||||||
|
- Ignition connects to Modbus sims via container name (e.g., `modbus-sim-pumps:5020`).
|
||||||
|
- Ignition connects to PostgreSQL via `postgres:5432`.
|
||||||
|
- External access to WebDev API goes through Traefik or direct port mapping.
|
||||||
|
|
||||||
|
### Configuration Files
|
||||||
|
|
||||||
|
- Gateway backup/restore: mount `gateway.xml` for initial config only.
|
||||||
|
- Modbus configs: JSON files in `config/modbus/`, one per simulator.
|
||||||
|
- PostgreSQL init: `config/postgres/init.sql` runs on first start only.
|
||||||
|
|
||||||
|
### Adding a New Modbus Simulator
|
||||||
|
|
||||||
|
1. Create config file: `config/modbus/<device-group>.json`
|
||||||
|
2. Add service to `docker-compose.override.yml`
|
||||||
|
3. Assign unique port (start at 5020, increment by 1)
|
||||||
|
4. Add OPC-UA connection in Ignition config
|
||||||
|
5. Verify: `curl -s localhost:<port>` or test from Ignition
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
Store secrets in `.env` (gitignored):
|
||||||
|
|
||||||
|
```
|
||||||
|
IGNITION_ADMIN_PASSWORD=changeme
|
||||||
|
POSTGRES_PASSWORD=changeme
|
||||||
|
```
|
||||||
|
|
||||||
|
Never hardcode credentials in compose files or configs.
|
||||||
|
|
||||||
|
### Health Checks
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
ignition:
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8088/StatusPing"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
docker/
|
||||||
|
├── CLAUDE.md ← you are here
|
||||||
|
├── docker-compose.yml ← base stack (do not modify per-project)
|
||||||
|
├── docker-compose.override.yml ← project-specific additions
|
||||||
|
├── .env ← secrets (gitignored)
|
||||||
|
└── config/
|
||||||
|
├── ignition/
|
||||||
|
│ └── gateway.xml
|
||||||
|
├── postgres/
|
||||||
|
│ └── init.sql
|
||||||
|
├── traefik/
|
||||||
|
│ ├── traefik.yml
|
||||||
|
│ └── dynamic/
|
||||||
|
└── modbus/
|
||||||
|
└── (device-group configs)
|
||||||
|
```
|
||||||
188
ignition/CLAUDE.md
Normal file
188
ignition/CLAUDE.md
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
# Ignition Project Structure — CLAUDE.md
|
||||||
|
|
||||||
|
Parent: [../CLAUDE.md](../CLAUDE.md)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Ignition 8.3 with Git-native project storage. The project directory is
|
||||||
|
mounted into the Ignition Docker container and is the single source of truth.
|
||||||
|
All resources are JSON files on disk.
|
||||||
|
|
||||||
|
## Git-Native Project Storage
|
||||||
|
|
||||||
|
Ignition 8.3 stores projects as files and directories:
|
||||||
|
|
||||||
|
```
|
||||||
|
ignition/project/
|
||||||
|
├── project.json ← project metadata
|
||||||
|
├── com.inductiveautomation.perspective/
|
||||||
|
│ ├── views/
|
||||||
|
│ │ └── <view-name>/
|
||||||
|
│ │ ├── view.json ← view definition
|
||||||
|
│ │ └── resource.json ← resource metadata
|
||||||
|
│ └── page-config/
|
||||||
|
├── com.inductiveautomation.webdev/
|
||||||
|
│ └── <endpoint-name>/
|
||||||
|
│ ├── code.py ← Jython 2.7 handler
|
||||||
|
│ └── resource.json
|
||||||
|
└── ignition/
|
||||||
|
├── named-query/
|
||||||
|
│ └── <query-name>/
|
||||||
|
│ ├── query.sql
|
||||||
|
│ └── resource.json
|
||||||
|
├── script-python/
|
||||||
|
│ └── <package>/
|
||||||
|
│ ├── code.py ← project library scripts
|
||||||
|
│ └── resource.json
|
||||||
|
└── global-props/
|
||||||
|
└── props.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## JSON File Rules
|
||||||
|
|
||||||
|
All JSON files in the project directory:
|
||||||
|
|
||||||
|
1. **Must be valid JSON** — validate before writing (use `json.loads()` then write).
|
||||||
|
2. **Must use sorted keys** — `json.dumps(data, sort_keys=True, indent=2)`
|
||||||
|
3. **Must be backed up** before overwriting — `cp file.json file.json.bak`
|
||||||
|
4. **Must use 2-space indentation** — matches Ignition's native format.
|
||||||
|
|
||||||
|
### resource.json Template
|
||||||
|
|
||||||
|
Every resource directory contains a `resource.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"props": {
|
||||||
|
"resource.name": "<resource-name>"
|
||||||
|
},
|
||||||
|
"scope": "G",
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `scope`: `"G"` = gateway, `"C"` = client, `"A"` = all
|
||||||
|
- `version`: increment on changes for cache-busting
|
||||||
|
|
||||||
|
## UDT / Linking Pattern
|
||||||
|
|
||||||
|
### Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
[Device Type UDT]
|
||||||
|
├── Config/
|
||||||
|
│ ├── DeviceType (String)
|
||||||
|
│ ├── DeviceId (String)
|
||||||
|
│ └── Description (String)
|
||||||
|
├── Status/
|
||||||
|
│ ├── Running (Boolean)
|
||||||
|
│ ├── Faulted (Boolean)
|
||||||
|
│ └── ...
|
||||||
|
├── Command/
|
||||||
|
│ ├── Start (Boolean)
|
||||||
|
│ ├── Stop (Boolean)
|
||||||
|
│ └── ...
|
||||||
|
└── Linking/
|
||||||
|
└── Link/
|
||||||
|
├── LinkPath (String) ← path to PLC tag
|
||||||
|
└── LinkData (Dataset/Document) ← signal mapping
|
||||||
|
```
|
||||||
|
|
||||||
|
### How LinkPath Works
|
||||||
|
|
||||||
|
`LinkPath` contains the OPC tag path prefix for the PLC signals.
|
||||||
|
Example: `[ControlLogix]Program:MainProgram.P_101`
|
||||||
|
|
||||||
|
`LinkData` maps UDT members to PLC tag suffixes:
|
||||||
|
- `Status/Running` → `LinkPath + ".Sts_Running"`
|
||||||
|
- `Command/Start` → `LinkPath + ".Cmd_Start"`
|
||||||
|
|
||||||
|
This decouples the test/simulation layer from specific PLC I/O structures.
|
||||||
|
Changing the PLC program only requires updating `LinkPath` and `LinkData`.
|
||||||
|
|
||||||
|
### Rules
|
||||||
|
|
||||||
|
- Never modify `LinkPath` or `LinkData` at runtime from external tools.
|
||||||
|
- Set linking configuration through Ignition Designer or startup scripts.
|
||||||
|
- Test scenarios reference device UDT paths, not raw PLC tag paths.
|
||||||
|
|
||||||
|
## Scripting
|
||||||
|
|
||||||
|
### Project Library Scripts (`script-python/`)
|
||||||
|
|
||||||
|
Jython 2.7 — see root CLAUDE.md for language constraints.
|
||||||
|
|
||||||
|
Naming convention:
|
||||||
|
```
|
||||||
|
script-python/
|
||||||
|
├── testing/
|
||||||
|
│ ├── code.py ← testing.runScenario(), testing.validateResults()
|
||||||
|
│ └── resource.json
|
||||||
|
├── devices/
|
||||||
|
│ ├── code.py ← devices.getStatus(), devices.sendCommand()
|
||||||
|
│ └── resource.json
|
||||||
|
└── util/
|
||||||
|
├── code.py ← util.formatTag(), util.validateJson()
|
||||||
|
└── resource.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Access in Ignition: `project.testing.runScenario()`
|
||||||
|
|
||||||
|
### Gateway Event Scripts
|
||||||
|
|
||||||
|
Located in project config, not in `script-python/`.
|
||||||
|
Used for startup initialization, tag change events, scheduled tasks.
|
||||||
|
|
||||||
|
### Common Ignition API Calls
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Tag operations
|
||||||
|
system.tag.readBlocking(["path/to/tag"])
|
||||||
|
system.tag.writeBlocking(["path/to/tag"], [value])
|
||||||
|
system.tag.browseConfiguration("path", {})
|
||||||
|
|
||||||
|
# Named queries
|
||||||
|
system.db.runNamedQuery("queryName", {"param": value})
|
||||||
|
|
||||||
|
# WebDev (inside endpoint handlers)
|
||||||
|
# request object has: data, params, headers, remoteAddr
|
||||||
|
# return dict or string — Ignition serializes to JSON
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
logger = system.util.getLogger("testing")
|
||||||
|
logger.info("message")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tag Provider Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
[default]
|
||||||
|
├── _types_/ ← UDT definitions
|
||||||
|
│ ├── Devices/
|
||||||
|
│ │ ├── Motor/
|
||||||
|
│ │ ├── Valve/
|
||||||
|
│ │ └── Pump/
|
||||||
|
│ └── Linking/
|
||||||
|
│ └── Link/
|
||||||
|
├── Devices/ ← UDT instances
|
||||||
|
│ ├── Pumps/
|
||||||
|
│ │ ├── P-101/
|
||||||
|
│ │ └── P-102/
|
||||||
|
│ ├── Valves/
|
||||||
|
│ │ └── XV-1001/
|
||||||
|
│ └── Motors/
|
||||||
|
│ └── M-201/
|
||||||
|
└── System/ ← system/status tags
|
||||||
|
├── Health/
|
||||||
|
└── Config/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Perspective Views
|
||||||
|
|
||||||
|
Views are JSON definitions in `com.inductiveautomation.perspective/views/`.
|
||||||
|
Each view folder contains `view.json` (component tree) and `resource.json`.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Do not hand-edit `view.json` unless you understand the component schema.
|
||||||
|
- Prefer creating views through Ignition Designer when possible.
|
||||||
|
- If editing programmatically, validate the full JSON structure before writing.
|
||||||
272
testing/CLAUDE.md
Normal file
272
testing/CLAUDE.md
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
# Test Scenario Schema & Runner — CLAUDE.md
|
||||||
|
|
||||||
|
Parent: [../CLAUDE.md](../CLAUDE.md)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Automated PLC I/O validation through JSON-defined test scenarios.
|
||||||
|
The test runner is Python 3.10+ and communicates with Ignition exclusively
|
||||||
|
through the WebDev API. It never touches tags, files, or Modbus directly.
|
||||||
|
|
||||||
|
## Scenario Schema
|
||||||
|
|
||||||
|
Every scenario JSON file has exactly five top-level sections.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"identity": {
|
||||||
|
"id": "PMP-101-START-001",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Verify P-101 starts when start command is issued and permissives are met",
|
||||||
|
"author": "engineer@company.com",
|
||||||
|
"created": "2026-03-14",
|
||||||
|
"tags": ["pump", "start", "P-101", "plantpax"]
|
||||||
|
},
|
||||||
|
"device": {
|
||||||
|
"type": "Pump",
|
||||||
|
"udt_path": "[default]Devices/Pumps/P-101",
|
||||||
|
"plc_connection": "ControlLogix",
|
||||||
|
"related_devices": [
|
||||||
|
{
|
||||||
|
"type": "Valve",
|
||||||
|
"udt_path": "[default]Devices/Valves/XV-1001",
|
||||||
|
"role": "discharge_valve"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"initial_state": {
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"path": "[default]Devices/Pumps/P-101/Status/Running",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "[default]Devices/Pumps/P-101/Status/Faulted",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "[default]Devices/Pumps/P-101/Status/Ready",
|
||||||
|
"value": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "[default]Devices/Valves/XV-1001/Status/FullOpen",
|
||||||
|
"value": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sequence": [
|
||||||
|
{
|
||||||
|
"step": 1,
|
||||||
|
"action": "write",
|
||||||
|
"description": "Issue start command",
|
||||||
|
"path": "[default]Devices/Pumps/P-101/Command/Start",
|
||||||
|
"value": true,
|
||||||
|
"delay_ms": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": 2,
|
||||||
|
"action": "wait",
|
||||||
|
"description": "Allow PLC scan time for logic execution",
|
||||||
|
"delay_ms": 2000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": 3,
|
||||||
|
"action": "read",
|
||||||
|
"description": "Check running status",
|
||||||
|
"path": "[default]Devices/Pumps/P-101/Status/Running",
|
||||||
|
"delay_ms": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expected_outcomes": [
|
||||||
|
{
|
||||||
|
"path": "[default]Devices/Pumps/P-101/Status/Running",
|
||||||
|
"expected": true,
|
||||||
|
"tolerance": null,
|
||||||
|
"comparison": "equals",
|
||||||
|
"description": "Pump should be running after start command"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "[default]Devices/Pumps/P-101/Status/Faulted",
|
||||||
|
"expected": false,
|
||||||
|
"tolerance": null,
|
||||||
|
"comparison": "equals",
|
||||||
|
"description": "Pump should not be faulted"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "[default]Devices/Pumps/P-101/Status/Speed",
|
||||||
|
"expected": 60.0,
|
||||||
|
"tolerance": 2.0,
|
||||||
|
"comparison": "within_tolerance",
|
||||||
|
"description": "Pump speed should be near setpoint"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Section Details
|
||||||
|
|
||||||
|
#### identity
|
||||||
|
|
||||||
|
Required fields: `id`, `version`, `description`
|
||||||
|
Optional fields: `author`, `created`, `tags`
|
||||||
|
|
||||||
|
ID convention: `{DEVICE_PREFIX}-{NUMBER}-{ACTION}-{SEQUENCE}`
|
||||||
|
Example: `PMP-101-START-001`, `VLV-1001-OPEN-002`
|
||||||
|
|
||||||
|
Version follows semver. Bump on any change to sequence or expected outcomes.
|
||||||
|
|
||||||
|
#### device
|
||||||
|
|
||||||
|
Required fields: `type`, `udt_path`
|
||||||
|
Optional fields: `plc_connection`, `related_devices`
|
||||||
|
|
||||||
|
`udt_path` is the full Ignition tag path to the device UDT instance.
|
||||||
|
`related_devices` lists other devices involved in the test with their role.
|
||||||
|
|
||||||
|
#### initial_state
|
||||||
|
|
||||||
|
Array of `{path, value}` objects. These tags are written (via WebDev API)
|
||||||
|
before the test sequence begins. The runner writes all initial_state tags,
|
||||||
|
waits 500ms for propagation, then verifies they were set correctly.
|
||||||
|
|
||||||
|
#### sequence
|
||||||
|
|
||||||
|
Ordered array of steps. Each step has:
|
||||||
|
- `step` (integer) — execution order
|
||||||
|
- `action` — one of: `write`, `read`, `wait`, `assert`
|
||||||
|
- `description` — human-readable step description
|
||||||
|
- `path` — tag path (required for `write`, `read`, `assert`)
|
||||||
|
- `value` — value to write (required for `write`)
|
||||||
|
- `delay_ms` — milliseconds to wait before executing this step
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- `write` — write `value` to `path` via WebDev tagWrite
|
||||||
|
- `read` — read `path` via WebDev tagRead, store result for later assertion
|
||||||
|
- `wait` — pause execution for `delay_ms` (no tag interaction)
|
||||||
|
- `assert` — immediately check `path` against `value` (mid-sequence validation)
|
||||||
|
|
||||||
|
#### expected_outcomes
|
||||||
|
|
||||||
|
Array of final assertions evaluated after the sequence completes.
|
||||||
|
Each outcome has:
|
||||||
|
- `path` — tag to check
|
||||||
|
- `expected` — expected value
|
||||||
|
- `tolerance` — numeric tolerance (null for exact match)
|
||||||
|
- `comparison` — one of: `equals`, `within_tolerance`, `greater_than`, `less_than`, `not_equals`
|
||||||
|
- `description` — what this assertion validates
|
||||||
|
|
||||||
|
## Scenario File Rules
|
||||||
|
|
||||||
|
1. **One scenario per file.** Named: `{identity.id}.json`
|
||||||
|
2. **Validate JSON before writing.** Use sorted keys, 2-space indent.
|
||||||
|
3. **Store in `testing/scenarios/` directory.**
|
||||||
|
4. **Back up before overwriting:** `cp file.json file.json.bak`
|
||||||
|
|
||||||
|
## Test Runner
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
runner/
|
||||||
|
├── main.py ← CLI entry point (Python 3.10+)
|
||||||
|
├── executor.py ← scenario execution engine
|
||||||
|
├── api_client.py ← WebDev API client wrapper
|
||||||
|
├── validator.py ← scenario JSON schema validation
|
||||||
|
├── reporter.py ← results formatting and output
|
||||||
|
└── config.py ← runner configuration (gateway URL, timeouts)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Execution Flow
|
||||||
|
|
||||||
|
1. Load and validate scenario JSON against schema.
|
||||||
|
2. Connect to Ignition WebDev API (health check first).
|
||||||
|
3. Write `initial_state` tags → wait 500ms → verify writes.
|
||||||
|
4. Execute `sequence` steps in order, respecting `delay_ms`.
|
||||||
|
5. After sequence completes, evaluate `expected_outcomes`.
|
||||||
|
6. Generate results report (JSON + human-readable summary).
|
||||||
|
|
||||||
|
### API Client
|
||||||
|
|
||||||
|
The runner talks to Ignition through WebDev endpoints only:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# env: python3.10+ (external tooling — NOT Ignition)
|
||||||
|
|
||||||
|
class IgnitionAPIClient:
|
||||||
|
def __init__(self, base_url: str = "http://localhost:8088/system/webdev"):
|
||||||
|
self.base_url = base_url
|
||||||
|
|
||||||
|
def tag_write(self, path: str, value) -> dict:
|
||||||
|
"""POST /tagWrite {"path": path, "value": value}"""
|
||||||
|
|
||||||
|
def tag_read(self, path: str) -> dict:
|
||||||
|
"""GET /tagRead?path=<path>"""
|
||||||
|
|
||||||
|
def tag_browse(self, root: str = "") -> dict:
|
||||||
|
"""GET /tagBrowse?root=<root>"""
|
||||||
|
|
||||||
|
def health(self) -> dict:
|
||||||
|
"""GET /health"""
|
||||||
|
```
|
||||||
|
|
||||||
|
## Results Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scenario_id": "PMP-101-START-001",
|
||||||
|
"scenario_version": "1.0.0",
|
||||||
|
"timestamp": "2026-03-14T10:30:00Z",
|
||||||
|
"duration_ms": 4523,
|
||||||
|
"result": "PASS",
|
||||||
|
"initial_state_verification": {
|
||||||
|
"status": "OK",
|
||||||
|
"details": []
|
||||||
|
},
|
||||||
|
"sequence_log": [
|
||||||
|
{
|
||||||
|
"step": 1,
|
||||||
|
"action": "write",
|
||||||
|
"path": "[default]Devices/Pumps/P-101/Command/Start",
|
||||||
|
"value": true,
|
||||||
|
"response": {"status": "ok"},
|
||||||
|
"elapsed_ms": 45
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outcomes": [
|
||||||
|
{
|
||||||
|
"path": "[default]Devices/Pumps/P-101/Status/Running",
|
||||||
|
"expected": true,
|
||||||
|
"actual": true,
|
||||||
|
"result": "PASS",
|
||||||
|
"description": "Pump should be running after start command"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"summary": {
|
||||||
|
"total_outcomes": 3,
|
||||||
|
"passed": 3,
|
||||||
|
"failed": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Results are stored in `testing/results/` as `{scenario_id}_{timestamp}.json`.
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
testing/
|
||||||
|
├── CLAUDE.md ← you are here
|
||||||
|
├── scenarios/
|
||||||
|
│ ├── PMP-101-START-001.json
|
||||||
|
│ ├── PMP-101-STOP-001.json
|
||||||
|
│ └── VLV-1001-OPEN-001.json
|
||||||
|
├── runner/
|
||||||
|
│ ├── main.py
|
||||||
|
│ ├── executor.py
|
||||||
|
│ ├── api_client.py
|
||||||
|
│ ├── validator.py
|
||||||
|
│ ├── reporter.py
|
||||||
|
│ └── config.py
|
||||||
|
└── results/
|
||||||
|
└── (timestamped result files)
|
||||||
|
```
|
||||||
322
webdev/CLAUDE.md
Normal file
322
webdev/CLAUDE.md
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
# 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
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user