kilonova 0.1.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.
- kilonova-0.1.0/.github/workflows/ci.yml +50 -0
- kilonova-0.1.0/.github/workflows/release.yml +30 -0
- kilonova-0.1.0/.gitignore +13 -0
- kilonova-0.1.0/LICENSE +25 -0
- kilonova-0.1.0/PKG-INFO +111 -0
- kilonova-0.1.0/PLAN.md +126 -0
- kilonova-0.1.0/README.md +85 -0
- kilonova-0.1.0/doc/Architecture.md +53 -0
- kilonova-0.1.0/doc/DeviceLogic.md +87 -0
- kilonova-0.1.0/doc/Parity.md +62 -0
- kilonova-0.1.0/examples/sca/Design.xml +43 -0
- kilonova-0.1.0/examples/sca/config.xml +13 -0
- kilonova-0.1.0/examples/sca/demo.py +33 -0
- kilonova-0.1.0/pyproject.toml +55 -0
- kilonova-0.1.0/src/kilonova/__init__.py +12 -0
- kilonova-0.1.0/src/kilonova/address_space.py +413 -0
- kilonova-0.1.0/src/kilonova/calculated.py +234 -0
- kilonova-0.1.0/src/kilonova/cli.py +58 -0
- kilonova-0.1.0/src/kilonova/config.py +222 -0
- kilonova-0.1.0/src/kilonova/design.py +390 -0
- kilonova-0.1.0/src/kilonova/dump.py +192 -0
- kilonova-0.1.0/src/kilonova/errors.py +13 -0
- kilonova-0.1.0/src/kilonova/meta.py +135 -0
- kilonova-0.1.0/src/kilonova/objects.py +89 -0
- kilonova-0.1.0/src/kilonova/oracle.py +162 -0
- kilonova-0.1.0/src/kilonova/server.py +389 -0
- kilonova-0.1.0/tests/__init__.py +0 -0
- kilonova-0.1.0/tests/conformance/__init__.py +0 -0
- kilonova-0.1.0/tests/conformance/test_conformance.py +77 -0
- kilonova-0.1.0/tests/conftest.py +45 -0
- kilonova-0.1.0/tests/data/Design.xml +43 -0
- kilonova-0.1.0/tests/data/config.xml +13 -0
- kilonova-0.1.0/tests/test_cache_variables.py +71 -0
- kilonova-0.1.0/tests/test_calculated_variables.py +74 -0
- kilonova-0.1.0/tests/test_design.py +79 -0
- kilonova-0.1.0/tests/test_instantiation.py +40 -0
- kilonova-0.1.0/tests/test_meta.py +50 -0
- kilonova-0.1.0/tests/test_methods.py +46 -0
- kilonova-0.1.0/tests/test_robustness.py +206 -0
- kilonova-0.1.0/tests/test_server_boot.py +33 -0
- kilonova-0.1.0/tests/test_setters.py +76 -0
- kilonova-0.1.0/tests/test_source_variables.py +124 -0
- kilonova-0.1.0/tools/cacophony_crosscheck.py +101 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
lint:
|
|
15
|
+
name: ruff
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
- uses: astral-sh/setup-uv@v5
|
|
20
|
+
- run: uv sync --dev
|
|
21
|
+
- run: uv run ruff check src tests examples tools
|
|
22
|
+
- run: uv run ruff format --check src tests examples tools || true
|
|
23
|
+
|
|
24
|
+
test:
|
|
25
|
+
name: tests + quasar conformance (py${{ matrix.python-version }})
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
strategy:
|
|
28
|
+
fail-fast: false
|
|
29
|
+
matrix:
|
|
30
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v4
|
|
33
|
+
# kilonova's oracle is quasar's own CI test-case suite: the conformance job
|
|
34
|
+
# serves every case and diffs the client-visible address space against the
|
|
35
|
+
# reference nodesets - the same gate quasar's C++ CI uses for its backends.
|
|
36
|
+
- name: Check out quasar (conformance oracle)
|
|
37
|
+
uses: actions/checkout@v4
|
|
38
|
+
with:
|
|
39
|
+
repository: quasar-team/quasar
|
|
40
|
+
path: quasar
|
|
41
|
+
- uses: astral-sh/setup-uv@v5
|
|
42
|
+
with:
|
|
43
|
+
python-version: ${{ matrix.python-version }}
|
|
44
|
+
- run: uv sync --dev
|
|
45
|
+
- name: Unit + UX tests
|
|
46
|
+
run: uv run pytest tests -q --ignore=tests/conformance
|
|
47
|
+
- name: Conformance vs quasar reference nodesets
|
|
48
|
+
env:
|
|
49
|
+
KILONOVA_QUASAR_ROOT: ${{ github.workspace }}/quasar
|
|
50
|
+
run: uv run pytest tests/conformance -v
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- uses: astral-sh/setup-uv@v5
|
|
13
|
+
- run: uv build
|
|
14
|
+
- uses: actions/upload-artifact@v4
|
|
15
|
+
with:
|
|
16
|
+
name: dist
|
|
17
|
+
path: dist/
|
|
18
|
+
|
|
19
|
+
publish:
|
|
20
|
+
needs: build
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
environment: pypi
|
|
23
|
+
permissions:
|
|
24
|
+
id-token: write # PyPI Trusted Publishing (OIDC)
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/download-artifact@v4
|
|
27
|
+
with:
|
|
28
|
+
name: dist
|
|
29
|
+
path: dist/
|
|
30
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
kilonova-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
BSD 2-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Paris Moschovakos
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
17
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
18
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
19
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
20
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
21
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
22
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
23
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
24
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
25
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
kilonova-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kilonova
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pure-Python OPC UA servers from quasar Design files — the quasarnova family's no-codegen engine
|
|
5
|
+
Project-URL: Homepage, https://github.com/quasarnova-team/kilonova
|
|
6
|
+
Project-URL: Repository, https://github.com/quasarnova-team/kilonova
|
|
7
|
+
Project-URL: Issues, https://github.com/quasarnova-team/kilonova/issues
|
|
8
|
+
Author-email: Paris Moschovakos <paris@moschovakos.com>
|
|
9
|
+
License-Expression: BSD-2-Clause
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: asyncua,cern,opc-ua,opcua,quasar,scada,server
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Manufacturing
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
21
|
+
Classifier: Topic :: System :: Hardware
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: asyncua>=2.0
|
|
24
|
+
Requires-Dist: lxml>=5
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# kilonova
|
|
28
|
+
|
|
29
|
+
[](https://github.com/quasarnova-team/kilonova/actions/workflows/ci.yml)
|
|
30
|
+
|
|
31
|
+
What is this?
|
|
32
|
+
-------------
|
|
33
|
+
|
|
34
|
+
kilonova serves a quasar `Design.xml` + `config.xml` as a live OPC UA server in pure
|
|
35
|
+
Python — no code generation, no C++. It produces the same address space as a
|
|
36
|
+
[quasar](https://github.com/quasar-team/quasar)-generated server (same `ns=2` string NodeIds,
|
|
37
|
+
same dotted `parent.child` addressing), so quasar ecosystem tools —
|
|
38
|
+
[Cacophony](https://github.com/quasar-team/Cacophony)/WinCC OA,
|
|
39
|
+
[UaoForQuasar](https://github.com/quasar-team/UaoForQuasar) clients, plain OPC UA clients —
|
|
40
|
+
work against it unmodified.
|
|
41
|
+
|
|
42
|
+
A *kilonova* is the luminous flash of a neutron-star merger — a lighter, faster transient
|
|
43
|
+
in the nova family. This kilonova is the pure-Python engine of the
|
|
44
|
+
[quasarnova](https://github.com/quasarnova-team) family: successor of
|
|
45
|
+
[MilkyWay](https://github.com/quasar-team/MilkyWay) (Piotr Nikiel's 2021 prototype, and
|
|
46
|
+
briefly named microquasar), rebuilt from scratch on
|
|
47
|
+
[asyncua](https://github.com/FreeOpcUa/opcua-asyncio) 2.x.
|
|
48
|
+
|
|
49
|
+
Basic usage mode
|
|
50
|
+
----------------
|
|
51
|
+
|
|
52
|
+
1. Install (Python ≥ 3.10): `pip install .` (from this repository, for now)
|
|
53
|
+
1. Run your existing quasar server's design, unchanged:
|
|
54
|
+
`kilonova run --design Design/Design.xml --config bin/config.xml`
|
|
55
|
+
1. Point any OPC UA client at `opc.tcp://host:4841` — the address space is quasar's.
|
|
56
|
+
1. Dump a running server's address space (uasak_dump-style NodeSet2):
|
|
57
|
+
`kilonova dump --endpoint opc.tcp://127.0.0.1:4841 --output dump.xml`
|
|
58
|
+
|
|
59
|
+
Device logic is plain Python (see [doc/DeviceLogic.md](doc/DeviceLogic.md)):
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from kilonova import Server
|
|
63
|
+
|
|
64
|
+
server = Server("Design.xml", config_path="config.xml")
|
|
65
|
+
|
|
66
|
+
@server.read("sca1.adc") # source variable: runs inside the client read
|
|
67
|
+
async def read_adc(obj):
|
|
68
|
+
return await hardware.read_adc()
|
|
69
|
+
|
|
70
|
+
@server.method("sca1.reset") # method handler
|
|
71
|
+
async def reset(obj):
|
|
72
|
+
await obj.setOnline(0) # generated setter, quasar naming
|
|
73
|
+
|
|
74
|
+
async with server:
|
|
75
|
+
...
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
What works
|
|
79
|
+
----------
|
|
80
|
+
|
|
81
|
+
All 12 cases of quasar's own CI test suite pass against the reference nodesets
|
|
82
|
+
(cache/source/calculated variables, methods incl. arguments, config entries and
|
|
83
|
+
restrictions, singleVariableNode, design/config instantiation, StandardMetaData).
|
|
84
|
+
Production designs (ATLAS ATCA, CAEN, CanOpen) were probed at structural parity against
|
|
85
|
+
live C++ servers of both backends. Details: [doc/Parity.md](doc/Parity.md).
|
|
86
|
+
|
|
87
|
+
Limitations
|
|
88
|
+
-----------
|
|
89
|
+
|
|
90
|
+
- Device logic is registered per address at runtime — there is no generated `D<Class>`
|
|
91
|
+
skeleton (that is the point).
|
|
92
|
+
- Design-mandated children are instantiated unconditionally; C++ device logic may create
|
|
93
|
+
some conditionally.
|
|
94
|
+
- No server-side security policies yet (NoSecurity endpoint only).
|
|
95
|
+
- Values live in asyncua's address space; extreme write rates were not a design goal.
|
|
96
|
+
|
|
97
|
+
Documentation
|
|
98
|
+
-------------
|
|
99
|
+
|
|
100
|
+
- [doc/Architecture.md](doc/Architecture.md) — modules and data flow
|
|
101
|
+
- [doc/DeviceLogic.md](doc/DeviceLogic.md) — the user API
|
|
102
|
+
- [doc/Parity.md](doc/Parity.md) — the parity contract and current status
|
|
103
|
+
- [PLAN.md](PLAN.md) — milestone log and engineering notes
|
|
104
|
+
|
|
105
|
+
Credits
|
|
106
|
+
-------
|
|
107
|
+
|
|
108
|
+
- Paris Moschovakos (paris@moschovakos.com) — kilonova
|
|
109
|
+
- Piotr Nikiel — quasar concept and architecture; MilkyWay, the predecessor
|
|
110
|
+
|
|
111
|
+
License: BSD-2-Clause.
|
kilonova-0.1.0/PLAN.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# kilonova — implementation plan
|
|
2
|
+
|
|
3
|
+
One feature at a time; every step lands with tests, and every user-visible behaviour is tested
|
|
4
|
+
**from the UX**: a real asyncua `Client` connects to the running server and reads the address
|
|
5
|
+
space, exactly as WinCC OA, UaoForQuasar, or uasak_dump would.
|
|
6
|
+
|
|
7
|
+
## The parity contract
|
|
8
|
+
|
|
9
|
+
The oracle is quasar's own CI: `quasar/.CI/test_cases/manifest.json` declares ~12 cases, each
|
|
10
|
+
with `Design.xml` + `config.xml` + `reference_ns2.xml`. The gate (same semantics as
|
|
11
|
+
`NodeSetTools/nodeset_compare.py`):
|
|
12
|
+
|
|
13
|
+
- every `UAObject` / `UAVariable` / `UAMethod` NodeId in the reference must exist exactly once
|
|
14
|
+
in our client-side dump;
|
|
15
|
+
- every attribute present in the reference (`BrowseName`, `DataType`, `ValueRank`,
|
|
16
|
+
`AccessLevel`) must match exactly. Format rules learned from uasak_dump: NS0 ids as `i=N`,
|
|
17
|
+
quasar ids as `ns=2;s=path`, XSD-default attributes suppressed (DataType `i=24`,
|
|
18
|
+
ValueRank `-1`, AccessLevel `1`);
|
|
19
|
+
- `StandardMetaData` is compared in full for the `default_design` case (its reference is
|
|
20
|
+
the meta oracle); for the other cases it is ignored exactly as quasar's own CI does —
|
|
21
|
+
their references carry stale, mutually contradictory meta snapshots (minThreads i=7 vs
|
|
22
|
+
i=12). kilonova's meta is live: log-level variables drive the Python loggers, and
|
|
23
|
+
`<StandardMetaData>` config sections set initial levels.
|
|
24
|
+
|
|
25
|
+
## Milestones
|
|
26
|
+
|
|
27
|
+
| # | Feature | UX test gate | Status |
|
|
28
|
+
|---|---------|--------------|--------|
|
|
29
|
+
| M0 | Scaffold: pyproject/uv, ruff, pytest, git | `uv run pytest` runs | done |
|
|
30
|
+
| M1 | Design layer: typed `Design.xml` parser | parses all quasar CI test-case designs | done |
|
|
31
|
+
| M2 | Server core on asyncua: boot, ns=2, ObjectTypes | Client connects, browses, finds types | done |
|
|
32
|
+
| M3 | Config instantiation: recursive objects, dotted string NodeIds | Client resolves exact NodeIds from config tree | done |
|
|
33
|
+
| M4 | Cache variables: DataType/ValueRank/AccessLevel/initialValue+Status | Client reads every attribute + value + status | done |
|
|
34
|
+
| M5 | MilkyWay parity: `set_cv`, generated `setXxx` setters, live demo | Client subscribes, sees ticking value | done |
|
|
35
|
+
| M6 | Conformance runner + dumper (uasak_dump equivalent) | parity table over quasar CI cases | done |
|
|
36
|
+
| M7 | Methods: nodes + real async handlers (decorator API) | Client calls method, gets result | done |
|
|
37
|
+
| M8 | Source variables + delegated-write callbacks | Client read/write triggers user coroutine | done |
|
|
38
|
+
| M9 | CalculatedVariables (safe formula eval) | Client reads computed value | done |
|
|
39
|
+
| M10 | StandardMetaData subtree | default_design case passes un-ignored | done |
|
|
40
|
+
| M11 | Config restrictions + cardinality validation | invalid config rejected like C++ Configurator | done |
|
|
41
|
+
| M12 | Ecosystem smoke: UaoForQuasar client + Cacophony against kilonova | generated client works unmodified | done (local legs) |
|
|
42
|
+
| M13 | parity-night third backend column (production servers) | probe parity vs live C++ backends | done — ATCA/CAEN full structural parity; CanOpen surfaced 2 cross-backend quirks (see .parity-night/cells/*-kilonova/deps-note.txt) |
|
|
43
|
+
|
|
44
|
+
## Current parity table (M6 gate, StandardMetaData ignored)
|
|
45
|
+
|
|
46
|
+
Run `uv run pytest tests/conformance -v` with a quasar checkout next door.
|
|
47
|
+
As of M6 (all verified by `pytest tests/conformance`, 2026-07-11):
|
|
48
|
+
|
|
49
|
+
| quasar CI case | verdict |
|
|
50
|
+
|----------------|---------|
|
|
51
|
+
| default_design | PASS (full comparison incl. StandardMetaData) |
|
|
52
|
+
| methods | PASS |
|
|
53
|
+
| async_methods | PASS |
|
|
54
|
+
| cache_variables | PASS |
|
|
55
|
+
| config_entries | PASS |
|
|
56
|
+
| recurrent_hasobjects | PASS |
|
|
57
|
+
| single_variable_node | PASS |
|
|
58
|
+
| instantiation_from_design | PASS |
|
|
59
|
+
| config_restrictions | PASS |
|
|
60
|
+
| defaulted_instance_name | PASS |
|
|
61
|
+
| source_variables | PASS |
|
|
62
|
+
| calculated_variables | PASS |
|
|
63
|
+
|
|
64
|
+
Method *nodes* (incl. `.args`/`.return_values` argument properties) are at parity as part of
|
|
65
|
+
M6; M7 added callable handlers via the `@server.method("sca1.scale")` decorator API.
|
|
66
|
+
|
|
67
|
+
M8 (source variables): `@server.read("sca1.adc")` runs the device coroutine *inside* the
|
|
68
|
+
client's read transaction (asyncua's awaited PreRead callback — true quasar synchronous-read
|
|
69
|
+
semantics, impossible in the 2021 sync prototype); `@server.write(...)` intercepts client
|
|
70
|
+
writes to source variables and `addressSpaceWrite="delegated"` cache variables before storage
|
|
71
|
+
(raise `ua.UaStatusCodeError` to refuse with a real per-item status; unhandled delegated
|
|
72
|
+
writes answer BadNotImplemented). AccessLevel encodes the design's read/write modes
|
|
73
|
+
(read-only 1, write-only 2, read-write 3); until first device interaction source variables
|
|
74
|
+
serve BadWaitingForInitialData. Server-side `set_cv` bypasses delegation by design.
|
|
75
|
+
|
|
76
|
+
M9 (calculated variables): config-level `<FreeVariable>`, `<CalculatedVariable>` and
|
|
77
|
+
`<CalculatedVariableGenericFormula>` are served; formulas compile to a whitelisted AST (no
|
|
78
|
+
eval), inputs are dotted addresses, `$thisObjectAddress` and `$applyGenericFormula(...)`
|
|
79
|
+
are substituted, and recalculation runs through server-side datachange callbacks — a
|
|
80
|
+
dependent recomputes *inside* the write that changed its input, so the next read is fresh.
|
|
81
|
+
Any null/bad input yields BadWaitingForInitialData. **All 12 quasar CI oracle cases now pass.**
|
|
82
|
+
|
|
83
|
+
M12 (ecosystem smoke, local legs — 2026-07-11): Cacophony generates its WinCC OA CTRL
|
|
84
|
+
artifacts cleanly from kilonova-served designs, and `tools/cacophony_crosscheck.py`
|
|
85
|
+
verifies every periphery address the generated configParser.ctl would assign resolves on a
|
|
86
|
+
live kilonova server: 8/8 on the SCA demo, **600/600 on the production ATCA design**.
|
|
87
|
+
UaoForQuasar client classes generate cleanly for production classes (Manager/Board/FanTray)
|
|
88
|
+
and their generated NodeId construction (`parentId + ".var"`, parent namespace) is exactly
|
|
89
|
+
kilonova's addressing. Deferred: compiling/running the generated C++ client (needs UASDK
|
|
90
|
+
— a docker parity-image job, natural follow-up in the parity-night campaign).
|
|
91
|
+
|
|
92
|
+
## Design decisions (2026 rewrite, vs the 2021 MilkyWay prototype)
|
|
93
|
+
|
|
94
|
+
- **asyncua 2.x** (python-opcua is deprecated); async-first, `asyncio.run` friendly.
|
|
95
|
+
- **No copy-paste of quasar's DesignInspector** — a clean-room typed design layer
|
|
96
|
+
(`design.py`) exposing only what the server needs.
|
|
97
|
+
- **Setters done right**: `set` + upper-first camelCase (`setMyVar`, not `.title()`), async.
|
|
98
|
+
- **DataType always set from the Design** (the 2021 prototype derived it from values;
|
|
99
|
+
null-initialized variables then reported BaseDataType).
|
|
100
|
+
- **Conformance from day one**: the dumper reads the address space through a real client
|
|
101
|
+
connection — testing "from the UX" is the default, not an afterthought.
|
|
102
|
+
- The 2021 bugs are regression-tested: initialStatus actually applied, numeric initialValue
|
|
103
|
+
works, unsupported config elements raise instead of NameError-ing.
|
|
104
|
+
|
|
105
|
+
## Parity rules learned the hard way (2026-07-11 adversarial review)
|
|
106
|
+
|
|
107
|
+
A 38-agent review panel with live-repro verification found parity bugs the M6 gate is
|
|
108
|
+
structurally blind to (the comparison is one-directional and value-less). All fixed and
|
|
109
|
+
regression-tested in `tests/test_robustness.py`:
|
|
110
|
+
|
|
111
|
+
- **DataType follows nullPolicy**: C++ quasar sets the concrete DataType only for
|
|
112
|
+
`nullForbidden` variables; `nullAllowed` ones serve BaseDataType (i=24) so null writes
|
|
113
|
+
stay legal. (asyncua's type heuristic refuses writes to BaseDataType nodes — instance-local
|
|
114
|
+
override in `Server._allow_writes_to_base_datatype_nodes`, worth upstreaming.)
|
|
115
|
+
- **Arrays are `<value>` elements** in config.xml (quasar's generated Configuration.xsd);
|
|
116
|
+
text-content arrays are rejected loudly, as the C++ Configurator would.
|
|
117
|
+
- **`defaultConfigInitializerValue`** is honoured when the config omits a value.
|
|
118
|
+
- **singleVariableNode instances** take configuration values.
|
|
119
|
+
- **Array config entries** are NOT published as properties (C++ skips them).
|
|
120
|
+
- Robustness: unknown config attributes/children and out-of-range integers are rejected at
|
|
121
|
+
load; `set_cv` raises on refused writes and range violations; method calls validate
|
|
122
|
+
argument counts (BadArgumentsMissing/BadTooManyArguments).
|
|
123
|
+
|
|
124
|
+
Known gate limitations (roadmap): the comparer checks only NodeId+attributes (like quasar's
|
|
125
|
+
own CI) — values and references are dumped but not compared; strengthening it beyond the C++
|
|
126
|
+
gate is future work alongside M11.
|
kilonova-0.1.0/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# kilonova
|
|
2
|
+
|
|
3
|
+
[](https://github.com/quasarnova-team/kilonova/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
What is this?
|
|
6
|
+
-------------
|
|
7
|
+
|
|
8
|
+
kilonova serves a quasar `Design.xml` + `config.xml` as a live OPC UA server in pure
|
|
9
|
+
Python — no code generation, no C++. It produces the same address space as a
|
|
10
|
+
[quasar](https://github.com/quasar-team/quasar)-generated server (same `ns=2` string NodeIds,
|
|
11
|
+
same dotted `parent.child` addressing), so quasar ecosystem tools —
|
|
12
|
+
[Cacophony](https://github.com/quasar-team/Cacophony)/WinCC OA,
|
|
13
|
+
[UaoForQuasar](https://github.com/quasar-team/UaoForQuasar) clients, plain OPC UA clients —
|
|
14
|
+
work against it unmodified.
|
|
15
|
+
|
|
16
|
+
A *kilonova* is the luminous flash of a neutron-star merger — a lighter, faster transient
|
|
17
|
+
in the nova family. This kilonova is the pure-Python engine of the
|
|
18
|
+
[quasarnova](https://github.com/quasarnova-team) family: successor of
|
|
19
|
+
[MilkyWay](https://github.com/quasar-team/MilkyWay) (Piotr Nikiel's 2021 prototype, and
|
|
20
|
+
briefly named microquasar), rebuilt from scratch on
|
|
21
|
+
[asyncua](https://github.com/FreeOpcUa/opcua-asyncio) 2.x.
|
|
22
|
+
|
|
23
|
+
Basic usage mode
|
|
24
|
+
----------------
|
|
25
|
+
|
|
26
|
+
1. Install (Python ≥ 3.10): `pip install .` (from this repository, for now)
|
|
27
|
+
1. Run your existing quasar server's design, unchanged:
|
|
28
|
+
`kilonova run --design Design/Design.xml --config bin/config.xml`
|
|
29
|
+
1. Point any OPC UA client at `opc.tcp://host:4841` — the address space is quasar's.
|
|
30
|
+
1. Dump a running server's address space (uasak_dump-style NodeSet2):
|
|
31
|
+
`kilonova dump --endpoint opc.tcp://127.0.0.1:4841 --output dump.xml`
|
|
32
|
+
|
|
33
|
+
Device logic is plain Python (see [doc/DeviceLogic.md](doc/DeviceLogic.md)):
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from kilonova import Server
|
|
37
|
+
|
|
38
|
+
server = Server("Design.xml", config_path="config.xml")
|
|
39
|
+
|
|
40
|
+
@server.read("sca1.adc") # source variable: runs inside the client read
|
|
41
|
+
async def read_adc(obj):
|
|
42
|
+
return await hardware.read_adc()
|
|
43
|
+
|
|
44
|
+
@server.method("sca1.reset") # method handler
|
|
45
|
+
async def reset(obj):
|
|
46
|
+
await obj.setOnline(0) # generated setter, quasar naming
|
|
47
|
+
|
|
48
|
+
async with server:
|
|
49
|
+
...
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
What works
|
|
53
|
+
----------
|
|
54
|
+
|
|
55
|
+
All 12 cases of quasar's own CI test suite pass against the reference nodesets
|
|
56
|
+
(cache/source/calculated variables, methods incl. arguments, config entries and
|
|
57
|
+
restrictions, singleVariableNode, design/config instantiation, StandardMetaData).
|
|
58
|
+
Production designs (ATLAS ATCA, CAEN, CanOpen) were probed at structural parity against
|
|
59
|
+
live C++ servers of both backends. Details: [doc/Parity.md](doc/Parity.md).
|
|
60
|
+
|
|
61
|
+
Limitations
|
|
62
|
+
-----------
|
|
63
|
+
|
|
64
|
+
- Device logic is registered per address at runtime — there is no generated `D<Class>`
|
|
65
|
+
skeleton (that is the point).
|
|
66
|
+
- Design-mandated children are instantiated unconditionally; C++ device logic may create
|
|
67
|
+
some conditionally.
|
|
68
|
+
- No server-side security policies yet (NoSecurity endpoint only).
|
|
69
|
+
- Values live in asyncua's address space; extreme write rates were not a design goal.
|
|
70
|
+
|
|
71
|
+
Documentation
|
|
72
|
+
-------------
|
|
73
|
+
|
|
74
|
+
- [doc/Architecture.md](doc/Architecture.md) — modules and data flow
|
|
75
|
+
- [doc/DeviceLogic.md](doc/DeviceLogic.md) — the user API
|
|
76
|
+
- [doc/Parity.md](doc/Parity.md) — the parity contract and current status
|
|
77
|
+
- [PLAN.md](PLAN.md) — milestone log and engineering notes
|
|
78
|
+
|
|
79
|
+
Credits
|
|
80
|
+
-------
|
|
81
|
+
|
|
82
|
+
- Paris Moschovakos (paris@moschovakos.com) — kilonova
|
|
83
|
+
- Piotr Nikiel — quasar concept and architecture; MilkyWay, the predecessor
|
|
84
|
+
|
|
85
|
+
License: BSD-2-Clause.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
What is this?
|
|
4
|
+
-------------
|
|
5
|
+
|
|
6
|
+
How a Design file becomes a served address space. One page; the code is small
|
|
7
|
+
(~1500 lines) and each module has one job.
|
|
8
|
+
|
|
9
|
+
Data flow
|
|
10
|
+
---------
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
Design.xml ──> design.py ──────┐
|
|
14
|
+
├──> address_space.py ──> asyncua Server (ns=2)
|
|
15
|
+
config.xml ──> config.py ──────┘ │
|
|
16
|
+
├── objects.py QuasarObject handles, setters
|
|
17
|
+
├── calculated.py formulas + recalculation graph
|
|
18
|
+
└── meta.py StandardMetaData subtree
|
|
19
|
+
server.py orchestrates; dispatches methods / source reads / delegated writes
|
|
20
|
+
dump.py reads it all back through a real client (uasak_dump equivalent)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Modules
|
|
24
|
+
-------
|
|
25
|
+
|
|
26
|
+
| Module | Job |
|
|
27
|
+
|--------|-----|
|
|
28
|
+
| `design.py` | Typed, frozen model of Design.xml. Total over quasar's CI corpus; unknown constructs fail loudly at instantiation, not parsing. |
|
|
29
|
+
| `config.py` | config.xml → instance tree. Validates like the C++ Configurator: unknown attributes/children, hasobjects, `<value>` arrays, restrictions input. |
|
|
30
|
+
| `oracle.py` | quasar dataType ↔ OPC UA type maps, value parsing, integer ranges, configRestriction checks. (Named after quasar's own Oracle.) |
|
|
31
|
+
| `address_space.py` | Builds ObjectTypes (`ns=2;i=1000+`) and instances (`ns=2;s=parent.child`). Owns the parity-critical attribute rules. |
|
|
32
|
+
| `objects.py` | `QuasarObject`: `set_cv`, `get_cv`, generated `setXxx` async setters. |
|
|
33
|
+
| `server.py` | Lifecycle + handler registries (`@server.method/read/write`), source-read PreRead hook, delegated-write interception. |
|
|
34
|
+
| `calculated.py` | Config-level CalculatedVariables/FreeVariables: whitelisted-AST formulas (no `eval`), datachange-driven recalculation. |
|
|
35
|
+
| `meta.py` | StandardMetaData, live: log-level nodes drive Python logging. |
|
|
36
|
+
| `dump.py` | Client-side NodeSet2 dump + reference comparison (the conformance gate). |
|
|
37
|
+
| `cli.py` | `kilonova run` / `kilonova dump`. |
|
|
38
|
+
|
|
39
|
+
Design decisions
|
|
40
|
+
----------------
|
|
41
|
+
|
|
42
|
+
- **asyncua 2.x, async-first.** Reads of source variables await device coroutines *inside*
|
|
43
|
+
the read transaction (asyncua PreRead callback); calculated variables recompute inside the
|
|
44
|
+
write that changed an input. The 2021 sync prototype could do neither.
|
|
45
|
+
- **No copy-paste of DesignInspector.** The design layer is clean-room and typed; drift
|
|
46
|
+
against quasar is caught by parsing quasar's whole CI corpus in the test suite.
|
|
47
|
+
- **Parity rules live in one place** (`address_space.py`): DataType is concrete only for
|
|
48
|
+
`nullPolicy="nullForbidden"` (else BaseDataType), ValueRank −1/1/−3 (scalar/array/UaVariant),
|
|
49
|
+
AccessLevel from `addressSpaceWrite` or source read/write modes.
|
|
50
|
+
- **Handlers are late-bound by address.** Registration works before or after `start()`;
|
|
51
|
+
unregistered methods/writes answer proper OPC UA status codes.
|
|
52
|
+
- **One asyncua override**, documented in `server.py`: BaseDataType variables accept any
|
|
53
|
+
concrete value type (OPC UA Part 3 semantics; upstream FIXME).
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Device logic
|
|
2
|
+
|
|
3
|
+
What is this?
|
|
4
|
+
-------------
|
|
5
|
+
|
|
6
|
+
In C++ quasar you implement `D<Class>` methods. In kilonova there is no generated
|
|
7
|
+
skeleton: you register plain async Python functions by dotted address. Everything below is
|
|
8
|
+
the entire user API.
|
|
9
|
+
|
|
10
|
+
Objects and cache variables
|
|
11
|
+
---------------------------
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
server = Server("Design.xml", config_path="config.xml")
|
|
15
|
+
await server.start() # or: async with server:
|
|
16
|
+
|
|
17
|
+
sca1 = server.objects["sca1"] # dotted addresses, as in the address space
|
|
18
|
+
await sca1.setOnline(42) # generated setter (quasar naming: setMyVar)
|
|
19
|
+
await sca1.set_cv("online", 42) # same thing, explicit
|
|
20
|
+
await sca1.set_cv("online", 0, status=ua.StatusCodes.Bad) # value + status
|
|
21
|
+
value = await sca1.get_cv("online") # None while status is bad
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`set_cv` raises on refused writes (type mismatch, null into `nullForbidden`) and on
|
|
25
|
+
integer range violations — no silent drops.
|
|
26
|
+
|
|
27
|
+
Methods
|
|
28
|
+
-------
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
@server.method("sca1.scale")
|
|
32
|
+
async def scale(obj, factor): # obj is the owning QuasarObject
|
|
33
|
+
return factor * 2.0 # mapped to the Design's return values
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
- Argument counts are validated (`BadArgumentsMissing` / `BadTooManyArguments`).
|
|
37
|
+
- Unregistered methods answer `BadNotImplemented`.
|
|
38
|
+
- Multiple return values: return a tuple. A single array return value is never unpacked.
|
|
39
|
+
|
|
40
|
+
Source variables
|
|
41
|
+
----------------
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
@server.read("sca1.adc") # addressSpaceRead: runs INSIDE the client read
|
|
45
|
+
async def read_adc(obj):
|
|
46
|
+
return await hw.read() # value
|
|
47
|
+
# or: return value, ua.StatusCodes.Good
|
|
48
|
+
# or: return ua.DataValue(...)
|
|
49
|
+
|
|
50
|
+
@server.write("sca1.dac") # addressSpaceWrite
|
|
51
|
+
async def write_dac(obj, value):
|
|
52
|
+
if not 0 <= value <= 10:
|
|
53
|
+
raise ua.UaStatusCodeError(ua.StatusCodes.BadOutOfRange) # client gets this
|
|
54
|
+
await hw.write(value)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Until the first interaction a source variable serves `BadWaitingForInitialData`.
|
|
58
|
+
|
|
59
|
+
Delegated cache variables
|
|
60
|
+
-------------------------
|
|
61
|
+
|
|
62
|
+
`addressSpaceWrite="delegated"` cache variables use the same `@server.write` decorator: the
|
|
63
|
+
handler runs before the value is stored, its exception status is returned to the client,
|
|
64
|
+
and an unregistered delegated write answers `BadNotImplemented`. Server-side `set_cv` never
|
|
65
|
+
triggers the handler (it *is* device logic).
|
|
66
|
+
|
|
67
|
+
Calculated variables and free variables
|
|
68
|
+
---------------------------------------
|
|
69
|
+
|
|
70
|
+
Config-level, exactly as in C++ quasar:
|
|
71
|
+
|
|
72
|
+
```xml
|
|
73
|
+
<FreeVariable name="fv" type="Double" initialValue="5"/>
|
|
74
|
+
<CalculatedVariable name="sum" value="$thisObjectAddress.fv + 7"/>
|
|
75
|
+
<CalculatedVariableGenericFormula name="Doubled" formula="$thisObjectAddress.fv * 2"/>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Formulas are compiled to a whitelisted AST (numbers, addresses, `+ - * / % ^`) — never
|
|
79
|
+
`eval`. Dependents recompute inside the write that changed an input; null/bad inputs
|
|
80
|
+
propagate `BadWaitingForInitialData`.
|
|
81
|
+
|
|
82
|
+
Logging
|
|
83
|
+
-------
|
|
84
|
+
|
|
85
|
+
Standard Python `logging`, loggers `kilonova.*`. The StandardMetaData log-level nodes
|
|
86
|
+
(`TRC/DBG/INF/WRN/ERR`) set those loggers at runtime, like LogIt on a C++ server; a config
|
|
87
|
+
`<StandardMetaData>` section sets initial levels.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Parity
|
|
2
|
+
|
|
3
|
+
What is this?
|
|
4
|
+
-------------
|
|
5
|
+
|
|
6
|
+
kilonova's definition of done: serve the address space the C++ quasar framework would,
|
|
7
|
+
verified — never claimed. Two gates exist; both read the server through a real OPC UA
|
|
8
|
+
client connection.
|
|
9
|
+
|
|
10
|
+
Gate 1: quasar's own CI oracle
|
|
11
|
+
------------------------------
|
|
12
|
+
|
|
13
|
+
`tests/conformance` loads `quasar/.CI/test_cases/manifest.json`, serves each case's
|
|
14
|
+
Design + config in-process, dumps the address space (uasak_dump-compatible NodeSet2) and
|
|
15
|
+
compares it against the case's `reference_ns2.xml`:
|
|
16
|
+
|
|
17
|
+
1. Have a quasar checkout next door (or set `KILONOVA_QUASAR_ROOT`).
|
|
18
|
+
1. `uv run pytest tests/conformance -v`
|
|
19
|
+
|
|
20
|
+
Comparison semantics (same as quasar's NodeSetTools): every `UAObject`/`UAVariable`/
|
|
21
|
+
`UAMethod` NodeId in the reference must exist exactly once in the dump, with every
|
|
22
|
+
reference attribute (`BrowseName`, `DataType`, `ValueRank`, `AccessLevel`) equal.
|
|
23
|
+
`StandardMetaData` is ignored except for the `default_design` case, whose reference *is*
|
|
24
|
+
the meta oracle (quasar's own CI ignores it everywhere; the checked-in references carry
|
|
25
|
+
mutually contradictory meta snapshots).
|
|
26
|
+
|
|
27
|
+
Current status: **12/12 cases PASS.**
|
|
28
|
+
|
|
29
|
+
Gate 2: live production servers
|
|
30
|
+
-------------------------------
|
|
31
|
+
|
|
32
|
+
The `.parity-night` campaign compares probes of real servers. kilonova runs as a third
|
|
33
|
+
backend column, no docker/build:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
bash .parity-night/scripts/run_kilonova_cell.sh <cell> <server-src> <config.xml> <port>
|
|
37
|
+
python3 .parity-night/scripts/compare.py cells/<a>/probe.json cells/<b>/probe.json
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Results (2026-07-11): ATCA and CAEN at full structural parity vs both live C++ backends;
|
|
41
|
+
CanOpen surfaced two genuine cross-backend deltas (method-argument AccessLevel: kilonova
|
|
42
|
+
agrees with UASDK; FreeVariable writability: kilonova agrees with open62541).
|
|
43
|
+
|
|
44
|
+
Expected, accepted differences
|
|
45
|
+
------------------------------
|
|
46
|
+
|
|
47
|
+
- Source variables report `err:BadWaitingForInitialData` where C++ device logic answers —
|
|
48
|
+
a kilonova cell has no device logic.
|
|
49
|
+
- LogIt components in StandardMetaData reflect each build (`mule`, `ThreadPool`,
|
|
50
|
+
`open62541` on C++; kilonova serves its own subsystems).
|
|
51
|
+
- Design-mandated children are instantiated unconditionally (C++ device logic may skip
|
|
52
|
+
some at runtime).
|
|
53
|
+
|
|
54
|
+
Ecosystem cross-checks
|
|
55
|
+
----------------------
|
|
56
|
+
|
|
57
|
+
- Cacophony: `tools/cacophony_crosscheck.py` verifies every periphery address the
|
|
58
|
+
generated `configParser.ctl` would assign resolves on a live kilonova —
|
|
59
|
+
600/600 on the production ATCA design.
|
|
60
|
+
- UaoForQuasar: client classes generate cleanly from production designs; the generated
|
|
61
|
+
NodeId construction is exactly kilonova's addressing. Compile/run of the C++ client
|
|
62
|
+
needs UASDK (docker) — pending.
|