sylo-ignition 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +25 -0
- package/assets/write-allowlist.json +19 -0
- package/extensions/index.ts +349 -0
- package/package.json +36 -0
- package/references/README.md +45 -0
- package/references/gateway-rest-api-8.3.md +119 -0
- package/references/quickref/README.md +12 -0
- package/references/quickref/formats-quickref.md +94 -0
- package/references/quickref/gateway-rest-api-8.3.md +119 -0
- package/scripts/_allowlist.py +119 -0
- package/scripts/_ignition.py +181 -0
- package/scripts/_json_out.py +19 -0
- package/scripts/api_get.py +71 -0
- package/scripts/backup.py +49 -0
- package/scripts/fetch_docs.py +278 -0
- package/scripts/gateway_logs.py +57 -0
- package/scripts/project_create.py +64 -0
- package/scripts/project_resources.py +127 -0
- package/scripts/requirements.txt +3 -0
- package/scripts/resource_read.py +102 -0
- package/scripts/resource_write.py +171 -0
- package/scripts/scan.py +85 -0
- package/scripts/screenshot.py +96 -0
- package/scripts/status.py +91 -0
- package/scripts/validate.py +252 -0
- package/skills/ignition/SKILL.md +131 -0
- package/skills/ignition-reference/SKILL.md +157 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Offline lint/validate of a project resource before scanning.
|
|
3
|
+
|
|
4
|
+
For ignition_validate tool. Heuristic (no vendor SDK):
|
|
5
|
+
- view.json: structure, unique meta.name, known component types, binding shapes
|
|
6
|
+
- resource.json: required keys
|
|
7
|
+
- .py (project scripts): Jython 2.7 compatibility (f-strings etc. are errors)
|
|
8
|
+
- tag JSON: light structure check
|
|
9
|
+
Errors block the write; warnings are advisory. This tool never calls the gateway.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import json
|
|
16
|
+
import re
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from _ignition import load_config, project_dir, resolve_project
|
|
21
|
+
from _json_out import emit, emit_error
|
|
22
|
+
|
|
23
|
+
KNOWN_VIEW_TYPES = {
|
|
24
|
+
"ia.display.label",
|
|
25
|
+
"ia.display.led",
|
|
26
|
+
"ia.display.multistate-indicator",
|
|
27
|
+
"ia.display.table",
|
|
28
|
+
"ia.display.path",
|
|
29
|
+
"ia.shapes.svg",
|
|
30
|
+
"ia.input.btn",
|
|
31
|
+
"ia.input.dropdown",
|
|
32
|
+
"ia.input.numeric-entry",
|
|
33
|
+
"ia.input.text-field",
|
|
34
|
+
"ia.input.slider",
|
|
35
|
+
"ia.input.toggle-switch",
|
|
36
|
+
"ia.input.checkbox",
|
|
37
|
+
"ia.container.flex",
|
|
38
|
+
"ia.container.column",
|
|
39
|
+
"ia.container.row",
|
|
40
|
+
"ia.container.coord",
|
|
41
|
+
"ia.container.tabs",
|
|
42
|
+
"ia.container.accordian",
|
|
43
|
+
"ia.container.dropdown-menu",
|
|
44
|
+
"ia.container.menus",
|
|
45
|
+
"ia.container.modal",
|
|
46
|
+
"ia.container.popup",
|
|
47
|
+
"ia.container.split",
|
|
48
|
+
"ia.container.tab-container",
|
|
49
|
+
"ia.container.template",
|
|
50
|
+
"ia.chart.xy-chart",
|
|
51
|
+
"ia.powerchart.powerchart",
|
|
52
|
+
"ia.gauge.led-numeric-entry",
|
|
53
|
+
"ia.gauge.linear-gauge",
|
|
54
|
+
"ia.gauge.radial-gauge",
|
|
55
|
+
"ia.gauge.arc",
|
|
56
|
+
"ia.alarm.journal-table",
|
|
57
|
+
"ia.alarm.status-table",
|
|
58
|
+
"ia.logos.inductive-automation",
|
|
59
|
+
"ia.display.image",
|
|
60
|
+
"ia.display.markdown",
|
|
61
|
+
"ia.embedded.websocket",
|
|
62
|
+
"ia.embedded.video",
|
|
63
|
+
"ia.barcode.qrcode",
|
|
64
|
+
"ia.tagtags.tag-canvas",
|
|
65
|
+
"em.charts.chartjs",
|
|
66
|
+
}
|
|
67
|
+
BINDING_TYPES = {"property", "tag", "expr", "query", "tagHistory", "propertyBinding", "exprBinding", "tagBinding"}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def main() -> None:
|
|
71
|
+
parser = argparse.ArgumentParser()
|
|
72
|
+
parser.add_argument("--project", default="")
|
|
73
|
+
parser.add_argument("--path", required=True)
|
|
74
|
+
args = parser.parse_args()
|
|
75
|
+
|
|
76
|
+
cfg = load_config(require_token=False)
|
|
77
|
+
name = resolve_project(cfg, args.project)
|
|
78
|
+
root = project_dir(cfg, name)
|
|
79
|
+
target = (root / args.path.replace("\\", "/").strip("/")).resolve()
|
|
80
|
+
if not target.is_file():
|
|
81
|
+
emit_error(f"Not found: {args.path} (in {root})")
|
|
82
|
+
|
|
83
|
+
errors: list[str] = []
|
|
84
|
+
warnings: list[str] = []
|
|
85
|
+
checks: list[str] = []
|
|
86
|
+
|
|
87
|
+
raw = target.read_text(encoding="utf-8", errors="replace")
|
|
88
|
+
|
|
89
|
+
if target.name == "view.json":
|
|
90
|
+
validate_view(raw, errors, warnings, checks)
|
|
91
|
+
elif target.name == "resource.json":
|
|
92
|
+
validate_resource_json(raw, errors, warnings, checks)
|
|
93
|
+
elif target.suffix == ".py":
|
|
94
|
+
validate_jython(raw, errors, warnings, checks)
|
|
95
|
+
elif target.suffix == ".json":
|
|
96
|
+
checks.append("generic JSON parse")
|
|
97
|
+
try:
|
|
98
|
+
json.loads(raw)
|
|
99
|
+
except (json.JSONDecodeError, ValueError) as e:
|
|
100
|
+
errors.append(f"invalid JSON: {e}")
|
|
101
|
+
else:
|
|
102
|
+
checks.append("no validation rules for this file type — skipped")
|
|
103
|
+
|
|
104
|
+
emit(
|
|
105
|
+
{
|
|
106
|
+
"ok": len(errors) == 0,
|
|
107
|
+
"project": name,
|
|
108
|
+
"path": args.path,
|
|
109
|
+
"errors": errors,
|
|
110
|
+
"warnings": warnings,
|
|
111
|
+
"checks_run": checks,
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# ---- view.json ----------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
def validate_view(raw: str, errors: list[str], warnings: list[str], checks: list[str]) -> None:
|
|
119
|
+
checks.append("view.json: JSON parse")
|
|
120
|
+
try:
|
|
121
|
+
view = json.loads(raw)
|
|
122
|
+
except (json.JSONDecodeError, ValueError) as e:
|
|
123
|
+
errors.append(f"view.json is not valid JSON: {e}")
|
|
124
|
+
return
|
|
125
|
+
if not isinstance(view, dict):
|
|
126
|
+
errors.append("view.json root must be an object")
|
|
127
|
+
return
|
|
128
|
+
checks.append("view.json: root component (verified 8.3.9 shape)")
|
|
129
|
+
|
|
130
|
+
# VERIFIED structure (live 8.3.9): top-level "root" is the root COMPONENT
|
|
131
|
+
# (e.g. ia.container.coord) with children — NOT props.root.children.
|
|
132
|
+
# A malformed view is silently dropped by the scan.
|
|
133
|
+
root = view.get("root")
|
|
134
|
+
if not isinstance(root, dict):
|
|
135
|
+
errors.append(
|
|
136
|
+
'view.json must have a top-level "root" component object '
|
|
137
|
+
'(e.g. {"type": "ia.container.coord", "meta": {"name": "root"}, "children": [...]})'
|
|
138
|
+
)
|
|
139
|
+
return
|
|
140
|
+
if root.get("type") not in ("ia.container.coord", "ia.container.flex", "ia.container.column", "ia.container.row"):
|
|
141
|
+
errors.append(f'root.type should be a container type, got {root.get("type")!r}')
|
|
142
|
+
if not isinstance(root.get("children"), list):
|
|
143
|
+
warnings.append("root.children missing or not a list — empty view?")
|
|
144
|
+
|
|
145
|
+
names: list[str] = []
|
|
146
|
+
depth_violations: list[str] = []
|
|
147
|
+
count = 0
|
|
148
|
+
percent_mode_nodes: list[str] = []
|
|
149
|
+
|
|
150
|
+
def walk(node: Any, path: str, depth: int) -> None:
|
|
151
|
+
nonlocal count
|
|
152
|
+
if not isinstance(node, dict):
|
|
153
|
+
return
|
|
154
|
+
count += 1
|
|
155
|
+
ctype = node.get("type", "")
|
|
156
|
+
if isinstance(ctype, str) and ctype and ctype not in KNOWN_VIEW_TYPES and not ctype.startswith("ia."):
|
|
157
|
+
warnings.append(f"unknown component type {ctype!r} at {path}")
|
|
158
|
+
meta = node.get("meta", {})
|
|
159
|
+
nm = meta.get("name") if isinstance(meta, dict) else None
|
|
160
|
+
if nm:
|
|
161
|
+
if nm in names:
|
|
162
|
+
errors.append(f"duplicate meta.name {nm!r} at {path} — names must be unique")
|
|
163
|
+
names.append(nm)
|
|
164
|
+
pos = node.get("position")
|
|
165
|
+
if isinstance(pos, dict) and pos.get("mode") == "percent":
|
|
166
|
+
percent_mode_nodes.append(path)
|
|
167
|
+
if depth > 12:
|
|
168
|
+
depth_violations.append(path)
|
|
169
|
+
for child in node.get("children", []) or []:
|
|
170
|
+
walk(child, f"{path}/{nm or ctype}", depth + 1)
|
|
171
|
+
|
|
172
|
+
for child in root.get("children") or []:
|
|
173
|
+
walk(child, "<root>", 1)
|
|
174
|
+
checks.append(f"view.json: {count} components, {len(names)} named")
|
|
175
|
+
|
|
176
|
+
if depth_violations:
|
|
177
|
+
warnings.append(f"deep nesting (>12) at: {', '.join(depth_violations[:5])}")
|
|
178
|
+
if count > 300:
|
|
179
|
+
warnings.append(f"{count} components — very heavy view; consider templates/splitting")
|
|
180
|
+
if percent_mode_nodes:
|
|
181
|
+
warnings.append(
|
|
182
|
+
"position.mode 'percent' observed but UNVERIFIED on 8.3.9 (labels stacked at 0,0 in "
|
|
183
|
+
"live testing — use absolute px inside coord containers until verified)"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Binding shape check: any {binding: {...}} must have a known type
|
|
187
|
+
bind_re = re.compile(r'"binding"\s*:\s*\{')
|
|
188
|
+
if bind_re.search(raw):
|
|
189
|
+
checks.append("view.json: binding objects present — verifying types")
|
|
190
|
+
bad = find_bad_bindings(view)
|
|
191
|
+
for b in bad:
|
|
192
|
+
errors.append(f"binding without a known 'type' at {b}")
|
|
193
|
+
|
|
194
|
+
# Perspective expression binding syntax quick check
|
|
195
|
+
for m in re.finditer(r"\{([^{}\n]{1,200})\}", raw):
|
|
196
|
+
expr = m.group(1)
|
|
197
|
+
if re.search(r"\bf\"|f'", expr):
|
|
198
|
+
warnings.append(f"possible f-string in expression {expr[:60]!r} — Perspective expressions are not Python")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def find_bad_bindings(node: Any, path: str = "") -> list[str]:
|
|
202
|
+
bad: list[str] = []
|
|
203
|
+
if isinstance(node, dict):
|
|
204
|
+
if set(node.keys()) == {"binding"} or "binding" in node:
|
|
205
|
+
b = node.get("binding")
|
|
206
|
+
if isinstance(b, dict) and b.get("type") not in BINDING_TYPES:
|
|
207
|
+
bad.append(path or "<root>")
|
|
208
|
+
for k, v in node.items():
|
|
209
|
+
bad.extend(find_bad_bindings(v, f"{path}/{k}"))
|
|
210
|
+
elif isinstance(node, list):
|
|
211
|
+
for i, v in enumerate(node):
|
|
212
|
+
bad.extend(find_bad_bindings(v, f"{path}[{i}]"))
|
|
213
|
+
return bad
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# ---- resource.json ------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
def validate_resource_json(raw: str, errors: list[str], warnings: list[str], checks: list[str]) -> None:
|
|
219
|
+
checks.append("resource.json: keys")
|
|
220
|
+
try:
|
|
221
|
+
meta = json.loads(raw)
|
|
222
|
+
except (json.JSONDecodeError, ValueError) as e:
|
|
223
|
+
errors.append(f"resource.json is not valid JSON: {e}")
|
|
224
|
+
return
|
|
225
|
+
for key in ("scope", "version", "files"):
|
|
226
|
+
if key not in meta:
|
|
227
|
+
errors.append(f"resource.json missing key: {key}")
|
|
228
|
+
if meta.get("scope") != "G":
|
|
229
|
+
errors.append("resource.json scope must be 'G'")
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
# ---- Jython 2.7 ---------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
def validate_jython(raw: str, errors: list[str], warnings: list[str], checks: list[str]) -> None:
|
|
235
|
+
checks.append("python: Jython 2.7 compatibility")
|
|
236
|
+
for i, line in enumerate(raw.splitlines(), 1):
|
|
237
|
+
if re.search(r"\bf['\"]", line):
|
|
238
|
+
errors.append(f"line {i}: f-string — Jython 2.7 has no f-strings; use .format() or %")
|
|
239
|
+
if re.search(r"\basync\s+def\b|\bawait\b", line):
|
|
240
|
+
errors.append(f"line {i}: async/await — not available in Jython 2.7")
|
|
241
|
+
if ":=" in line and not line.strip().startswith("#"):
|
|
242
|
+
warnings.append(f"line {i}: walrus ':=' — not available in Jython 2.7")
|
|
243
|
+
if re.match(r"\s*print\s+[^(\s]", line) and "#" not in line.split("print")[0]:
|
|
244
|
+
warnings.append(f"line {i}: print statement without parens (works in 2.7, not Python 3)")
|
|
245
|
+
if re.search(r"\bmatch\s+\w+.*:$", line.strip()) and "case" in raw:
|
|
246
|
+
warnings.append(f"line {i}: 'match' statement — Jython 2.7 has no structural pattern matching")
|
|
247
|
+
if re.search(r"^\s*import\s+(dataclasses|typing\.extensions|pathlib)\b", raw, re.M):
|
|
248
|
+
warnings.append("import of a Python-3-only stdlib module (dataclasses/typing.extensions/pathlib) — unavailable in Jython 2.7")
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
if __name__ == "__main__":
|
|
252
|
+
main()
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ignition
|
|
3
|
+
description: Create and edit Ignition 8.3 projects — file-based resource authoring on the gateway's data dir, REST scan to hot-apply, screenshot-verified Perspective UI. Write-allowlist gated; 8.1 is NOT supported.
|
|
4
|
+
metadata:
|
|
5
|
+
sylo:
|
|
6
|
+
category: automation
|
|
7
|
+
icon: cpu
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Ignition — file-based 8.3 workflow
|
|
11
|
+
|
|
12
|
+
Sylo edits Ignition **8.3** project resources directly on disk (`data/projects/<name>/...`),
|
|
13
|
+
then triggers a gateway **scan** to hot-apply them — no Designer import step, no zip. All
|
|
14
|
+
mutating tools enforce the operator-managed **write-allowlist**. This targets **8.3 only**:
|
|
15
|
+
8.1 keeps projects + config in an internal SQLite DB with no REST API — see the reference
|
|
16
|
+
skill's version notes.
|
|
17
|
+
|
|
18
|
+
## First-run setup (operator, one time)
|
|
19
|
+
|
|
20
|
+
1. **API key**: gateway web UI → Platform → Security → API Keys → Create API Key +
|
|
21
|
+
(Basic Token). Uncheck "Require secure connections" for a plain-HTTP local gateway.
|
|
22
|
+
Copy the token.
|
|
23
|
+
2. **Security level**: Platform → Security → **Levels** → select `Authenticated` →
|
|
24
|
+
Add Level + → name it `SyloAPI` → Save. Then edit the API key and check `SyloAPI`.
|
|
25
|
+
(Role-derived levels like Administrator are greyed out for API keys — custom levels are not.)
|
|
26
|
+
3. **Grant access**: Platform → Security → General Settings → **Roles and Permissions** →
|
|
27
|
+
check `SyloAPI` for Gateway Read AND Gateway Write. Save.
|
|
28
|
+
4. **Config file** at `C:\Users\<user>\.ignition-sylo\config.json` (OUTSIDE any repo —
|
|
29
|
+
tokens never go in git):
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"gateway_url": "http://localhost:8088",
|
|
33
|
+
"api_token": "Sylo:...",
|
|
34
|
+
"data_dir": "C:\\Program Files\\Inductive Automation\\Ignition\\data",
|
|
35
|
+
"default_project": "SyloSandbox"
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
5. **File-write permission** (native Windows install under Program Files — one time,
|
|
39
|
+
elevated PowerShell; the Users group only has read on the data dir by default):
|
|
40
|
+
```powershell
|
|
41
|
+
icacls "C:\Program Files\Inductive Automation\Ignition\data\projects" /grant "<user>:(OI)(CI)M"
|
|
42
|
+
icacls "C:\Program Files\Inductive Automation\Ignition\data\config" /grant "<user>:(OI)(CI)M"
|
|
43
|
+
```
|
|
44
|
+
6. `ignition_status` should now show `gateway_reachable: true` and the project list.
|
|
45
|
+
|
|
46
|
+
## Core loop
|
|
47
|
+
|
|
48
|
+
1. **`ignition_status`** — always first. Note open Designer sessions (conflict risk),
|
|
49
|
+
which projects exist, and what the allowlist permits.
|
|
50
|
+
2. **`ignition_project_resources`** — see the resource tree of the project you'll touch.
|
|
51
|
+
3. **`ignition_resource_read`** — study existing views before authoring (match the
|
|
52
|
+
house style; the Example project's `mainView` is a good reference).
|
|
53
|
+
4. Author/edit → **`ignition_validate`** (catch structure + Jython-2.7 errors offline).
|
|
54
|
+
5. **`ignition_resource_write`** — allowlist-gated atomic write. New folders get a
|
|
55
|
+
scan-compatible `resource.json` scaffold automatically.
|
|
56
|
+
6. **`ignition_scan`** (scope projects) — hot-apply into the gateway + Designer.
|
|
57
|
+
7. **`ignition_screenshot`** → `analyze_image` — visual verification / design critique loop.
|
|
58
|
+
8. If something didn't apply: **`ignition_gateway_logs`** (search "scan" / "resource").
|
|
59
|
+
|
|
60
|
+
Use **`ignition_project_create`** for a scratch project, and **`ignition_backup`**
|
|
61
|
+
before risky or bulk changes (restorable .gwbk in `~/.ignition-sylo/backups/`).
|
|
62
|
+
**`ignition_api_get`** is the read-only passthrough for everything else on the
|
|
63
|
+
588-route API (resource lists, sessions, tag export JSON, gateway info...).
|
|
64
|
+
|
|
65
|
+
## Gates (hard rules)
|
|
66
|
+
|
|
67
|
+
| Tool | Gate |
|
|
68
|
+
|------|------|
|
|
69
|
+
| `ignition_resource_write` | project must be present + **enabled** in `assets/write-allowlist.json` |
|
|
70
|
+
| `ignition_scan` | `allow_scan` must be true |
|
|
71
|
+
| `ignition_project_create` | `allow_project_create` must be true (creates only NEW isolated projects) |
|
|
72
|
+
| write-allowlist itself | **never edit it as the agent** — operator-owned; ask the operator |
|
|
73
|
+
|
|
74
|
+
If a gate blocks you, explain to the operator exactly what to change and stop.
|
|
75
|
+
Default scratch project: **SyloSandbox** (pre-enabled). Never write `Example`
|
|
76
|
+
(read-only by default) or any production project the operator hasn't listed.
|
|
77
|
+
|
|
78
|
+
## Forbidden paths (the write tool also refuses these)
|
|
79
|
+
|
|
80
|
+
- `**/.resources/`, `*.digest.json` — gateway internals
|
|
81
|
+
- `data/var/**`, `config/local/**`, `*.idb` — runtime state / local overrides
|
|
82
|
+
- `*.bin`, thumbnails, images — binary payloads produced by Designer/gateway
|
|
83
|
+
- Existing `resource.json` — the gateway owns it (signatures/timestamps);
|
|
84
|
+
new-resource scaffolds are created automatically by the write tool
|
|
85
|
+
- Gateway `data/config/**` — out of scope for project writes (Phase 3)
|
|
86
|
+
|
|
87
|
+
## Designer conflict rule
|
|
88
|
+
|
|
89
|
+
A scan while the Designer has **unsaved edits** can clobber them. Before
|
|
90
|
+
`ignition_scan`, check `ignition_status`. If Designer sessions are open, ask the
|
|
91
|
+
operator to save/close (or confirm proceed). Never scan silently over an open
|
|
92
|
+
Designer.
|
|
93
|
+
|
|
94
|
+
## Gotchas
|
|
95
|
+
|
|
96
|
+
- **Views are reachable only via pages**: a new project has no page-config, so sessions
|
|
97
|
+
show "No view configured for this page". Write
|
|
98
|
+
`com.inductiveautomation.perspective/page-config/config.json` mapping `"/"` to your
|
|
99
|
+
view path (schema in the reference skill), then scan. The URL after the project name
|
|
100
|
+
is the PAGE path, not the view path.
|
|
101
|
+
- **The scan silently drops malformed resources** — a bad view.json produces no error,
|
|
102
|
+
just an absent view. ALWAYS screenshot-verify after scanning; if the view is missing,
|
|
103
|
+
check the structure against the verified schema (top-level `root` component — see
|
|
104
|
+
reference skill) and `ignition_gateway_logs`.
|
|
105
|
+
- **Position modes**: `flow` inside flex containers is verified working; **`percent`
|
|
106
|
+
inside coord containers did NOT apply in live testing** (labels stacked at 0,0) — use
|
|
107
|
+
absolute pixel x/y/width/height inside coord containers.
|
|
108
|
+
- **Jython 2.7**: project gateway scripts are Python 2 — no f-strings, no
|
|
109
|
+
async/await, no pathlib. `ignition_validate` catches these; write `.format()` code.
|
|
110
|
+
- **Windows paths**: resource paths use forward slashes in tool calls; the
|
|
111
|
+
scripts handle OS joins. Keep view folder names short (255-char path limit).
|
|
112
|
+
- **Program Files ACL**: native installs need the one-time icacls grant (setup step 5)
|
|
113
|
+
— otherwise writes fail with Access denied.
|
|
114
|
+
- **Encoding**: scripts run with UTF-8 forced; write tool always writes UTF-8
|
|
115
|
+
with `\n` endings.
|
|
116
|
+
- **Scans are global**: `scan/projects` scans ALL projects, not just yours —
|
|
117
|
+
another reason the Designer rule matters.
|
|
118
|
+
- **No tag VALUES via REST**: 8.3 REST manages tag *definitions* (tags export/import
|
|
119
|
+
JSON); live values go through scripting/OPC-UA — currently out of scope.
|
|
120
|
+
- The full offline 8.3 User Manual (1,566 pages) + SDK docs live in the Sylo repo at
|
|
121
|
+
`packages/sylo-ignition/references/` — grep them via the **ignition-reference** skill
|
|
122
|
+
before guessing formats.
|
|
123
|
+
|
|
124
|
+
## Design quality (operator standard: beautiful, intuitive, clean)
|
|
125
|
+
|
|
126
|
+
When building views, aim for real design quality, not just function: consistent
|
|
127
|
+
spacing rhythm, meaningful hierarchy (page title → section → content), color used
|
|
128
|
+
sparingly for state (not decoration), labels that read like the machine ("Conveyor
|
|
129
|
+
Speed" not "tag1"), large readable values for at-a-glance operation. Always
|
|
130
|
+
screenshot and critique against these rules before reporting done; iterate at
|
|
131
|
+
least once when something feels off.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ignition-reference
|
|
3
|
+
description: Ignition 8.3 offline documentation map + format quickrefs — Perspective view JSON, bindings, component catalog, tag JSON, theme files, gateway REST routes, Jython 2.7. Grep the local manual before guessing.
|
|
4
|
+
metadata:
|
|
5
|
+
sylo:
|
|
6
|
+
category: automation
|
|
7
|
+
icon: cpu
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Ignition reference — offline docs + format quickrefs
|
|
11
|
+
|
|
12
|
+
## Offline documentation (grep here first)
|
|
13
|
+
|
|
14
|
+
Full 8.3 User Manual as grep-friendly Markdown (1,566 pages) + SDK guide live in the
|
|
15
|
+
Sylo repo:
|
|
16
|
+
|
|
17
|
+
- `packages/sylo-ignition/references/user-manual-8.3/` — the whole manual, Docusaurus
|
|
18
|
+
pages converted to `.md` (pipe/grid tables, fenced code, internal links rewritten
|
|
19
|
+
to relative `.md`). `references/README.md` lists key pages.
|
|
20
|
+
- `packages/sylo-ignition/references/sdk-docs/` — SDK Programmer's Guide (resource
|
|
21
|
+
collections, module model).
|
|
22
|
+
- `packages/sylo-ignition/references/user-manual-8.1-pdfs/` — official 8.1 PDFs
|
|
23
|
+
(git-ignored, 262 MB; 8.1 has NO REST API / file config — only zip + gwbk workflows).
|
|
24
|
+
- `packages/sylo-ignition/references/gateway-rest-api-8.3.md` — verified route map of
|
|
25
|
+
the 588-route REST API + the working API-key auth recipe.
|
|
26
|
+
- Regenerate everything: `python packages/sylo-ignition/scripts/fetch_docs.py`.
|
|
27
|
+
|
|
28
|
+
Key manual pages (relative to `user-manual-8.3/`):
|
|
29
|
+
|
|
30
|
+
- Perspective components: `appendix/components/perspective-components/` (every
|
|
31
|
+
component: props, events, style tips)
|
|
32
|
+
- Perspective props/bindings: `perspective/props/` and `perspective/bindings/`
|
|
33
|
+
- Expressions: `appendix/expression-functions/` ( Perspective expression language)
|
|
34
|
+
- Scripting (Jython 2.7): `appendix/scripting-functions/` + `perspective/scripting/`
|
|
35
|
+
- Tags: `platform/tags/` (tag properties, UDTs, tag file JSON)
|
|
36
|
+
- Gateway folder structure: `appendix/reference-pages/gateway-folder-structure.md`
|
|
37
|
+
- Version control / file-based config: `tutorials/version-control-guide.md`
|
|
38
|
+
- Security: `platform/security/` (API keys, security levels, zones)
|
|
39
|
+
|
|
40
|
+
## On-disk layout (8.3, verified on a live gateway)
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
data/
|
|
44
|
+
├── projects/<Name>/
|
|
45
|
+
│ ├── project.json # {title, description, enabled, inheritable, parent}
|
|
46
|
+
│ └── com.inductiveautomation.perspective/
|
|
47
|
+
│ ├── views/<viewPath>/ # view.json + thumbnail.png + resource.json
|
|
48
|
+
│ ├── page-config/config.json/
|
|
49
|
+
│ ├── page-startup/onPageStartup.py/
|
|
50
|
+
│ └── session-props/props.json/
|
|
51
|
+
└── config/ # gateway config (JSON, scan via /scan/config)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`resource.json` (per folder, gateway-owned — never hand-edit existing):
|
|
55
|
+
`{scope: "G", version: 1, restricted, overridable, files: [...], attributes:
|
|
56
|
+
{lastModificationSignature, lastModification: {actor, timestamp}}}`
|
|
57
|
+
|
|
58
|
+
## Perspective view.json quickref (VERIFIED live on 8.3.9)
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"custom": {},
|
|
63
|
+
"params": {},
|
|
64
|
+
"props": {},
|
|
65
|
+
"root": {
|
|
66
|
+
"type": "ia.container.coord",
|
|
67
|
+
"meta": { "name": "root" },
|
|
68
|
+
"children": [
|
|
69
|
+
{
|
|
70
|
+
"type": "ia.display.label",
|
|
71
|
+
"meta": { "name": "titleLabel" },
|
|
72
|
+
"position": { "x": 24, "y": 24, "width": 400, "height": 40 },
|
|
73
|
+
"props": {
|
|
74
|
+
"text": "Machine Overview",
|
|
75
|
+
"style": { "fontSize": "24px", "fontWeight": "bold" }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
- **`root` is a top-level key holding the root COMPONENT** (usually
|
|
84
|
+
`ia.container.coord`) — NOT `props.root.children`. Malformed views are
|
|
85
|
+
silently dropped by the scan.
|
|
86
|
+
- Component `type` comes from the catalog (`ia.<family>.<component>`); full prop
|
|
87
|
+
details in `appendix/components/perspective-components/`.
|
|
88
|
+
- `meta.name` must be **unique** across the view.
|
|
89
|
+
- Position: absolute px (`x`,`y`,`width`,`height`) inside coord containers — VERIFIED.
|
|
90
|
+
`mode: "flow"` for children of flex containers — VERIFIED. `mode: "percent"` —
|
|
91
|
+
did NOT apply in live testing (unverified; avoid for now).
|
|
92
|
+
- Style lives under `props.style` (CSS-ish: fontSize, backgroundColor, padding,
|
|
93
|
+
width, height); shared looks go in **style classes** (manual: `perspective/style-classes/`),
|
|
94
|
+
referenced via `props.style.classes`.
|
|
95
|
+
|
|
96
|
+
## Page-config quickref (VERIFIED live on 8.3.9)
|
|
97
|
+
|
|
98
|
+
`com.inductiveautomation.perspective/page-config/config.json` — without it,
|
|
99
|
+
sessions show "No view configured for this page":
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"pages": {
|
|
104
|
+
"/": { "viewPath": "Home" },
|
|
105
|
+
"/alarms": { "viewPath": "Alarms/Console" }
|
|
106
|
+
},
|
|
107
|
+
"sharedDocks": { "cornerPriority": "top-bottom" }
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
URL `…/data/perspective/client/<project>/<page>` maps `page` to the pages key above
|
|
112
|
+
(root page = `/`).
|
|
113
|
+
|
|
114
|
+
## Bindings quickref
|
|
115
|
+
|
|
116
|
+
Property bindings wrap the value:
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
"props": { "text": { "binding": { "type": "property", "config": { "path": "view.params.title" } } } }
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
- `type: "property"` → `config.path` (`view.x`, `self.x`, `session.props.x`)
|
|
123
|
+
- `type: "tag"` → `config.path: "[default]Line1/Speed"`, optional `config.op` e.g.
|
|
124
|
+
`readBlocking`? (see manual `perspective/bindings/tag-binding.md` for transforms/format)
|
|
125
|
+
- `type: "expr"` → `config.expression` — Perspective expression language
|
|
126
|
+
(`{view.params.speed} * 60`, `coalesce()`, `if()`, `toFix()`)
|
|
127
|
+
- Transforms: `config.transforms: [{"type": "map", "config": {...}}]` etc.
|
|
128
|
+
|
|
129
|
+
## Tag file JSON quickref
|
|
130
|
+
|
|
131
|
+
Tags (definitions) live in the provider's config or can be round-tripped as JSON via
|
|
132
|
+
`GET /data/api/v1/tags/export` / `POST /data/api/v1/tags/import`. UDT definitions,
|
|
133
|
+
value types (`Int4`, `Float8`, `Boolean`, `String8`), alarm definitions — see
|
|
134
|
+
`platform/tags/tag-properties.md` and `platform/tags/udts/`. REST manages
|
|
135
|
+
definitions; live **values** need scripting/OPC-UA.
|
|
136
|
+
|
|
137
|
+
## Jython 2.7 quickref (gateway scripts)
|
|
138
|
+
|
|
139
|
+
Python 2 syntax: `print x` ok, prefer `print(x)`; string formatting `%` or
|
|
140
|
+
`.format()`; no f-strings, no async, no pathlib/dataclasses/match. `system.*`
|
|
141
|
+
scripting functions in `appendix/scripting-functions/` (e.g.
|
|
142
|
+
`system.tag.readBlocking`, `system.perspective.sendMessage`).
|
|
143
|
+
|
|
144
|
+
## Gateway REST quickref
|
|
145
|
+
|
|
146
|
+
- Auth: header `X-Ignition-API-Token`; 401 = dead token, 403 = insufficient
|
|
147
|
+
security level (see gateway-rest-api-8.3.md for the working SyloAPI recipe)
|
|
148
|
+
- Projects: `/data/api/v1/projects/{list,names,find/{n},copy,rename/{n},export/{n},import/{n}}`
|
|
149
|
+
- Scan: `POST /data/api/v1/scan/{projects,config}`; lock:
|
|
150
|
+
`POST /data/api/v1/scan-lock/projects`
|
|
151
|
+
- Resources (gateway config types incl. tag-provider, opcua device, perspective
|
|
152
|
+
themes): `/data/api/v1/resources/{list,names,find,singleton}/{moduleId}/{typeId}`
|
|
153
|
+
- Tags: `/data/api/v1/tags/{export,import}` (JSON definitions)
|
|
154
|
+
- Perspective: `/data/perspective/api/v1/sessions/`, `.../themes/copy-base-themes`
|
|
155
|
+
- Status/diagnostics: `/data/api/v1/{gateway-info,overview,overview/problems,logs}`,
|
|
156
|
+
`GET /data/api/v1/backup` (.gwbk), `/data/api/v1/designers` (open sessions)
|
|
157
|
+
- Full spec: `GET /openapi.json` (12 MB — save to disk, don't inline)
|