iocmng 2.0.0__tar.gz
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.
- iocmng-2.0.0/PKG-INFO +378 -0
- iocmng-2.0.0/README.md +338 -0
- iocmng-2.0.0/pyproject.toml +77 -0
- iocmng-2.0.0/setup.cfg +4 -0
- iocmng-2.0.0/src/iocmng/__init__.py +13 -0
- iocmng-2.0.0/src/iocmng/api/__init__.py +3 -0
- iocmng-2.0.0/src/iocmng/api/app.py +94 -0
- iocmng-2.0.0/src/iocmng/api/models.py +68 -0
- iocmng-2.0.0/src/iocmng/api/routes.py +198 -0
- iocmng-2.0.0/src/iocmng/base/__init__.py +4 -0
- iocmng-2.0.0/src/iocmng/base/job.py +254 -0
- iocmng-2.0.0/src/iocmng/base/task.py +364 -0
- iocmng-2.0.0/src/iocmng/core/__init__.py +5 -0
- iocmng-2.0.0/src/iocmng/core/controller.py +270 -0
- iocmng-2.0.0/src/iocmng/core/loader.py +237 -0
- iocmng-2.0.0/src/iocmng/core/validator.py +196 -0
- iocmng-2.0.0/src/iocmng/ophyd/__init__.py +5 -0
- iocmng-2.0.0/src/iocmng/ophyd/factory.py +97 -0
- iocmng-2.0.0/src/iocmng.egg-info/PKG-INFO +378 -0
- iocmng-2.0.0/src/iocmng.egg-info/SOURCES.txt +26 -0
- iocmng-2.0.0/src/iocmng.egg-info/dependency_links.txt +1 -0
- iocmng-2.0.0/src/iocmng.egg-info/entry_points.txt +2 -0
- iocmng-2.0.0/src/iocmng.egg-info/requires.txt +25 -0
- iocmng-2.0.0/src/iocmng.egg-info/top_level.txt +1 -0
- iocmng-2.0.0/tests/test_api.py +71 -0
- iocmng-2.0.0/tests/test_base.py +115 -0
- iocmng-2.0.0/tests/test_loader.py +109 -0
- iocmng-2.0.0/tests/test_validator.py +109 -0
iocmng-2.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: iocmng
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: A pluggable task/job framework for IOC Manager applications with REST API
|
|
5
|
+
Author: INFN Beamline Controls
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/infn-epics/epik8s-beamline-controller
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: fastapi>=0.100.0
|
|
20
|
+
Requires-Dist: uvicorn[standard]>=0.20.0
|
|
21
|
+
Requires-Dist: pyyaml>=6.0
|
|
22
|
+
Requires-Dist: pydantic>=2.0
|
|
23
|
+
Requires-Dist: softioc
|
|
24
|
+
Requires-Dist: cothread
|
|
25
|
+
Provides-Extra: ophyd
|
|
26
|
+
Requires-Dist: ophyd; extra == "ophyd"
|
|
27
|
+
Requires-Dist: infn_ophyd_hal; extra == "ophyd"
|
|
28
|
+
Provides-Extra: kubernetes
|
|
29
|
+
Requires-Dist: kubernetes; extra == "kubernetes"
|
|
30
|
+
Provides-Extra: all
|
|
31
|
+
Requires-Dist: iocmng[kubernetes,ophyd]; extra == "all"
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
35
|
+
Requires-Dist: httpx; extra == "dev"
|
|
36
|
+
Requires-Dist: black; extra == "dev"
|
|
37
|
+
Requires-Dist: flake8; extra == "dev"
|
|
38
|
+
Requires-Dist: build; extra == "dev"
|
|
39
|
+
Requires-Dist: twine; extra == "dev"
|
|
40
|
+
|
|
41
|
+
# iocmng — IOC Manager Framework
|
|
42
|
+
|
|
43
|
+
A pluggable task/job framework for IOC Manager applications. Provides base classes for continuous **tasks** and one-shot **jobs** that can be dynamically loaded at runtime via a REST API.
|
|
44
|
+
|
|
45
|
+
## Features
|
|
46
|
+
|
|
47
|
+
- **`TaskBase`** — base class for continuous tasks (run in a loop)
|
|
48
|
+
- **`JobBase`** — base class for one-shot jobs (run once, return result)
|
|
49
|
+
- **REST API** — add/remove tasks and jobs at runtime from git repositories
|
|
50
|
+
- **Validation** — plugins are validated (must derive from base class, must compile, abstract methods must be implemented)
|
|
51
|
+
- **EPICS soft IOC PVs** — every task and job gets default PVs (STATUS, MESSAGE, etc.) via `softioc`
|
|
52
|
+
- **Per-plugin `config.yaml`** — each plugin defines its PVs and parameters in a config file inside its git repo
|
|
53
|
+
- **Path support** — specify a sub-directory inside the git repo where the plugin sources live
|
|
54
|
+
- **Plugin `requirements.txt`** — plugins can ship their own dependencies
|
|
55
|
+
- **Optional Ophyd integration** — device abstraction via `ophyd`/`infn_ophyd_hal` (optional dependency)
|
|
56
|
+
- **Docker image** — ready-to-run container with the REST API
|
|
57
|
+
- **PyPI package** — `pip install iocmng`
|
|
58
|
+
|
|
59
|
+
## Quick Start
|
|
60
|
+
|
|
61
|
+
### Install from PyPI
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install iocmng
|
|
65
|
+
|
|
66
|
+
# With all optional dependencies (ophyd, kubernetes)
|
|
67
|
+
pip install iocmng[all]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Run the API Server
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Using the CLI entry point
|
|
74
|
+
iocmng-server
|
|
75
|
+
|
|
76
|
+
# Or with environment variables
|
|
77
|
+
IOCMNG_PORT=8080 IOCMNG_LOG_LEVEL=debug iocmng-server
|
|
78
|
+
|
|
79
|
+
# Or with Docker
|
|
80
|
+
docker run -p 8080:8080 ghcr.io/infn-epics/epik8s-beamline-controller:latest
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Create a Task
|
|
84
|
+
|
|
85
|
+
Create a git repository with:
|
|
86
|
+
1. A Python file with a class deriving from `TaskBase`
|
|
87
|
+
2. A `config.yaml` defining PVs and parameters
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
my-monitor-repo/
|
|
91
|
+
├── my_monitor.py
|
|
92
|
+
├── config.yaml
|
|
93
|
+
└── requirements.txt # optional — extra dependencies
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**my_monitor.py**
|
|
97
|
+
```python
|
|
98
|
+
from iocmng import TaskBase
|
|
99
|
+
|
|
100
|
+
class MyMonitor(TaskBase):
|
|
101
|
+
def initialize(self):
|
|
102
|
+
self.logger.info("Starting monitor")
|
|
103
|
+
|
|
104
|
+
def execute(self):
|
|
105
|
+
value = self.read_sensor()
|
|
106
|
+
self.set_pv("READING", value)
|
|
107
|
+
if value > self.parameters.get("threshold", 75):
|
|
108
|
+
self.set_pv("ALARM", 1)
|
|
109
|
+
|
|
110
|
+
def cleanup(self):
|
|
111
|
+
self.logger.info("Stopping monitor")
|
|
112
|
+
|
|
113
|
+
def read_sensor(self):
|
|
114
|
+
return 42.0
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**config.yaml**
|
|
118
|
+
```yaml
|
|
119
|
+
parameters:
|
|
120
|
+
mode: continuous
|
|
121
|
+
interval: 1.0
|
|
122
|
+
threshold: 75.0
|
|
123
|
+
|
|
124
|
+
pvs:
|
|
125
|
+
inputs:
|
|
126
|
+
SETPOINT:
|
|
127
|
+
type: float
|
|
128
|
+
value: 50.0
|
|
129
|
+
unit: "%"
|
|
130
|
+
prec: 2
|
|
131
|
+
low: 0
|
|
132
|
+
high: 100
|
|
133
|
+
outputs:
|
|
134
|
+
READING:
|
|
135
|
+
type: float
|
|
136
|
+
value: 0.0
|
|
137
|
+
unit: "arb"
|
|
138
|
+
prec: 3
|
|
139
|
+
ALARM:
|
|
140
|
+
type: bool
|
|
141
|
+
value: 0
|
|
142
|
+
znam: "OK"
|
|
143
|
+
onam: "ALARM"
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Create a Job
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
# my_diagnostics.py
|
|
150
|
+
from iocmng import JobBase
|
|
151
|
+
from iocmng.base.job import JobResult
|
|
152
|
+
|
|
153
|
+
class MyDiagnostics(JobBase):
|
|
154
|
+
def initialize(self):
|
|
155
|
+
self.logger.info("Preparing diagnostics")
|
|
156
|
+
|
|
157
|
+
def execute(self) -> JobResult:
|
|
158
|
+
info = {"status": "healthy", "uptime": 12345}
|
|
159
|
+
self.set_pv("SYSTEM_NAME", info["status"])
|
|
160
|
+
return JobResult(success=True, data=info, message="Diagnostics OK")
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### REST API Usage
|
|
164
|
+
|
|
165
|
+
#### Add a task
|
|
166
|
+
```bash
|
|
167
|
+
curl -X POST http://localhost:8080/api/v1/tasks \
|
|
168
|
+
-H "Content-Type: application/json" \
|
|
169
|
+
-d '{
|
|
170
|
+
"name": "my-monitor",
|
|
171
|
+
"git_url": "https://github.com/user/my-monitor-task.git",
|
|
172
|
+
"pat": "ghp_optional_token",
|
|
173
|
+
"branch": "main",
|
|
174
|
+
"path": "src/monitor",
|
|
175
|
+
"parameters": {"threshold": 80.0}
|
|
176
|
+
}'
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The `path` field specifies the sub-directory inside the repo where the Python file and `config.yaml` live. Parameters passed in the REST request override values from `config.yaml`.
|
|
180
|
+
|
|
181
|
+
#### Remove a task
|
|
182
|
+
```bash
|
|
183
|
+
curl -X DELETE http://localhost:8080/api/v1/tasks/my-monitor
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
#### Add a job
|
|
187
|
+
```bash
|
|
188
|
+
curl -X POST http://localhost:8080/api/v1/jobs \
|
|
189
|
+
-H "Content-Type: application/json" \
|
|
190
|
+
-d '{
|
|
191
|
+
"name": "my-diag",
|
|
192
|
+
"git_url": "https://github.com/user/my-diagnostics-job.git",
|
|
193
|
+
"path": "jobs/diagnostics"
|
|
194
|
+
}'
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
#### Run a job
|
|
198
|
+
```bash
|
|
199
|
+
curl -X POST http://localhost:8080/api/v1/jobs/my-diag/run
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
#### Remove a job
|
|
203
|
+
```bash
|
|
204
|
+
curl -X DELETE http://localhost:8080/api/v1/jobs/my-diag
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
#### List all tasks
|
|
208
|
+
```bash
|
|
209
|
+
curl http://localhost:8080/api/v1/tasks
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
#### Health check
|
|
213
|
+
```bash
|
|
214
|
+
curl http://localhost:8080/api/v1/health
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Plugin Structure
|
|
218
|
+
|
|
219
|
+
Each plugin lives in a git repository (or a sub-directory of one). The expected layout:
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
<repo-root>/
|
|
223
|
+
└── <path>/ # optional sub-directory (specified via REST "path" field)
|
|
224
|
+
├── my_plugin.py # Python module with TaskBase/JobBase subclass
|
|
225
|
+
├── config.yaml # Plugin configuration (PVs, parameters)
|
|
226
|
+
└── requirements.txt # Optional additional pip dependencies
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### config.yaml Format
|
|
230
|
+
|
|
231
|
+
```yaml
|
|
232
|
+
# Parameters — passed to the plugin constructor as self.parameters
|
|
233
|
+
# REST-supplied parameters override these defaults
|
|
234
|
+
parameters:
|
|
235
|
+
mode: continuous # "continuous" or "triggered"
|
|
236
|
+
interval: 1.0 # application-specific
|
|
237
|
+
threshold: 75.0 # application-specific
|
|
238
|
+
|
|
239
|
+
# PV definitions — created automatically by the IOC Manager
|
|
240
|
+
pvs:
|
|
241
|
+
inputs: # writable PVs (operator → plugin)
|
|
242
|
+
SETPOINT:
|
|
243
|
+
type: float # float, int, string, bool
|
|
244
|
+
value: 50.0 # initial value
|
|
245
|
+
unit: "%" # EGU (float only)
|
|
246
|
+
prec: 2 # precision (float only)
|
|
247
|
+
low: 0 # LOPR (float only)
|
|
248
|
+
high: 100 # HOPR (float only)
|
|
249
|
+
outputs: # read-only PVs (plugin → operator)
|
|
250
|
+
READING:
|
|
251
|
+
type: float
|
|
252
|
+
value: 0.0
|
|
253
|
+
ALARM:
|
|
254
|
+
type: bool
|
|
255
|
+
value: 0
|
|
256
|
+
znam: "OK" # zero-state name (bool only)
|
|
257
|
+
onam: "ALARM" # one-state name (bool only)
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Plugin Validation
|
|
261
|
+
|
|
262
|
+
When a task or job is added, the framework performs the following checks:
|
|
263
|
+
|
|
264
|
+
1. **Clone** — the git repository is cloned (with optional PAT for private repos)
|
|
265
|
+
2. **Dependencies** — `requirements.txt` is installed if present (from `path` or repo root)
|
|
266
|
+
3. **Config** — `config.yaml` is loaded from `path` to read PV definitions and default parameters
|
|
267
|
+
4. **Syntax** — Python files are parsed via AST for syntax errors
|
|
268
|
+
5. **Import** — the module is imported to check for runtime import errors
|
|
269
|
+
6. **Inheritance** — at least one class must derive from `TaskBase` or `JobBase`
|
|
270
|
+
7. **Abstract methods** — all abstract methods (`initialize`, `execute`, `cleanup`) must be implemented
|
|
271
|
+
|
|
272
|
+
If any check fails, the plugin is rejected and the error details are returned.
|
|
273
|
+
|
|
274
|
+
## Default PVs
|
|
275
|
+
|
|
276
|
+
Every task automatically gets these PVs (prefix: `BEAMLINE:NAMESPACE:TASKNAME`):
|
|
277
|
+
|
|
278
|
+
| PV | Type | Description |
|
|
279
|
+
|---|---|---|
|
|
280
|
+
| `ENABLE` | boolOut | Enable/disable the task |
|
|
281
|
+
| `STATUS` | mbbIn | INIT / RUN / PAUSED / END / ERROR |
|
|
282
|
+
| `MESSAGE` | stringIn | Human-readable status message |
|
|
283
|
+
| `CYCLE_COUNT` | longIn | Cycle counter (continuous mode) |
|
|
284
|
+
| `RUN` | boolOut | Trigger execution (triggered mode) |
|
|
285
|
+
|
|
286
|
+
Every job gets:
|
|
287
|
+
|
|
288
|
+
| PV | Type | Description |
|
|
289
|
+
|---|---|---|
|
|
290
|
+
| `STATUS` | mbbIn | IDLE / RUNNING / SUCCESS / FAILED |
|
|
291
|
+
| `MESSAGE` | stringIn | Human-readable status message |
|
|
292
|
+
|
|
293
|
+
Additional PVs are created from the `pvs` section of `config.yaml`.
|
|
294
|
+
|
|
295
|
+
## Choosing Between Continuous Task, Triggered Task, and Job
|
|
296
|
+
|
|
297
|
+
| | **Continuous Task** | **Triggered Task** | **Job** |
|
|
298
|
+
|---|---|---|---|
|
|
299
|
+
| **Execution** | `execute()` loops indefinitely | `execute()` called when `RUN` PV is written | `execute()` called via REST |
|
|
300
|
+
| **How triggered** | Automatic (runs on start) | Operator writes `1` to the `RUN` EPICS PV | `POST /api/v1/jobs/{name}/run` |
|
|
301
|
+
| **Return value** | None (side effects only) | None (side effects only) | `JobResult` with `success`, `data`, `message` |
|
|
302
|
+
| **Has `cleanup()`** | Yes | Yes | No |
|
|
303
|
+
| **EPICS PV** | `CYCLE_COUNT` | `RUN` (boolOut) | — |
|
|
304
|
+
| **Typical use** | Polling, monitoring, periodic updates | Operator-driven actions from CS-Studio/Phoebus | API-driven actions from scripts or services |
|
|
305
|
+
|
|
306
|
+
**Rule of thumb:**
|
|
307
|
+
- Use a **continuous task** for anything that needs to run on a regular cycle (e.g., reading a sensor every second).
|
|
308
|
+
- Use a **triggered task** when the action is initiated from the EPICS control system (e.g., an operator clicks a button in Phoebus that writes to a PV).
|
|
309
|
+
- Use a **job** when the action is initiated from software/REST (e.g., a Kubernetes CronJob, a CI script, or another microservice).
|
|
310
|
+
|
|
311
|
+
## Configuration
|
|
312
|
+
|
|
313
|
+
### Environment Variables
|
|
314
|
+
|
|
315
|
+
| Variable | Default | Description |
|
|
316
|
+
|---|---|---|
|
|
317
|
+
| `IOCMNG_CONFIG` | (none) | Path to config.yaml |
|
|
318
|
+
| `IOCMNG_BEAMLINE_CONFIG` | (none) | Path to values.yaml |
|
|
319
|
+
| `IOCMNG_PLUGINS_DIR` | `/data/plugins` | Directory for cloned plugins |
|
|
320
|
+
| `IOCMNG_HOST` | `0.0.0.0` | Server bind address |
|
|
321
|
+
| `IOCMNG_PORT` | `8080` | Server port |
|
|
322
|
+
| `IOCMNG_DISABLE_OPHYD` | `true` | Skip ophyd initialization |
|
|
323
|
+
| `IOCMNG_LOG_LEVEL` | `info` | Logging level |
|
|
324
|
+
|
|
325
|
+
### Optional: Ophyd Device Integration
|
|
326
|
+
|
|
327
|
+
When `ophyd` and `infn_ophyd_hal` are installed and `IOCMNG_DISABLE_OPHYD=false`, the controller automatically creates Ophyd device instances from your `values.yaml` IOC configuration. Tasks can access devices via `self.get_device()` and `self.list_devices()`.
|
|
328
|
+
|
|
329
|
+
## Project Structure
|
|
330
|
+
|
|
331
|
+
```
|
|
332
|
+
src/iocmng/
|
|
333
|
+
├── __init__.py # Package entry: exports TaskBase, JobBase
|
|
334
|
+
├── base/
|
|
335
|
+
│ ├── task.py # TaskBase — continuous tasks with PV support
|
|
336
|
+
│ └── job.py # JobBase — one-shot jobs with PV support
|
|
337
|
+
├── core/
|
|
338
|
+
│ ├── controller.py # Central plugin manager
|
|
339
|
+
│ ├── loader.py # Git clone + config loading + module loading
|
|
340
|
+
│ └── validator.py # Plugin validation
|
|
341
|
+
├── api/
|
|
342
|
+
│ ├── app.py # FastAPI application factory
|
|
343
|
+
│ ├── models.py # Pydantic request/response models
|
|
344
|
+
│ └── routes.py # REST API endpoints
|
|
345
|
+
└── ophyd/
|
|
346
|
+
└── factory.py # Optional ophyd device creation
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## Development
|
|
350
|
+
|
|
351
|
+
```bash
|
|
352
|
+
# Install in editable mode with dev dependencies
|
|
353
|
+
pip install -e ".[dev]"
|
|
354
|
+
|
|
355
|
+
# Run tests
|
|
356
|
+
pytest tests/ -v
|
|
357
|
+
|
|
358
|
+
# Format
|
|
359
|
+
black .
|
|
360
|
+
|
|
361
|
+
# Lint
|
|
362
|
+
flake8 .
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
## GitHub Actions
|
|
366
|
+
|
|
367
|
+
The workflow in `.github/workflows/release.yml` triggers on:
|
|
368
|
+
- **Git tags** matching `v*` (e.g., `v2.0.0`)
|
|
369
|
+
- **Manual dispatch** (workflow_dispatch)
|
|
370
|
+
|
|
371
|
+
It will:
|
|
372
|
+
1. Run tests
|
|
373
|
+
2. Build and publish the Python package to PyPI
|
|
374
|
+
3. Build and push a Docker image to GitHub Container Registry (ghcr.io)
|
|
375
|
+
|
|
376
|
+
## License
|
|
377
|
+
|
|
378
|
+
MIT
|
iocmng-2.0.0/README.md
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
# iocmng — IOC Manager Framework
|
|
2
|
+
|
|
3
|
+
A pluggable task/job framework for IOC Manager applications. Provides base classes for continuous **tasks** and one-shot **jobs** that can be dynamically loaded at runtime via a REST API.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **`TaskBase`** — base class for continuous tasks (run in a loop)
|
|
8
|
+
- **`JobBase`** — base class for one-shot jobs (run once, return result)
|
|
9
|
+
- **REST API** — add/remove tasks and jobs at runtime from git repositories
|
|
10
|
+
- **Validation** — plugins are validated (must derive from base class, must compile, abstract methods must be implemented)
|
|
11
|
+
- **EPICS soft IOC PVs** — every task and job gets default PVs (STATUS, MESSAGE, etc.) via `softioc`
|
|
12
|
+
- **Per-plugin `config.yaml`** — each plugin defines its PVs and parameters in a config file inside its git repo
|
|
13
|
+
- **Path support** — specify a sub-directory inside the git repo where the plugin sources live
|
|
14
|
+
- **Plugin `requirements.txt`** — plugins can ship their own dependencies
|
|
15
|
+
- **Optional Ophyd integration** — device abstraction via `ophyd`/`infn_ophyd_hal` (optional dependency)
|
|
16
|
+
- **Docker image** — ready-to-run container with the REST API
|
|
17
|
+
- **PyPI package** — `pip install iocmng`
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
### Install from PyPI
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install iocmng
|
|
25
|
+
|
|
26
|
+
# With all optional dependencies (ophyd, kubernetes)
|
|
27
|
+
pip install iocmng[all]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Run the API Server
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Using the CLI entry point
|
|
34
|
+
iocmng-server
|
|
35
|
+
|
|
36
|
+
# Or with environment variables
|
|
37
|
+
IOCMNG_PORT=8080 IOCMNG_LOG_LEVEL=debug iocmng-server
|
|
38
|
+
|
|
39
|
+
# Or with Docker
|
|
40
|
+
docker run -p 8080:8080 ghcr.io/infn-epics/epik8s-beamline-controller:latest
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Create a Task
|
|
44
|
+
|
|
45
|
+
Create a git repository with:
|
|
46
|
+
1. A Python file with a class deriving from `TaskBase`
|
|
47
|
+
2. A `config.yaml` defining PVs and parameters
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
my-monitor-repo/
|
|
51
|
+
├── my_monitor.py
|
|
52
|
+
├── config.yaml
|
|
53
|
+
└── requirements.txt # optional — extra dependencies
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**my_monitor.py**
|
|
57
|
+
```python
|
|
58
|
+
from iocmng import TaskBase
|
|
59
|
+
|
|
60
|
+
class MyMonitor(TaskBase):
|
|
61
|
+
def initialize(self):
|
|
62
|
+
self.logger.info("Starting monitor")
|
|
63
|
+
|
|
64
|
+
def execute(self):
|
|
65
|
+
value = self.read_sensor()
|
|
66
|
+
self.set_pv("READING", value)
|
|
67
|
+
if value > self.parameters.get("threshold", 75):
|
|
68
|
+
self.set_pv("ALARM", 1)
|
|
69
|
+
|
|
70
|
+
def cleanup(self):
|
|
71
|
+
self.logger.info("Stopping monitor")
|
|
72
|
+
|
|
73
|
+
def read_sensor(self):
|
|
74
|
+
return 42.0
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**config.yaml**
|
|
78
|
+
```yaml
|
|
79
|
+
parameters:
|
|
80
|
+
mode: continuous
|
|
81
|
+
interval: 1.0
|
|
82
|
+
threshold: 75.0
|
|
83
|
+
|
|
84
|
+
pvs:
|
|
85
|
+
inputs:
|
|
86
|
+
SETPOINT:
|
|
87
|
+
type: float
|
|
88
|
+
value: 50.0
|
|
89
|
+
unit: "%"
|
|
90
|
+
prec: 2
|
|
91
|
+
low: 0
|
|
92
|
+
high: 100
|
|
93
|
+
outputs:
|
|
94
|
+
READING:
|
|
95
|
+
type: float
|
|
96
|
+
value: 0.0
|
|
97
|
+
unit: "arb"
|
|
98
|
+
prec: 3
|
|
99
|
+
ALARM:
|
|
100
|
+
type: bool
|
|
101
|
+
value: 0
|
|
102
|
+
znam: "OK"
|
|
103
|
+
onam: "ALARM"
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Create a Job
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
# my_diagnostics.py
|
|
110
|
+
from iocmng import JobBase
|
|
111
|
+
from iocmng.base.job import JobResult
|
|
112
|
+
|
|
113
|
+
class MyDiagnostics(JobBase):
|
|
114
|
+
def initialize(self):
|
|
115
|
+
self.logger.info("Preparing diagnostics")
|
|
116
|
+
|
|
117
|
+
def execute(self) -> JobResult:
|
|
118
|
+
info = {"status": "healthy", "uptime": 12345}
|
|
119
|
+
self.set_pv("SYSTEM_NAME", info["status"])
|
|
120
|
+
return JobResult(success=True, data=info, message="Diagnostics OK")
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### REST API Usage
|
|
124
|
+
|
|
125
|
+
#### Add a task
|
|
126
|
+
```bash
|
|
127
|
+
curl -X POST http://localhost:8080/api/v1/tasks \
|
|
128
|
+
-H "Content-Type: application/json" \
|
|
129
|
+
-d '{
|
|
130
|
+
"name": "my-monitor",
|
|
131
|
+
"git_url": "https://github.com/user/my-monitor-task.git",
|
|
132
|
+
"pat": "ghp_optional_token",
|
|
133
|
+
"branch": "main",
|
|
134
|
+
"path": "src/monitor",
|
|
135
|
+
"parameters": {"threshold": 80.0}
|
|
136
|
+
}'
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The `path` field specifies the sub-directory inside the repo where the Python file and `config.yaml` live. Parameters passed in the REST request override values from `config.yaml`.
|
|
140
|
+
|
|
141
|
+
#### Remove a task
|
|
142
|
+
```bash
|
|
143
|
+
curl -X DELETE http://localhost:8080/api/v1/tasks/my-monitor
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
#### Add a job
|
|
147
|
+
```bash
|
|
148
|
+
curl -X POST http://localhost:8080/api/v1/jobs \
|
|
149
|
+
-H "Content-Type: application/json" \
|
|
150
|
+
-d '{
|
|
151
|
+
"name": "my-diag",
|
|
152
|
+
"git_url": "https://github.com/user/my-diagnostics-job.git",
|
|
153
|
+
"path": "jobs/diagnostics"
|
|
154
|
+
}'
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
#### Run a job
|
|
158
|
+
```bash
|
|
159
|
+
curl -X POST http://localhost:8080/api/v1/jobs/my-diag/run
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
#### Remove a job
|
|
163
|
+
```bash
|
|
164
|
+
curl -X DELETE http://localhost:8080/api/v1/jobs/my-diag
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
#### List all tasks
|
|
168
|
+
```bash
|
|
169
|
+
curl http://localhost:8080/api/v1/tasks
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
#### Health check
|
|
173
|
+
```bash
|
|
174
|
+
curl http://localhost:8080/api/v1/health
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Plugin Structure
|
|
178
|
+
|
|
179
|
+
Each plugin lives in a git repository (or a sub-directory of one). The expected layout:
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
<repo-root>/
|
|
183
|
+
└── <path>/ # optional sub-directory (specified via REST "path" field)
|
|
184
|
+
├── my_plugin.py # Python module with TaskBase/JobBase subclass
|
|
185
|
+
├── config.yaml # Plugin configuration (PVs, parameters)
|
|
186
|
+
└── requirements.txt # Optional additional pip dependencies
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### config.yaml Format
|
|
190
|
+
|
|
191
|
+
```yaml
|
|
192
|
+
# Parameters — passed to the plugin constructor as self.parameters
|
|
193
|
+
# REST-supplied parameters override these defaults
|
|
194
|
+
parameters:
|
|
195
|
+
mode: continuous # "continuous" or "triggered"
|
|
196
|
+
interval: 1.0 # application-specific
|
|
197
|
+
threshold: 75.0 # application-specific
|
|
198
|
+
|
|
199
|
+
# PV definitions — created automatically by the IOC Manager
|
|
200
|
+
pvs:
|
|
201
|
+
inputs: # writable PVs (operator → plugin)
|
|
202
|
+
SETPOINT:
|
|
203
|
+
type: float # float, int, string, bool
|
|
204
|
+
value: 50.0 # initial value
|
|
205
|
+
unit: "%" # EGU (float only)
|
|
206
|
+
prec: 2 # precision (float only)
|
|
207
|
+
low: 0 # LOPR (float only)
|
|
208
|
+
high: 100 # HOPR (float only)
|
|
209
|
+
outputs: # read-only PVs (plugin → operator)
|
|
210
|
+
READING:
|
|
211
|
+
type: float
|
|
212
|
+
value: 0.0
|
|
213
|
+
ALARM:
|
|
214
|
+
type: bool
|
|
215
|
+
value: 0
|
|
216
|
+
znam: "OK" # zero-state name (bool only)
|
|
217
|
+
onam: "ALARM" # one-state name (bool only)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Plugin Validation
|
|
221
|
+
|
|
222
|
+
When a task or job is added, the framework performs the following checks:
|
|
223
|
+
|
|
224
|
+
1. **Clone** — the git repository is cloned (with optional PAT for private repos)
|
|
225
|
+
2. **Dependencies** — `requirements.txt` is installed if present (from `path` or repo root)
|
|
226
|
+
3. **Config** — `config.yaml` is loaded from `path` to read PV definitions and default parameters
|
|
227
|
+
4. **Syntax** — Python files are parsed via AST for syntax errors
|
|
228
|
+
5. **Import** — the module is imported to check for runtime import errors
|
|
229
|
+
6. **Inheritance** — at least one class must derive from `TaskBase` or `JobBase`
|
|
230
|
+
7. **Abstract methods** — all abstract methods (`initialize`, `execute`, `cleanup`) must be implemented
|
|
231
|
+
|
|
232
|
+
If any check fails, the plugin is rejected and the error details are returned.
|
|
233
|
+
|
|
234
|
+
## Default PVs
|
|
235
|
+
|
|
236
|
+
Every task automatically gets these PVs (prefix: `BEAMLINE:NAMESPACE:TASKNAME`):
|
|
237
|
+
|
|
238
|
+
| PV | Type | Description |
|
|
239
|
+
|---|---|---|
|
|
240
|
+
| `ENABLE` | boolOut | Enable/disable the task |
|
|
241
|
+
| `STATUS` | mbbIn | INIT / RUN / PAUSED / END / ERROR |
|
|
242
|
+
| `MESSAGE` | stringIn | Human-readable status message |
|
|
243
|
+
| `CYCLE_COUNT` | longIn | Cycle counter (continuous mode) |
|
|
244
|
+
| `RUN` | boolOut | Trigger execution (triggered mode) |
|
|
245
|
+
|
|
246
|
+
Every job gets:
|
|
247
|
+
|
|
248
|
+
| PV | Type | Description |
|
|
249
|
+
|---|---|---|
|
|
250
|
+
| `STATUS` | mbbIn | IDLE / RUNNING / SUCCESS / FAILED |
|
|
251
|
+
| `MESSAGE` | stringIn | Human-readable status message |
|
|
252
|
+
|
|
253
|
+
Additional PVs are created from the `pvs` section of `config.yaml`.
|
|
254
|
+
|
|
255
|
+
## Choosing Between Continuous Task, Triggered Task, and Job
|
|
256
|
+
|
|
257
|
+
| | **Continuous Task** | **Triggered Task** | **Job** |
|
|
258
|
+
|---|---|---|---|
|
|
259
|
+
| **Execution** | `execute()` loops indefinitely | `execute()` called when `RUN` PV is written | `execute()` called via REST |
|
|
260
|
+
| **How triggered** | Automatic (runs on start) | Operator writes `1` to the `RUN` EPICS PV | `POST /api/v1/jobs/{name}/run` |
|
|
261
|
+
| **Return value** | None (side effects only) | None (side effects only) | `JobResult` with `success`, `data`, `message` |
|
|
262
|
+
| **Has `cleanup()`** | Yes | Yes | No |
|
|
263
|
+
| **EPICS PV** | `CYCLE_COUNT` | `RUN` (boolOut) | — |
|
|
264
|
+
| **Typical use** | Polling, monitoring, periodic updates | Operator-driven actions from CS-Studio/Phoebus | API-driven actions from scripts or services |
|
|
265
|
+
|
|
266
|
+
**Rule of thumb:**
|
|
267
|
+
- Use a **continuous task** for anything that needs to run on a regular cycle (e.g., reading a sensor every second).
|
|
268
|
+
- Use a **triggered task** when the action is initiated from the EPICS control system (e.g., an operator clicks a button in Phoebus that writes to a PV).
|
|
269
|
+
- Use a **job** when the action is initiated from software/REST (e.g., a Kubernetes CronJob, a CI script, or another microservice).
|
|
270
|
+
|
|
271
|
+
## Configuration
|
|
272
|
+
|
|
273
|
+
### Environment Variables
|
|
274
|
+
|
|
275
|
+
| Variable | Default | Description |
|
|
276
|
+
|---|---|---|
|
|
277
|
+
| `IOCMNG_CONFIG` | (none) | Path to config.yaml |
|
|
278
|
+
| `IOCMNG_BEAMLINE_CONFIG` | (none) | Path to values.yaml |
|
|
279
|
+
| `IOCMNG_PLUGINS_DIR` | `/data/plugins` | Directory for cloned plugins |
|
|
280
|
+
| `IOCMNG_HOST` | `0.0.0.0` | Server bind address |
|
|
281
|
+
| `IOCMNG_PORT` | `8080` | Server port |
|
|
282
|
+
| `IOCMNG_DISABLE_OPHYD` | `true` | Skip ophyd initialization |
|
|
283
|
+
| `IOCMNG_LOG_LEVEL` | `info` | Logging level |
|
|
284
|
+
|
|
285
|
+
### Optional: Ophyd Device Integration
|
|
286
|
+
|
|
287
|
+
When `ophyd` and `infn_ophyd_hal` are installed and `IOCMNG_DISABLE_OPHYD=false`, the controller automatically creates Ophyd device instances from your `values.yaml` IOC configuration. Tasks can access devices via `self.get_device()` and `self.list_devices()`.
|
|
288
|
+
|
|
289
|
+
## Project Structure
|
|
290
|
+
|
|
291
|
+
```
|
|
292
|
+
src/iocmng/
|
|
293
|
+
├── __init__.py # Package entry: exports TaskBase, JobBase
|
|
294
|
+
├── base/
|
|
295
|
+
│ ├── task.py # TaskBase — continuous tasks with PV support
|
|
296
|
+
│ └── job.py # JobBase — one-shot jobs with PV support
|
|
297
|
+
├── core/
|
|
298
|
+
│ ├── controller.py # Central plugin manager
|
|
299
|
+
│ ├── loader.py # Git clone + config loading + module loading
|
|
300
|
+
│ └── validator.py # Plugin validation
|
|
301
|
+
├── api/
|
|
302
|
+
│ ├── app.py # FastAPI application factory
|
|
303
|
+
│ ├── models.py # Pydantic request/response models
|
|
304
|
+
│ └── routes.py # REST API endpoints
|
|
305
|
+
└── ophyd/
|
|
306
|
+
└── factory.py # Optional ophyd device creation
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## Development
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
# Install in editable mode with dev dependencies
|
|
313
|
+
pip install -e ".[dev]"
|
|
314
|
+
|
|
315
|
+
# Run tests
|
|
316
|
+
pytest tests/ -v
|
|
317
|
+
|
|
318
|
+
# Format
|
|
319
|
+
black .
|
|
320
|
+
|
|
321
|
+
# Lint
|
|
322
|
+
flake8 .
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## GitHub Actions
|
|
326
|
+
|
|
327
|
+
The workflow in `.github/workflows/release.yml` triggers on:
|
|
328
|
+
- **Git tags** matching `v*` (e.g., `v2.0.0`)
|
|
329
|
+
- **Manual dispatch** (workflow_dispatch)
|
|
330
|
+
|
|
331
|
+
It will:
|
|
332
|
+
1. Run tests
|
|
333
|
+
2. Build and publish the Python package to PyPI
|
|
334
|
+
3. Build and push a Docker image to GitHub Container Registry (ghcr.io)
|
|
335
|
+
|
|
336
|
+
## License
|
|
337
|
+
|
|
338
|
+
MIT
|