Skip to content

Commit 0f24c5c

Browse files
EECSBclaude
andcommitted
Lab Equipment Controller
Discovers and controls lab instruments over Ethernet using SCPI: network discovery, per-instrument consoles, one- and multi-instrument scripting, waveform and screen capture, and 35 curated command catalogs (23,174 entries) transcribed from vendor programming guides by the bundled scpi-extract toolchain. Three front ends over one UI-free Core library on .NET 10: a WinForms app for Windows, `lec` for the terminal, and a Blazor client/server pair that puts the same bench in a browser with every socket on the server. Core carries no platform API, which is what keeps all three honest. 1,258 tests. The desktop app ships as a portable self-contained build and as an Inno Setup installer; the CLI publishes per RID; the web version builds into a container; and Core packs for NuGet as "LabEquipmentController", catalogs embedded, MIT licensed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
0 parents  commit 0f24c5c

250 files changed

Lines changed: 68854 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.dockerignore

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# The build context is the repository root, so keep out everything the image does not need.
2+
# Copying bin/obj in would also risk a host-built artefact shadowing the container's own.
3+
4+
**/bin/
5+
**/obj/
6+
**/.vs/
7+
**/.git/
8+
**/.github/
9+
**/node_modules/
10+
11+
# Vendor guides and the pipeline's intermediates: large, and not redistributable.
12+
datasheets/
13+
tools/scpi-extract/manuals/
14+
tools/scpi-extract/parsed/
15+
bench-reports/
16+
docs/images/
17+
18+
# The desktop app and the CLI are not part of this image.
19+
Tests/
20+
installer/
21+
22+
.env
23+
*.user
24+
*.suo

.env.example

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Copy to .env and fill in. .env is git-ignored; never commit a real key.
2+
#
3+
# Used only for development and testing of the AI datasheet-extraction feature.
4+
# End users do not need this file — they enter their own connection in the app,
5+
# which stores the key encrypted per-user rather than in a file.
6+
7+
GEMINI_API_KEY=
8+
ANTHROPIC_API_KEY=
9+
OPENAI_API_KEY=

