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.
Files changed (43) hide show
  1. kilonova-0.1.0/.github/workflows/ci.yml +50 -0
  2. kilonova-0.1.0/.github/workflows/release.yml +30 -0
  3. kilonova-0.1.0/.gitignore +13 -0
  4. kilonova-0.1.0/LICENSE +25 -0
  5. kilonova-0.1.0/PKG-INFO +111 -0
  6. kilonova-0.1.0/PLAN.md +126 -0
  7. kilonova-0.1.0/README.md +85 -0
  8. kilonova-0.1.0/doc/Architecture.md +53 -0
  9. kilonova-0.1.0/doc/DeviceLogic.md +87 -0
  10. kilonova-0.1.0/doc/Parity.md +62 -0
  11. kilonova-0.1.0/examples/sca/Design.xml +43 -0
  12. kilonova-0.1.0/examples/sca/config.xml +13 -0
  13. kilonova-0.1.0/examples/sca/demo.py +33 -0
  14. kilonova-0.1.0/pyproject.toml +55 -0
  15. kilonova-0.1.0/src/kilonova/__init__.py +12 -0
  16. kilonova-0.1.0/src/kilonova/address_space.py +413 -0
  17. kilonova-0.1.0/src/kilonova/calculated.py +234 -0
  18. kilonova-0.1.0/src/kilonova/cli.py +58 -0
  19. kilonova-0.1.0/src/kilonova/config.py +222 -0
  20. kilonova-0.1.0/src/kilonova/design.py +390 -0
  21. kilonova-0.1.0/src/kilonova/dump.py +192 -0
  22. kilonova-0.1.0/src/kilonova/errors.py +13 -0
  23. kilonova-0.1.0/src/kilonova/meta.py +135 -0
  24. kilonova-0.1.0/src/kilonova/objects.py +89 -0
  25. kilonova-0.1.0/src/kilonova/oracle.py +162 -0
  26. kilonova-0.1.0/src/kilonova/server.py +389 -0
  27. kilonova-0.1.0/tests/__init__.py +0 -0
  28. kilonova-0.1.0/tests/conformance/__init__.py +0 -0
  29. kilonova-0.1.0/tests/conformance/test_conformance.py +77 -0
  30. kilonova-0.1.0/tests/conftest.py +45 -0
  31. kilonova-0.1.0/tests/data/Design.xml +43 -0
  32. kilonova-0.1.0/tests/data/config.xml +13 -0
  33. kilonova-0.1.0/tests/test_cache_variables.py +71 -0
  34. kilonova-0.1.0/tests/test_calculated_variables.py +74 -0
  35. kilonova-0.1.0/tests/test_design.py +79 -0
  36. kilonova-0.1.0/tests/test_instantiation.py +40 -0
  37. kilonova-0.1.0/tests/test_meta.py +50 -0
  38. kilonova-0.1.0/tests/test_methods.py +46 -0
  39. kilonova-0.1.0/tests/test_robustness.py +206 -0
  40. kilonova-0.1.0/tests/test_server_boot.py +33 -0
  41. kilonova-0.1.0/tests/test_setters.py +76 -0
  42. kilonova-0.1.0/tests/test_source_variables.py +124 -0
  43. 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
@@ -0,0 +1,13 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .mypy_cache/
11
+ uv.lock
12
+ dump*.xml
13
+ dist/
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.
@@ -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
+ [![CI](https://github.com/quasarnova-team/kilonova/actions/workflows/ci.yml/badge.svg)](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.
@@ -0,0 +1,85 @@
1
+ # kilonova
2
+
3
+ [![CI](https://github.com/quasarnova-team/kilonova/actions/workflows/ci.yml/badge.svg)](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.