rtems-proxy 2.1.0__tar.gz → 3.0.0b6__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 (69) hide show
  1. rtems_proxy-3.0.0b6/.claude/hooks/sandbox-check.sh +43 -0
  2. rtems_proxy-3.0.0b6/.claude/settings.json +18 -0
  3. rtems_proxy-3.0.0b6/.claude/skills/rtems-hybrid-debug/SKILL.md +374 -0
  4. rtems_proxy-3.0.0b6/.claude/statusline-command.sh +57 -0
  5. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/.copier-answers.yml +1 -1
  6. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/.devcontainer/devcontainer.json +15 -0
  7. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/.github/CONTRIBUTING.md +1 -1
  8. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/.github/workflows/_dist.yml +7 -0
  9. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/.github/workflows/ci.yml +2 -3
  10. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/.gitignore +6 -0
  11. rtems_proxy-3.0.0b6/.gitmodules +3 -0
  12. rtems_proxy-3.0.0b6/Dockerfile +63 -0
  13. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/PKG-INFO +10 -5
  14. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/README.md +5 -0
  15. rtems_proxy-3.0.0b6/docs/example-bl19i-va-ioc-01.md +337 -0
  16. rtems_proxy-3.0.0b6/docs/hybrid.md +326 -0
  17. rtems_proxy-3.0.0b6/docs/overview.md +107 -0
  18. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/pyproject.toml +6 -6
  19. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/requirements.txt +1 -1
  20. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/src/rtems_proxy/__main__.py +87 -39
  21. rtems_proxy-3.0.0b6/src/rtems_proxy/_version.py +24 -0
  22. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/src/rtems_proxy/configure.py +11 -12
  23. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/src/rtems_proxy/copy.py +17 -14
  24. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/src/rtems_proxy/globals.py +61 -37
  25. rtems_proxy-3.0.0b6/src/rtems_proxy/hybrid.py +283 -0
  26. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/src/rtems_proxy/rsync.sh.jinja +8 -10
  27. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/src/rtems_proxy.egg-info/PKG-INFO +10 -5
  28. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/src/rtems_proxy.egg-info/SOURCES.txt +14 -3
  29. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/src/rtems_proxy.egg-info/requires.txt +2 -1
  30. rtems_proxy-3.0.0b6/tests/test_globals.py +39 -0
  31. rtems_proxy-3.0.0b6/update-schema +16 -0
  32. rtems_proxy-3.0.0b6/uv.lock +1019 -0
  33. rtems_proxy-3.0.0b6/vacuumSpace-fix.patch +175 -0
  34. rtems_proxy-3.0.0b6/vacuumSpace.ibek.support.yaml.fixed +823 -0
  35. rtems_proxy-2.1.0/.python-version +0 -1
  36. rtems_proxy-2.1.0/Dockerfile +0 -46
  37. rtems_proxy-2.1.0/rtems-proxy.code-workspace +0 -14
  38. rtems_proxy-2.1.0/src/rtems_proxy/_version.py +0 -34
  39. rtems_proxy-2.1.0/uv.lock +0 -1022
  40. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  41. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/.github/ISSUE_TEMPLATE/issue.md +0 -0
  42. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md +0 -0
  43. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/.github/copilot-instructions.md +0 -0
  44. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/.github/pages/index.html +0 -0
  45. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/.github/pages/make_switcher.py +0 -0
  46. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/.github/workflows/_container.yml +0 -0
  47. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/.github/workflows/_pypi.yml +0 -0
  48. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/.github/workflows/_release.yml +0 -0
  49. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/.github/workflows/_test.yml +0 -0
  50. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/.github/workflows/_tox.yml +0 -0
  51. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/.gitleaks.toml +0 -0
  52. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/.pre-commit-config.yaml +0 -0
  53. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/.vscode/extensions.json +0 -0
  54. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/.vscode/launch.json +0 -0
  55. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/.vscode/settings.json +0 -0
  56. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/.vscode/tasks.json +0 -0
  57. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/LICENSE +0 -0
  58. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/renovate.json +0 -0
  59. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/setup.cfg +0 -0
  60. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/src/rtems_proxy/__init__.py +0 -0
  61. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/src/rtems_proxy/connect.py +0 -0
  62. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/src/rtems_proxy/telnet.py +0 -0
  63. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/src/rtems_proxy/trace.py +0 -0
  64. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/src/rtems_proxy/utils.py +0 -0
  65. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/src/rtems_proxy.egg-info/dependency_links.txt +0 -0
  66. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/src/rtems_proxy.egg-info/entry_points.txt +0 -0
  67. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/src/rtems_proxy.egg-info/top_level.txt +0 -0
  68. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/tests/conftest.py +0 -0
  69. {rtems_proxy-2.1.0 → rtems_proxy-3.0.0b6}/tests/test_cli.py +0 -0
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env bash
2
+ # UserPromptSubmit hook. Verifies the Claude sandbox is intact before
3
+ # every prompt. Exit 2 blocks the prompt and surfaces the message.
4
+ #
5
+ # Belt-and-suspenders against the "user invoked Claude via a non-shadow
6
+ # path" bypass — the bwrap launcher sets IS_SANDBOX=1, so an unset
7
+ # value means we are not in the sandbox.
8
+ #
9
+ # Skip the gate on Claude Code Web (CLAUDE_CODE_REMOTE=true). The
10
+ # hosted runtime is already sandboxed by Anthropic, and the local
11
+ # positive assertions below (GIT_CONFIG_GLOBAL=/etc/claude-gitconfig,
12
+ # GIT_CONFIG_SYSTEM=/dev/null) only hold inside our bwrap shadow.
13
+
14
+ fail() { echo "BLOCKED: $1" >&2; exit 2; }
15
+
16
+ [ "${CLAUDE_CODE_REMOTE:-}" = "true" ] && exit 0
17
+
18
+ [ "${IS_SANDBOX:-}" = "1" ] || \
19
+ fail "IS_SANDBOX unset — Claude was launched outside the bwrap shadow. Run via /usr/local/bin/claude."
20
+
21
+ # Strict-under-/root: the host gitconfig must NOT be readable.
22
+ [ ! -e "$HOME/.gitconfig" ] || ! [ -s "$HOME/.gitconfig" ] || \
23
+ fail "$HOME/.gitconfig is reachable — strict-under-/root inversion broken or the file mask regressed."
24
+
25
+ # Env scrub: tokens that may have been on the host shell must be empty.
26
+ [ -z "${GH_TOKEN:-}" ] || fail "GH_TOKEN is set inside the sandbox — --clearenv allowlist regressed."
27
+ [ -z "${GITHUB_TOKEN:-}" ] || fail "GITHUB_TOKEN is set inside the sandbox — --clearenv allowlist regressed."
28
+ [ -z "${ANTHROPIC_API_KEY:-}" ] || fail "ANTHROPIC_API_KEY is set inside the sandbox — --clearenv allowlist regressed."
29
+ [ -z "${SSH_AUTH_SOCK:-}" ] || fail "SSH_AUTH_SOCK is set inside the sandbox — --clearenv allowlist regressed."
30
+ [ -z "${DISPLAY:-}" ] || fail "DISPLAY is set inside the sandbox — --clearenv allowlist regressed."
31
+
32
+ # Curated gitconfig steering.
33
+ [ "${GIT_CONFIG_GLOBAL:-}" = "/etc/claude-gitconfig" ] || \
34
+ fail "GIT_CONFIG_GLOBAL is '${GIT_CONFIG_GLOBAL:-<unset>}', not /etc/claude-gitconfig — git would fall back to the host gitconfig."
35
+ [ "${GIT_CONFIG_SYSTEM:-}" = "/dev/null" ] || \
36
+ fail "GIT_CONFIG_SYSTEM is '${GIT_CONFIG_SYSTEM:-<unset>}', not /dev/null — git would read the host /etc/gitconfig."
37
+
38
+ # /run/secrets must be empty.
39
+ if [ -d /run/secrets ] && [ -n "$(ls -A /run/secrets 2>/dev/null)" ]; then
40
+ fail "/run/secrets is non-empty — Docker/Compose secrets are reachable. tmpfs mask regressed."
41
+ fi
42
+
43
+ exit 0
@@ -0,0 +1,18 @@
1
+ {
2
+ "hooks": {
3
+ "UserPromptSubmit": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "type": "command",
8
+ "command": ".claude/hooks/sandbox-check.sh"
9
+ }
10
+ ]
11
+ }
12
+ ]
13
+ },
14
+ "statusLine": {
15
+ "type": "command",
16
+ "command": ".claude/statusline-command.sh"
17
+ }
18
+ }
@@ -0,0 +1,374 @@
1
+ ---
2
+ name: rtems-hybrid-debug
3
+ description: >-
4
+ Debugging recipes for hybrid RTEMS5 IOCs driven by rtems-proxy — inspecting
5
+ cross-compiled IOC binary symbols from a Linux box, the ibek st.cmd <-> NFS
6
+ layout contract, and telling "not linked" apart from "not iocsh-registered".
7
+ Use when an RTEMS IOC boots but a st.cmd command is "not found", PVs are
8
+ missing, or boot paths look wrong.
9
+ ---
10
+
11
+ # Debugging hybrid RTEMS5 IOCs
12
+
13
+ Context: rtems-proxy generates an IOC's runtime (ibek + msi), places it on
14
+ NFS/TFTP, and drives a VME crate via motBoot. The crate NFS-mounts its per-IOC
15
+ export at `/epics` and TFTP-boots a PowerPC `.boot` image. Most "it boots but
16
+ doesn't work" failures are in the **Generic IOC build** or the
17
+ **path/layout contract**, not the instance `ioc.yaml`.
18
+
19
+ ## Inspecting symbols in the RTEMS IOC binary (from any Linux box)
20
+
21
+ The build tree (`.../ioc/<gen-ioc>/bin/RTEMS-beatnik/`) holds two files:
22
+
23
+ - `<IOC>` (no extension, large, e.g. 31 MB) — **unstripped ELF, has `.symtab`**.
24
+ This is the one to inspect.
25
+ - `<IOC>.boot` (small, e.g. 3.9 MB) — stripped boot image TFTP'd to the crate.
26
+ No symbols.
27
+
28
+ Stock RHEL8 `binutils` reads the cross-compiled `ELF32 PowerPC` image fine —
29
+ `readelf`/`nm` symbol listing is target-independent, so **no PowerPC
30
+ cross-toolchain is needed** (only `objdump -d` disassembly would need it).
31
+ `file` may be absent on DLS boxes; use `readelf -h` to confirm arch.
32
+
33
+ ```bash
34
+ F=.../bin/RTEMS-beatnik/<IOC> # the no-extension ELF, NOT .boot
35
+ readelf -h "$F" | grep -E 'Class|Machine' # ELF32 / PowerPC
36
+ nm "$F" | grep -i <symbol> # is it linked at all?
37
+ nm "$F" | grep ' [Tt] ' # all defined functions
38
+ ```
39
+
40
+ ## "not linked" vs "not iocsh-registered" — the key distinction
41
+
42
+ A st.cmd line like `DLS8516Configure(...)` failing with **"not found"** does
43
+ NOT necessarily mean the function is missing. An iocsh command needs more than
44
+ the function symbol — the module must also export an iocsh **registrar**.
45
+ Look for the registration machinery EPICS generates per command:
46
+
47
+ - `T <Func>` — the C function (callable from code)
48
+ - `t <Func>CallFunc`, `d <Func>FuncDef`, `d <Func>InitArg*` — the iocsh wrapper
49
+ - `t <Func>Register` + `D pvar_func_<Func>Register` — the `epicsExportRegistrar`
50
+
51
+ Three outcomes:
52
+ 1. **Function symbol absent** → module not linked into the IOC. Fix: add the
53
+ lib (+ its dbd) to the Generic IOC build. (e.g. pvlogging's
54
+ `set_logging_enable`/`set_max_array_length` were 0 matches = not linked.)
55
+ 2. **Function present but no `pvar_func_<Func>Register`** → linked but never
56
+ wrapped as an iocsh command. Fix: add the iocsh registration in the support
57
+ module C source + `registrar(...)` in its `.dbd`, then rebuild. (e.g.
58
+ `DLS8516Configure` was `T` but had no `...ConfigureRegister`, while its
59
+ sibling `DLS8516Display` did — a copy-paste omission, betrayed by mashed
60
+ `DLS85158516*` symbol names.)
61
+ 3. **Registrar present but command still not found** → the module `.dbd` isn't
62
+ in the loaded `ioc.dbd` (composition problem). Compare with a sibling
63
+ command that works.
64
+
65
+ Diff a working sibling against the broken one (`nm "$F" | grep -i 8515` vs
66
+ `8516`) to localise which of the three it is.
67
+
68
+ ## ibek st.cmd <-> NFS layout contract
69
+
70
+ ibek's `st.cmd.jinja` renders fixed paths the crate reads after mounting its
71
+ export at `/epics`:
72
+
73
+ ```
74
+ cd "{{ get_env('IOC') }}" -> /epics/ioc
75
+ dbLoadDatabase dbd/ioc.dbd -> /epics/ioc/dbd/ioc.dbd
76
+ STREAM_PROTOCOL_PATH /epics/runtime/protocol/
77
+ set_requestfile_path("/epics", "runtime") -> autosave *.req
78
+ dbLoadRecords {{ get_env('RUNTIME_DIR') }}/ioc.db
79
+ ```
80
+
81
+ `get_env` has **no default** — an unset env var renders empty. So:
82
+
83
+ - **Foot-gun:** if `RUNTIME_DIR` is unset when ibek runs, `dbLoadRecords`
84
+ becomes `/ioc.db` (wrong). rtems-proxy must pass `RUNTIME_DIR=/epics/runtime`
85
+ to the ibek subprocess (`hybrid.py:_run_ibek_generate`).
86
+ - The NFS export must therefore be laid out as **`runtime/`** (st.cmd, ioc.db,
87
+ protocol/, autosave *.req) and **`ioc/dbd/`** — not flat. `_copy_to_nfs`
88
+ builds exactly these two subfolders.
89
+ - Autosave `*.req` aggregates (`autosave_settings.req`/`autosave_positions.req`)
90
+ are emitted by ibek into the runtime dir only if a loaded template has a
91
+ matching `<stem>_settings.req`/`_positions.req` under `/epics/support/**`;
92
+ none match -> none emitted (not an error).
93
+
94
+ When changing the runtime files on a real crate, the NFS export must be
95
+ emptied and re-copied (stale flat files — e.g. an old st.cmd pointing at the
96
+ retired `/epics_rtems_root` mount — will otherwise be booted).
97
+
98
+ ## ibek substitution foot-gun: shared `.template` -> positional row misalignment
99
+
100
+ ibek (`render_db.py`) merges **every** `databases:` entry across **all** entities
101
+ that points at the **same** `.template` path into **one** msi `pattern { … }`
102
+ block. The header is taken from the arg-key order of the **first** entity to
103
+ reference that file; every other entity's row values are then appended **in that
104
+ entity's own arg order — ibek never re-keys by name**. So if two different
105
+ `entity_model`s instantiate the same template with args in different order, the
106
+ second's values land in the **wrong columns**, silently.
107
+
108
+ - **Symptoms (at `dbLoadRecords` of the expanded `ioc.db`):**
109
+ `Can't set "<rec>.<FIELD>" to "<value>": Illegal choice` / `No digits to
110
+ convert` (a string value landed in a menu/numeric field), and garbage record
111
+ names like `4:SEQCCHV` (a delay/number value landed in the `device` column).
112
+ - **Diagnose:** regenerate the subst to a scratch dir
113
+ (`ibek runtime generate --no-pvi <ioc.yaml> /epics/ibek-defs/*.yaml -o /tmp/x`;
114
+ exclude a symlinked def and pass a writable copy if you need to edit one),
115
+ then for each `file "…template" {` block compare the `pattern { … }` header to
116
+ the offending rows. A quick check: every value row must have the **same column
117
+ count** as the header (regex `"([^"]*)"` per row); a count mismatch = guaranteed
118
+ break, and equal counts can still be **semantically** swapped (e.g. SELM↔gauge).
119
+ - **Fix:** make the wrapper model list that template's args in the **exact** order
120
+ of the owning module's own `entity_model`. When the owning model uses
121
+ `args: { .*: }` (regex = all params), the header is the entity's **full**
122
+ param list **including ibek's injected `type, entity_enabled` prefix** — the
123
+ wrapper must reproduce those two leading (ignored) columns too.
124
+
125
+ ### Companion foot-gun: re-instantiating a group template needs a unique device
126
+
127
+ A "wrapper" model (e.g. vacuumSpace `space`/`space_b`) that re-instantiates
128
+ another module's group template (`mks937aGaugeGroup`, `digitelMpcIonpGroup`,
129
+ `dlsPLC_vacValveGroup`, …) must give each group a **unique device**, or the
130
+ group's `$(device):PLOG/:P/:STA/…` records collide with the wrapper's own
131
+ `space.template` records → `Record "…:PLOG" of type sel redefined with new type
132
+ calc` + `dbRecordHead: tempList not empty`. The DLS builder convention
133
+ (`vacuumSpace/etc/builder.py::_make_groups`) is `device = $(device):<COMP>G`
134
+ (`GAUGEG`/`IONPG`/`IMGG`/`PIRGG`/`VALVEG`), and `space.template`'s
135
+ `gauge`/`ionp`/… macros then point at those group devices (`{{ device }}:GAUGEG`)
136
+ so the top-level space reads the group's combined output. The builder only makes
137
+ a group when ≥2 of a component exist; an ibek model can't express that
138
+ conditional, so the pragmatic equivalent is **always** make the group (padding
139
+ to 8 with the first device) — collision-free and correct, at the cost of extra
140
+ internal `:<COMP>G` PVs for single-device components.
141
+
142
+ ## Generic-IOC top `Makefile` foot-guns (`.../ioc/<gen-ioc>/Makefile`)
143
+
144
+ The top-level Makefile of a DLS generic IOC (e.g. `bl-va-ioc-01`) generates
145
+ `data/msi.vars` and stages StreamDevice protocol files into `data/` for the NFS
146
+ export. Three non-obvious traps, all hit June 2026:
147
+
148
+ - **`data/` is `.gitignore`d** (like `bin/dbd/db/lib`), so a fresh clone lacks
149
+ it. Any recipe that redirects into it (`> data/msi.vars`) dies on a clean
150
+ build with `/bin/sh: data/...: No such file or directory`. Every such target
151
+ must `@mkdir -p $(@D)` (or `mkdir -p data`) as its first recipe line. Latent
152
+ until you build a fresh clone — an old clone where `data/` already exists
153
+ masks it.
154
+ - **Top-level `DATA += ...` is DEAD.** EPICS's `DATA`/`buildInstall` install
155
+ mechanism only fires inside **App** dirs, **never at TOP**. A top Makefile that
156
+ does `DATA += $(all_protos)` (protos gathered from `$(SYS_EDM_PATHS)`, i.e.
157
+ every module's `data/*.proto*`) installs **nothing** → `data/` ends up with
158
+ only `msi.vars` → rtems-proxy's `hybrid.py` glob `data/*.proto*` finds nothing
159
+ → the NFS `runtime/protocol/` folder is created but **empty**. Fix: an explicit
160
+ `protocols` target hooked on `all` that copies the protos itself:
161
+ ```makefile
162
+ all: submodules protocols data/msi.vars
163
+ protocols:
164
+ @mkdir -p data
165
+ @for f in $(all_protos); do install -m 644 "$$f" data/; done
166
+ ```
167
+ Use `install -m 644`, **not `cp`**: prod-sourced protos are mode `555`
168
+ (read-only), so a second build's `cp` hits `Permission denied: cannot create
169
+ regular file 'data/x.protocol'` trying to overwrite the read-only dest.
170
+ `install` unlinks+recreates and pins a deterministic world-readable+writable
171
+ mode (what the NFS root-squash export needs; see the dbd-perms section).
172
+ - **Submodule lazy-init.** `ibek-support` / `ibek-support-dls` are git
173
+ submodules, empty after a fresh clone, and the `configure/CONFIG` build umask
174
+ does NOT reach them. Hook a `submodules` target on `all` that inits **only the
175
+ un-checked-out ones** — a blanket `git submodule update --init` would detach an
176
+ already-populated submodule to its recorded SHA and **orphan local branch
177
+ work** in it:
178
+ ```makefile
179
+ submodules:
180
+ @git submodule status | awk '/^-/ { print $$2 }' | while read p; do \
181
+ git submodule update --init --recursive "$$p"; done
182
+ ```
183
+ (`git submodule status` prefixes an un-initialised submodule with `-`.)
184
+ Running inside a build recipe also gives the checkout the build umask (0022) →
185
+ world-traversable, fixing the otherwise-unreachable submodule perms.
186
+
187
+ ## DLS8515/8516 serial: "port connects but device gives No reply"
188
+
189
+ Distinct from "could not connect" (missing `/dev/ttyNNN` node — see the
190
+ `DLS8516Configure` registrar story). `No reply within 1000 ms` from StreamDevice
191
+ means the asyn port opened fine but the **line parameters are wrong** (baud,
192
+ data bits, parity, stop) so framing is garbled and nothing comes back.
193
+
194
+ Card → port → module map (this IOC): cards configure as `ty_<card>_<chan>`.
195
+ `DLS8515Configure(40,…)`→`ty_40_*`, `(41,…)`→`ty_41_*` are **8515**;
196
+ `DLS8516Configure(42,…)`→`ty_42_*` is **8516**. Both `*Configure` funcs call the
197
+ same `DLS85158516Configure()` and pre-config every channel to **9600 8N2**
198
+ (`drvDLS8515-RTEMS.c`; legacy `drvDLS8515.c` identical) — so the driver default
199
+ is NOT what separates a working card from a failing one.
200
+
201
+ Diagnostic: correlate working vs failing ports against `asynSetOption` lines in
202
+ the generated st.cmd (`/ioc_nfs/runtime/st.cmd`). DLS devices commonly run
203
+ **7E2** (bits 7, parity Even, stop 2), NOT the driver's 8N2 default — so a
204
+ channel that relies on the default talks to a 7E2 device at 8N2 and gets no
205
+ reply. Ports with explicit settings in `ioc.yaml` get `asynSetOption` and work;
206
+ ports without get the wrong framing and fail.
207
+
208
+ - **Watch the emit gating:** a channel template may only emit `asynSetOption`
209
+ when `baud:` is present, silently dropping `parity:`/`stop:` set without a
210
+ baud. Diff `ioc.yaml` channel params against the actual `asynSetOption` lines
211
+ in st.cmd — a channel whose `parity: E` never appears in st.cmd is the smoking
212
+ gun. The fix is per-channel serial settings in `ioc.yaml` (recovered from the
213
+ original VxWorks/XmlBuilder build), and/or a template fix to emit asynSetOption
214
+ for parity/stop/bits independent of baud.
215
+
216
+ - **Mapped-enum vs template mismatch (the parity bug, fixed June 2026):** ibek's
217
+ `ioc_factory.py::fixup_enums` renders an enum param differently depending on
218
+ whether its `values:` are MAPPED. `{E: even, O: odd, N: none}` → the param
219
+ renders as the *value* (`"even"`/`"odd"`/`"none"`); `{E:, O:, N:}` (null
220
+ values) → it renders as the *key* (`"E"`). DLS8515channel.parity is mapped, so
221
+ a template testing `parity == "E"` never matched and **no parity asynSetOption
222
+ was emitted** — every 7E2 gauge ran 7N2, framing errors, "No reply within
223
+ 1000 ms". Fix in `DLS8515.ibek.support.yaml`: make both channels' parity enum
224
+ mapped to asyn's literal values and have the template emit
225
+ `asynSetOption(...,"parity","{{parity}}")` gated on `parity != "none"`. Parity
226
+ was the only mapped enum and the only one that broke; baud/data/stop/flow are
227
+ unmapped and their keys are already the literal asyn values (7,2,H,S).
228
+
229
+ ## "findInterface asynInt32Type" on every FINS record — missing FINS port layer
230
+
231
+ DLS PLC records (`dlsPLC.vacValve`/`read100`/`interlock`/`temperature`, and the
232
+ `:ACTUALCON` ao, `:INTn:RESET` ao) use `DTYP=asynInt32` with
233
+ `@asyn(PORT, addr, 0) FINS_DM_READ`/`FINS_DM_WRITE`. The `asynInt32` interface
234
+ **and** the `FINS_DM_*` drvUser strings are provided by a **FINS device port**
235
+ created by `finsDEVInit(finsPort, serialPort)` (after
236
+ `HostlinkInterposeInit(serialPort)`) — NOT by the bare serial port. If st.cmd
237
+ only has `drvAsynSerialPortConfigure`/`asynSetOption` for `PORT` and no
238
+ `HostlinkInterposeInit`/`finsDEVInit`, the port is plain octet-only, so every
239
+ FINS record fails at init:
240
+
241
+ ```
242
+ <PV> devAsynInt32::initCommon findInterface asynInt32Type
243
+ recGblRecordError: ao: init_record Error (514,11) PV: <PV>
244
+ ```
245
+
246
+ These are non-fatal for iocInit (see the "boots but zero PVs" section — they do
247
+ not abort `iocBuild`), **but the affected PVs never talk to the PLC**, so it is
248
+ a real failure, not noise, when those PVs are the point of the IOC.
249
+
250
+ Root cause seen June 2026 (bl19i-va-ioc-01): the builder2ibek conversion dropped
251
+ the FINS port objects and set each dlsPLC entity's `port:` directly to the
252
+ **underlying serial port** (`ty_40_5`/`ty_41_0`/`ty_41_1`/`ty_41_7`). The
253
+ instance `ioc.yaml` had **zero** `FINS.FINSHostlink` entities. Confirm in two
254
+ greps:
255
+
256
+ ```bash
257
+ grep -ni 'FINS\|Hostlink' .../config/ioc.yaml # broken IOC: NONE
258
+ grep -nE 'HostlinkInterposeInit|finsDEVInit' /ioc_nfs/runtime/st.cmd # NONE
259
+ ```
260
+
261
+ A working sibling (bl15i-va-ioc-01.yaml) has one `FINS.FINSHostlink` per serial
262
+ port carrying FINS devices, and the dlsPLC entities point at the **FINS** name,
263
+ not the serial name:
264
+
265
+ ```yaml
266
+ - type: FINS.FINSHostlink
267
+ asyn_port: ty_42_0 # the serial port (asyn.AsynSerial name)
268
+ name: TMPCC1.Hostlink # the FINS port — MUST be a DISTINCT name
269
+ # ...
270
+ - type: dlsPLC.read100
271
+ port: TMPCC1.Hostlink # references the FINS port, NOT ty_42_0
272
+ ```
273
+
274
+ `FINS.FINSHostlink` (`FINS.ibek.support.yaml`) emits exactly:
275
+ `HostlinkInterposeInit("{{asyn_port}}")` then
276
+ `finsDEVInit("{{name}}", "{{asyn_port}}")`. `HostlinkInterposeInit` interposes on
277
+ the serial port **in place**; `finsDEVInit` creates a **new** asyn port on top —
278
+ hence the FINS name must differ from the serial name (asyn port names are
279
+ unique).
280
+
281
+ Fix (purely an instance `ioc.yaml` change — **no Generic-IOC rebuild**): for each
282
+ serial port that carries FINS devices, add a `FINS.FINSHostlink` (distinct
283
+ `name`, `asyn_port:` = the serial port) and repoint every dlsPLC `port:` from the
284
+ serial name to that FINS name, then regen. The FINS driver is already linked —
285
+ `ioc.dbd` has `registrar(finsDEVRegister)` **and**
286
+ `registrar(HostlinkInterposeRegister)`, so the iocsh commands exist; they were
287
+ just never called.
288
+
289
+ ## Regenerating & deploying the runtime (`msi` must be on PATH)
290
+
291
+ `rtems-proxy start` runs `ibek runtime generate2` then **`msi`** (EPICS macro
292
+ expansion of the .db) then copies `runtime/` + `ioc/` to the NFS export. `msi`
293
+ ships with epics-base but is **not on PATH by default** — without it the run
294
+ prints `msi: command not found` / `msi expansion failed` and stops before the
295
+ NFS copy. It lives at `/epics/epics-base/bin/linux-x86_64/msi`; this repo
296
+ symlinks it into `.venv/bin/` (already on PATH) so the subprocess finds it.
297
+ If that symlink is missing (e.g. venv rebuilt), recreate it or
298
+ `export PATH=/epics/epics-base/bin/linux-x86_64:$PATH` before the run.
299
+
300
+ Regen + verify a serial/template change (no crate connection needed):
301
+
302
+ ```bash
303
+ rtems-proxy start --hybrid --no-connect \
304
+ --instance /workspaces/i19-services/services/bl19i-va-ioc-01
305
+ # generate2 writes /epics/runtime/st.cmd; after msi the NFS copy lands at
306
+ # /ioc_nfs/runtime/st.cmd (this is the file the crate boots).
307
+ grep -n 'asynSetOption\|parity' /ioc_nfs/runtime/st.cmd
308
+ ```
309
+
310
+ Note `/epics/ibek-defs/<MODULE>.ibek.support.yaml` is a **symlink** into the
311
+ module's `ibek-support-*` tree (e.g.
312
+ `/dls_sw/work/R7.0.7/ioc/BL/bl-va-ioc-01/ibek-support-dls/DLS8515/...`), so
313
+ editing the module def is picked up by the next regen with no copy step. The
314
+ `IOC binary not found ... .boot` message at the end is expected under
315
+ `--no-connect` (nothing is built/booted) and does not affect the generated
316
+ st.cmd.
317
+
318
+ ## "boots but zero PVs" — a fatal iocInit step aborts before the CA server
319
+
320
+ `iocInit` runs `iocBuild` → `iocRun`. If any **`iocBuild`** step fails (e.g.
321
+ `iocBuild: asInit Failed.`), iocInit returns early and **never reaches
322
+ `iocRun`**, so the CA server never starts — the crate prompt still returns but
323
+ serves no PVs. When triaging a no-PV log, scan for a `*Build: ... Failed` /
324
+ fatal line near `iocInit`; everything above it (asyn `findInterface` failures,
325
+ `ao: init_record Error (514,11)`, `save_restore: Can't open file`, unregistered
326
+ iocsh commands) is **non-fatal noise** that does not stop PVs on its own.
327
+ (Non-fatal ≠ harmless: mass `findInterface asynInt32Type` failures mean a whole
328
+ class of PVs is dead — see the FINS port-layer section above.)
329
+
330
+ - **Foot-gun (pvlogging):** `_copy_to_nfs` stages only `runtime/` and
331
+ `ioc/dbd/` — nothing under `/epics/support/...`. The `pvlogging` module's
332
+ st.cmd line `asSetFilename /epics/support/pvlogging/src/access.acf` then
333
+ points at a file absent from the NFS export, `asInit` fails hard, iocInit
334
+ aborts, no PVs. Fix: **remove pvlogging from the instance `ioc.yaml`** and
335
+ re-gen runtime assets (confirmed fix, June 2026). Any support module that
336
+ needs an absolute `/epics/support/...` file at boot has the same problem —
337
+ the file must be copied into the NFS tree too, or the feature dropped.
338
+
339
+ ## "registerRecordDeviceDriver failed <every recordtype>" — unreadable dbd dir
340
+
341
+ A boot log where `dbLoadDatabase dbd/ioc.dbd` is immediately followed by
342
+ `registerRecordDeviceDriver failed` for *every* recordtype (aSub, ai, ao, bi …)
343
+ plus `registryJLinkAdd failed calc` means **pdbbase is empty** — the dbd never
344
+ loaded. It is NOT a dbd-content or binary problem:
345
+
346
+ - `registerRecordDeviceDriver failed X` (base `registryCommon.c`) fires only
347
+ when `dbFindRecordType(pdbbase,"X")` misses *and* `registryRecordTypeAdd`
348
+ already **succeeded** — so the binary's record support is linked fine; the
349
+ recordtype just isn't in pdbbase.
350
+ - The real error is one line up, easily missed because it has no newline:
351
+ `filename="…/dbStatic/dbLexRoutines.c" line number=NNN dbRead opening file
352
+ dbd/ioc.dbd`. That is `dbReadCOM` reporting **`dbOpenFile()` returned NULL**
353
+ → `goto cleanup` → nothing parsed. The crate **could not open the file**.
354
+
355
+ Root cause seen June 2026: the `ioc/dbd/` directory on the NFS export was mode
356
+ **0750** (`drwxr-x---`). The crate NFS-mounts the export as a root-squashed /
357
+ anonymous user, so it traverses `ioc/` (0755) but is denied entry to `dbd/` —
358
+ open fails. `runtime/st.cmd` loads fine precisely because `runtime/` is 0755.
359
+ The 0750 came from `_copy_to_nfs` doing `rsync -r` of the build-tree `dbd/`
360
+ (which is 0750), and `cp -a` then propagating it to the live export.
361
+
362
+ **Rule: every directory the crate reads under `/epics` must be world-traversable
363
+ (o+rx) and every file world-readable (o+r).** Confirm with
364
+ `namei -l /ioc_nfs/ioc/dbd/ioc.dbd` — any dir without world `x` breaks the boot.
365
+
366
+ - `_copy_to_nfs` now copies dbd with `rsync -r --chmod=D755,F644` plus an
367
+ explicit `chmod 0755` on `ioc/` and `ioc/dbd/` (the explicit chmod is needed
368
+ because with a `src/` trailing slash rsync does NOT re-perm the transfer's
369
+ **top** destination dir, only its contents).
370
+ - Manual one-shot repair on the live export:
371
+ `chmod -R a+rX /dls_sw/<bl>/epics/rtems/<ioc>` (capital `X` = add traverse to
372
+ dirs only, never makes data files executable), then reboot.
373
+ - The deploy step is `cp -a /ioc_nfs/. /dls_sw/<bl>/epics/rtems/<ioc>/` — `cp -a`
374
+ **preserves** perms, so fixing `/ioc_nfs` then re-deploying carries the fix.
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env bash
2
+ # Claude Code status line: model + context usage.
3
+ #
4
+ # Reads Claude's JSON status payload from stdin and prints a colored
5
+ # one-liner: username · model · cwd · ctx · cost. Uses jq for JSON
6
+ # parsing so no python is needed — works fine inside the bwrap sandbox
7
+ # where the host's python is masked off. If jq is missing, falls
8
+ # through to a bash-only degraded line.
9
+
10
+ input=$(cat)
11
+
12
+ degraded_line() {
13
+ local username cwd short_cwd
14
+ username=$(whoami 2>/dev/null || echo "?")
15
+ cwd=$(printf '%s' "$input" | sed -n 's/.*"current_dir"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
16
+ [ -z "$cwd" ] && cwd="$PWD"
17
+ short_cwd="${cwd/#$HOME/~}"
18
+ printf "\033[0;35m%s\033[0m \033[0;33m%s\033[0m \033[2;37m(no jq — degraded statusline)\033[0m" \
19
+ "$username" "$short_cwd"
20
+ }
21
+
22
+ command -v jq >/dev/null 2>&1 || { degraded_line; exit 0; }
23
+
24
+ # Single jq pass emits tab-separated fields so a malformed value can't
25
+ # bleed across columns. `// empty` returns empty strings rather than
26
+ # the literal "null"; cost defaults to 0 for the printf below.
27
+ IFS=$'\t' read -r model cwd used remaining cost < <(
28
+ printf '%s' "$input" | jq -r '
29
+ [
30
+ (.model.display_name // "unknown model"),
31
+ (.workspace.current_dir // .cwd // ""),
32
+ (.context_window.used_percentage // empty | tostring),
33
+ (.context_window.remaining_percentage // empty | tostring),
34
+ (.cost.total_cost_usd // 0 | tostring)
35
+ ] | @tsv
36
+ ' 2>/dev/null
37
+ ) || { degraded_line; exit 0; }
38
+
39
+ if [ -z "$model" ]; then
40
+ degraded_line
41
+ exit 0
42
+ fi
43
+
44
+ short_cwd="${cwd/#$HOME/~}"
45
+ username=$(whoami 2>/dev/null || echo "unknown")
46
+ cost_info=$(printf 'cost: $%.2f' "${cost:-0}")
47
+
48
+ if [ -n "$used" ] && [ -n "$remaining" ]; then
49
+ # printf %.0f rounds half-away-from-zero, matching the old
50
+ # int(round(...)) behaviour closely enough for a status line.
51
+ context_info=$(printf 'ctx: %.0f%% used / %.0f%% left' "$used" "$remaining")
52
+ else
53
+ context_info="ctx: new session"
54
+ fi
55
+
56
+ printf "\033[0;35m%s\033[0m \033[0;36m%s\033[0m \033[0;33m%s\033[0m \033[0;32m%s\033[0m \033[0;31m%s\033[0m" \
57
+ "$username" "$model" "$short_cwd" "$context_info" "$cost_info"
@@ -1,5 +1,5 @@
1
1
  # Changes here will be overwritten by Copier
2
- _commit: 5.0.1
2
+ _commit: 5.0.2
3
3
  _src_path: https://github.com/DiamondLightSource/python-copier-template
4
4
  author_email: giles.knap@diamond.ac.uk
5
5
  author_name: Giles Knap
@@ -64,6 +64,21 @@
64
64
  {
65
65
  "target": "/workspaces/${localWorkspaceFolderBasename}/.venv",
66
66
  "type": "volume"
67
+ },
68
+ // WARNING- remove these to work on this repo outside of DLS ///////////////////
69
+ // mount in /dls_sw readonly for testing hybrid mode
70
+ // slave propagation ensures autofs submounts on the host
71
+ // are visible inside the container without needing to trigger them from within it
72
+ {
73
+ "type": "bind",
74
+ "source": "/dls_sw",
75
+ "target": "/dls_sw,readonly,bind-propagation=slave"
76
+ },
77
+ // folders we would like to work on from here!
78
+ {
79
+ "source": "/dls_sw/work/R7.0.7/ioc/BL/",
80
+ "target": "/dls_sw/work/R7.0.7/ioc/BL/",
81
+ "type": "bind"
67
82
  }
68
83
  ],
69
84
  // Mount the parent as /workspaces so we can pip install peers as editable
@@ -24,4 +24,4 @@ It is recommended that developers use a [vscode devcontainer](https://code.visua
24
24
 
25
25
  This project was created using the [Diamond Light Source Copier Template](https://github.com/DiamondLightSource/python-copier-template) for Python projects.
26
26
 
27
- For more information on common tasks like setting up a developer environment, running the tests, and setting a pre-commit hook, see the template's [How-to guides](https://diamondlightsource.github.io/python-copier-template/5.0.1/how-to.html).
27
+ For more information on common tasks like setting up a developer environment, running the tests, and setting a pre-commit hook, see the template's [How-to guides](https://diamondlightsource.github.io/python-copier-template/5.0.2/how-to.html).
@@ -29,6 +29,13 @@ jobs:
29
29
  - name: Check for packaging errors
30
30
  run: uvx twine check --strict dist/*
31
31
 
32
+ - name: Setup Python
33
+ # the runner's default python may be older than requires-python in
34
+ # pyproject.toml; pin to a supported version for the install/run checks
35
+ uses: actions/setup-python@v5
36
+ with:
37
+ python-version: "3.13"
38
+
32
39
  - name: Install produced wheel
33
40
  run: python -m pip install dist/*.whl
34
41
 
@@ -5,11 +5,10 @@ on:
5
5
  branches:
6
6
  - main
7
7
  tags:
8
- - '*'
8
+ - "*"
9
9
  pull_request:
10
10
 
11
11
  jobs:
12
-
13
12
  lint:
14
13
  uses: ./.github/workflows/_tox.yml
15
14
  with:
@@ -19,7 +18,7 @@ jobs:
19
18
  strategy:
20
19
  matrix:
21
20
  runs-on: ["ubuntu-latest"] # can add windows-latest, macos-latest
22
- python-version: ["3.11", "3.12", "3.13"]
21
+ python-version: ["3.13", "3.14"]
23
22
  fail-fast: false
24
23
  uses: ./.github/workflows/_test.yml
25
24
  with:
@@ -69,3 +69,9 @@ venv*
69
69
  .ruff_cache/
70
70
 
71
71
  bl21i-pss-ioc-01.sh
72
+
73
+ # vscode workspaces
74
+ *.code-workspace
75
+
76
+ # claude local (per-user) settings
77
+ .claude/settings.local.json
@@ -0,0 +1,3 @@
1
+ [submodule "ibek-runtime-support-dls"]
2
+ path = ibek-runtime-support-dls
3
+ url = https://gitlab.diamond.ac.uk/controls/containers/utils/ibek-runtime-support-dls.git
@@ -0,0 +1,63 @@
1
+ # This container is bases on epics-base so that we have access to the msi tool
2
+ # and CA client tools for diagnostics.
3
+ FROM ghcr.io/epics-containers/epics-base-developer:7.0.10ec1 AS developer
4
+
5
+ # Add any system dependencies for the developer/build environment here
6
+ RUN apt-get update -y && apt-get install -y --no-install-recommends \
7
+ rsync \
8
+ telnet \
9
+ && apt-get dist-clean
10
+
11
+ # The build stage installs the context into the venv
12
+ FROM developer AS build
13
+
14
+ # Change the working directory to the `app` directory
15
+ # and copy in the project
16
+ WORKDIR /app
17
+ COPY . /app
18
+ RUN chmod o+wrX .
19
+
20
+ # Tell uv sync to install python in a known location so we can copy it out later
21
+ ENV UV_PYTHON_INSTALL_DIR=/python
22
+
23
+ # Sync the project without its dev dependencies
24
+ RUN --mount=type=cache,target=/root/.cache/uv \
25
+ uv sync --locked --no-editable --no-dev
26
+
27
+ ENV PATH=/app/.venv/bin:$PATH
28
+
29
+ # Create directories for hybrid mode compatibility in devcontainers
30
+ RUN mkdir -p /epics/ioc /epics/runtime /ioc_tftp /ioc_nfsv2
31
+
32
+ # rtems-proxy rewrites the ibek-defs symlink farm and generates runtime assets
33
+ # at start time. In the cluster it runs as a non-root user, so these dirs must
34
+ # be writable+traversable by all (the symlink unlink/create needs w+x on the
35
+ # dir, not just r). Mounted volumes (/ioc_nfs, /ioc_tftp) get write access from
36
+ # the pod securityContext, not here.
37
+ RUN mkdir -p /epics/ibek-defs /epics/runtime /epics/autosave \
38
+ && chmod a+rwx /epics/ibek-defs /epics/runtime /epics/autosave
39
+
40
+ # The runtime stage copies the built venv into a runtime container
41
+ FROM ghcr.io/epics-containers/epics-base-runtime:7.0.10ec1 AS runtime
42
+
43
+ # Add apt-get system dependencies for runtime here if needed
44
+ # RUN apt-get update -y && apt-get install -y --no-install-recommends \
45
+ # some-library \
46
+ # && apt-get dist-clean
47
+
48
+ # Copy the python installation from the build stage
49
+ COPY --from=build /python /python
50
+
51
+ # Copy the environment, but not the source code
52
+ COPY --from=build /app/.venv /app/.venv
53
+ ENV PATH=/app/.venv/bin:$PATH
54
+
55
+ # rtems-proxy rewrites the ibek-defs symlink farm and generates runtime assets
56
+ # at start time as a non-root user in the cluster, so these dirs must be
57
+ # writable+traversable by all (symlink unlink/create needs w+x on the dir).
58
+ RUN mkdir -p /epics/ibek-defs /epics/runtime \
59
+ && chmod a+rwx /epics/ibek-defs /epics/runtime /epics/autosave
60
+
61
+ # change this entrypoint if it is not the same as the repo
62
+ ENTRYPOINT ["rtems-proxy"]
63
+ CMD ["--version"]