.github/workflows/ci.yml

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# Build and test on all three platforms.
2+
#
3+
# The point of the matrix is not thoroughness for its own sake: this project claims the
4+
# CLI runs on Windows, Linux and macOS, and that claim was previously only ever checked by
5+
# cross-compiling on Windows and looking at the file header. Here it is actually run.
6+
#
7+
# The WinForms app is Windows-only and always will be, so the solution is built whole only
8+
# on Windows; elsewhere the portable projects are built directly, which is exactly what the
9+
# README tells a Linux contributor to do.
10+
name: CI
11+
12+
on:
13+
push:
14+
branches: [master]
15+
pull_request:
16+
workflow_dispatch:
17+
18+
permissions:
19+
contents: read
20+
21+
jobs:
22+
build-and-test:
23+
name: ${{ matrix.os }}
24+
runs-on: ${{ matrix.os }}
25+
strategy:
26+
# One platform failing should not hide the result on the other two.
27+
fail-fast: false
28+
matrix:
29+
os: [ubuntu-latest, windows-latest, macos-latest]
30+
31+
steps:
32+
- uses: actions/checkout@v7
33+
34+
- uses: actions/setup-dotnet@v6
35+
with:
36+
dotnet-version: '10.0.x'
37+
38+
- name: Build the whole solution (Windows)
39+
if: runner.os == 'Windows'
40+
run: dotnet build LabEquipmentController.slnx -c Release
41+
42+
- name: Build the portable projects (Linux, macOS)
43+
if: runner.os != 'Windows'
44+
run: |
45+
dotnet build Core/LabEquipmentController.Core.csproj -c Release
46+
dotnet build Cli/LabEquipmentController.Cli.csproj -c Release
47+
dotnet build Web/LabEquipmentController.Web/LabEquipmentController.Web.csproj -c Release
48+
49+
- name: Test
50+
run: dotnet test Tests/LabEquipmentController.Tests.csproj -c Release --logger "console;verbosity=normal"
51+
52+
# Running the CLI is the part that cannot be faked by a cross-compile: it proves the
53+
# catalogs load, the resources resolve and nothing reached for a Windows-only API.
54+
- name: Run the CLI
55+
run: |
56+
dotnet run --project Cli/LabEquipmentController.Cli.csproj -c Release -- version
57+
dotnet run --project Cli/LabEquipmentController.Cli.csproj -c Release -- catalog "*IDN?" --limit 3
58+
dotnet run --project Cli/LabEquipmentController.Cli.csproj -c Release -- interfaces
59+
60+
- name: Draw a plot
61+
shell: bash
62+
run: |
63+
printf 'Hz,Vout\n1000,1.9\n2000,1.8\n5000,1.2\n' > sweep.csv
64+
dotnet run --project Cli/LabEquipmentController.Cli.csproj -c Release -- plot sweep.csv --out sweep.svg --quiet
65+
test -s sweep.svg
66+
grep -q '<svg' sweep.svg
67+
68+
- name: Publish the CLI for this platform
69+
run: dotnet publish Cli/LabEquipmentController.Cli.csproj -c Release -p:PublishSingleFile=true --self-contained false -o cli-out
70+
71+
- name: Toolchain tests
72+
run: node tools/scpi-extract/tests.js
73+
74+
docker:
75+
name: build the web container
76+
runs-on: ubuntu-latest
77+
steps:
78+
- uses: actions/checkout@v7
79+
80+
# The image is the deliverable for the web version, so a Dockerfile that no longer
81+
# builds should fail here rather than on somebody's bench. Built, not pushed — there
82+
# is nowhere agreed to push it to yet.
83+
- name: Build the image
84+
run: docker build -f Web/Dockerfile -t labequipmentcontroller-web:ci .
85+
86+
- name: Check it starts and serves both halves
87+
run: |
88+
docker run -d --name lec-ci -p 8080:8080 labequipmentcontroller-web:ci
89+
for i in $(seq 1 30); do
90+
if curl -fsS http://localhost:8080/api/catalogs > /dev/null 2>&1; then break; fi
91+
sleep 2
92+
done
93+
# The API and the browser half are served by the same process; a container that
94+
# answers one and 404s the other is the failure this catches.
95+
curl -fsS http://localhost:8080/api/catalogs | head -c 200
96+
curl -fsS http://localhost:8080/ | grep -q "Lab Equipment Controller"
97+
curl -fsS http://localhost:8080/_framework/blazor.webassembly.js > /dev/null
98+
docker rm -f lec-ci
99+
100+
pack:
101+
name: pack the NuGet package
102+
runs-on: ubuntu-latest
103+
steps:
104+
- uses: actions/checkout@v7
105+
106+
- uses: actions/setup-dotnet@v6
107+
with:
108+
dotnet-version: '10.0.x'
109+
110+
# Packing on every change catches a broken package long before a release does — a
111+
# missing licence expression or readme only shows up at pack time.
112+
- name: Pack
113+
run: dotnet pack Core/LabEquipmentController.Core.csproj -c Release -o artifacts
114+
115+
- uses: actions/upload-artifact@v7
116+
with:
117+
name: nupkg
118+
path: artifacts/*.*nupkg
119+
if-no-files-found: error
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# Publish LabEquipmentController to nuget.org using Trusted Publishing.
2+
#
3+
# No API key is stored anywhere. GitHub issues a short-lived, signed OIDC token describing
4+
# this repository and this workflow file; nuget.org validates it against a policy you
5+
# registered, and hands back an API key that lives for one hour. Nothing to rotate, nothing
6+
# to leak.
7+
#
8+
# ─── One-time setup on nuget.org ────────────────────────────────────────────────────────
9+
# Sign in → your username → Trusted Publishing → add a policy:
10+
# Repository Owner : EECSB
11+
# Repository : LabEquipmentController
12+
# Workflow File : publish-nuget.yml (file name only, no path)
13+
# Environment : (leave empty — this workflow declares none)
14+
#
15+
# ─── One-time setup on GitHub ───────────────────────────────────────────────────────────
16+
# Settings → Secrets and variables → Actions → New repository secret:
17+
# NUGET_USER = your nuget.org username (the profile name, NOT your email address)
18+
#
19+
# The policy is keyed to this file's NAME. Renaming this file breaks publishing until the
20+
# policy is updated to match.
21+
#
22+
# A policy on a private repository starts out "temporarily active" for 7 days and becomes
23+
# permanent on the first successful publish — nuget.org needs the repository and owner IDs,
24+
# which only arrive inside a real token, to pin the policy against someone deleting the
25+
# repo and recreating it under the same name.
26+
name: Publish to NuGet
27+
28+
# Manual only, on purpose. There is deliberately NO `release:` trigger: this library and
29+
# the desktop app version independently, so a GitHub release tagged v1.1.0 for the app
30+
# would otherwise publish version 1.1.0 of the library — permanently, since a version on
31+
# nuget.org can never be reused or deleted — without anyone having decided to.
32+
#
33+
# An approval gate via a GitHub Environment would be the alternative, but required
34+
# reviewers are a paid feature on private repositories and this one is private. Typing the
35+
# version by hand is the gate.
36+
on:
37+
workflow_dispatch:
38+
inputs:
39+
version:
40+
description: 'Version to publish, e.g. 1.0.0-beta.3. Leave empty to use the version in the csproj.'
41+
required: false
42+
type: string
43+
44+
permissions:
45+
contents: read
46+
47+
jobs:
48+
publish:
49+
runs-on: ubuntu-latest
50+
permissions:
51+
id-token: write # lets this job ask GitHub for the OIDC token
52+
contents: read
53+
54+
steps:
55+
- uses: actions/checkout@v7
56+
57+
- uses: actions/setup-dotnet@v6
58+
with:
59+
dotnet-version: '10.0.x'
60+
61+
# The version typed into the dispatch form wins; left empty, the csproj's own version
62+
# is used, which is the one CI has been packing all along.
63+
- name: Work out the version
64+
id: version
65+
shell: bash
66+
run: |
67+
version="${{ github.event.inputs.version }}"
68+
echo "value=$version" >> "$GITHUB_OUTPUT"
69+
echo "Publishing version: ${version:-(the version in the csproj)}"
70+
71+
# Never publish something that does not pass its own tests. A package cannot be
72+
# unpublished from nuget.org, only delisted.
73+
- name: Test
74+
run: dotnet test Tests/LabEquipmentController.Tests.csproj -c Release
75+
76+
- name: Pack
77+
run: |
78+
if [ -n "${{ steps.version.outputs.value }}" ]; then
79+
dotnet pack Core/LabEquipmentController.Core.csproj -c Release -o artifacts \
80+
-p:Version=${{ steps.version.outputs.value }}
81+
else
82+
dotnet pack Core/LabEquipmentController.Core.csproj -c Release -o artifacts
83+
fi
84+
ls -l artifacts
85+
shell: bash
86+
87+
# The temporary key is valid for one hour and each token buys exactly one key, so
88+
# this sits as late as possible — immediately before the push.
89+
- name: NuGet login (OIDC → short-lived API key)
90+
uses: NuGet/login@v1
91+
id: login
92+
with:
93+
user: ${{ secrets.NUGET_USER }}
94+
95+
- name: Push
96+
run: |
97+
dotnet nuget push "artifacts/*.nupkg" \
98+
--api-key ${{ steps.login.outputs.NUGET_API_KEY }} \
99+
--source https://api.nuget.org/v3/index.json \
100+
--skip-duplicate
101+
shell: bash
102+
103+
# The symbols package is pushed the same way; without it a consumer's stack trace
104+
# stops at the assembly boundary.
105+
- name: Push symbols
106+
continue-on-error: true
107+
run: |
108+
dotnet nuget push "artifacts/*.snupkg" \
109+
--api-key ${{ steps.login.outputs.NUGET_API_KEY }} \
110+
--source https://api.nuget.org/v3/index.json \
111+
--skip-duplicate
112+
shell: bash
113+
114+
- uses: actions/upload-artifact@v7
115+
with:
116+
name: published-nupkg
117+
path: artifacts/*.*nupkg
118+
if-no-files-found: error

.gitignore

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Build output
2+
[Bb]in/
3+
[Oo]bj/
4+
[Dd]ebug/
5+
[Rr]elease/
6+
x64/
7+
x86/
8+
[Ww][Ii][Nn]32/
9+
[Aa][Rr][Mm]/
10+
[Aa][Rr][Mm]64/
11+
12+
# Visual Studio
13+
.vs/
14+
*.user
15+
*.suo
16+
*.userosscache
17+
*.sln.docstates
18+
*.userprefs
19+
20+
# Rider / VS Code
21+
.idea/
22+
.vscode/
23+
24+
# NuGet
25+
*.nupkg
26+
*.snupkg
27+
packages/
28+
project.lock.json
29+
project.assets.json
30+
31+
# Secrets — API keys for the AI extraction feature, used in development and testing.
32+
# Never commit these. .env.example carries the variable names with no values.
33+
.env
34+
.env.*
35+
!.env.example
36+
*.key
37+
appsettings.*.local.json
38+
39+
# Vendor programming guides kept as a local reference, and the web pages and forum threads
40+
# archived while hunting for them. Free to download from the vendor, which is not a licence
41+
# to redistribute — so a local collection lives here and stays here. More so for the forum
42+
# threads: those are their authors' words, not the vendors'.
43+
#
44+
# The two committed files in this folder are the indexes — README.md for the guides,
45+
# ARCHIVED-PAGES.md for the saved pages. The guides and pages themselves never are.
46+
#
47+
# At any depth, not just the top: the guides are now filed under a folder per manufacturer,
48+
# and `datasheets/*.pdf` matches one level only — moving them into those folders would have
49+
# quietly un-ignored all of them.
50+
datasheets/**/*.pdf
51+
datasheets/**/*.zip
52+
datasheets/**/*.html
53+
54+
# scpi-extract working files. The pipeline's inputs and intermediates, not its output:
55+
# manuals/ holds the pdftotext dumps of vendor guides (same reason the PDFs are ignored
56+
# above), parsed/ and built-*.json are regenerated from those in one command. What is
57+
# committed is cfg/<family>.json — the recipe — and the catalog it produces in
58+
# Core/CommandData/.
59+
tools/scpi-extract/manuals/
60+
tools/scpi-extract/parsed/
61+
tools/scpi-extract/built-*.json
62+
63+
# Test run artefacts
64+
Tests/TestResults/
65+
66+
# Misc
67+
*.log
68+
*.tmp
69+
[Tt]humbs.db
70+
.DS_Store
71+
bench-reports/

0 commit comments

Comments
 (0)