{"id":50551661,"url":"https://github.com/say-5/scanguard","last_synced_at":"2026-06-04T04:00:33.467Z","repository":{"id":362361754,"uuid":"1258702970","full_name":"SAY-5/scanguard","owner":"SAY-5","description":null,"archived":false,"fork":false,"pushed_at":"2026-06-03T22:10:27.000Z","size":68,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-03T22:18:58.041Z","etag":null,"topics":["go","hardware-in-the-loop","instrumentation","medical-imaging","python","scpi","test-automation"],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/SAY-5.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"docs/CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-03T20:49:34.000Z","updated_at":"2026-06-03T22:09:25.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/SAY-5/scanguard","commit_stats":null,"previous_names":["say-5/scanguard"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/SAY-5/scanguard","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SAY-5%2Fscanguard","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SAY-5%2Fscanguard/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SAY-5%2Fscanguard/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SAY-5%2Fscanguard/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/SAY-5","download_url":"https://codeload.github.com/SAY-5/scanguard/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SAY-5%2Fscanguard/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33888302,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-04T02:00:06.755Z","response_time":64,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["go","hardware-in-the-loop","instrumentation","medical-imaging","python","scpi","test-automation"],"created_at":"2026-06-04T04:00:32.435Z","updated_at":"2026-06-04T04:00:33.456Z","avatar_url":"https://github.com/SAY-5.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# scanguard\n\nInstrument test and validation platform for a research microPET scanner.\n\nscanguard automates the pre-scan verification and scan-sequence orchestration\nfor a small-animal PET scanner used to image rodent brains. It replaces a\nmanual checklist and a flaky scan-sequence script with three pieces:\n\n1. Automated preflight checks that talk to the scanner over a TCP/IP\n   instrument protocol (SCPI) before any scan begins.\n2. A scan-sequence orchestrator that drives the motorized bed with setup-state\n   verification and wait-and-confirm execution.\n3. Resilient instrument transports with stable device addressing.\n\nEverything runs against simulated instruments, so the whole system runs with no\nhardware attached. The v1.0.0 release turns the preflight piece into a complete\ndeclarative checklist engine, v2.0.0 builds out the scan-sequence orchestrator\nwith setup-state verification and wait-and-confirm execution, v3.0.0 makes the\ninstrument transport resilient and addresses each device by its serial, v4.0.0\nturns the scripts into a platform: a run-history store, a REST API and Prometheus\nmetrics, and a web dashboard that read the reports and run logs the earlier\nreleases produce, and v5.0.0 hardens the platform for production with\nconfig-driven instrument profiles, a fault-injection chaos suite, SCPI parser\nfuzzing, hot-path benchmarks with a regression gate, and operability work\n(health and readiness, graceful shutdown, structured logging, and input\nvalidation).\n\n## Components\n\n| Path         | Language | What it does                                                        |\n|--------------|----------|---------------------------------------------------------------------|\n| `sim/`       | Go       | Simulated SCPI scanner and bed controller over TCP. Test backbone.  |\n| `core/`      | Go       | SCPI transport, bed and detector clients, the orchestrator, the run-history store, and the REST API daemon. |\n| `preflight/` | Python   | Declarative preflight checklist engine: four check types, three report formats. |\n| `web/`       | TypeScript | React and Vite dashboard reading the REST API: preflight, calibration countdown, runs, devices, and trends. |\n| `e2e/`       | Go       | End-to-end failure-mode proof: drives the sims and orchestrator as real processes. |\n| `chaos/`     | Go       | Fault-injection suite: drives the sims and orchestrator and asserts safe failure or recovery. |\n| `instrument/`| Go       | Config-driven instrument profiles and the two shipped profiles. |\n| `bench/`     | -        | Benchmark baseline and the regression-gate documentation. |\n| `docs/`      | Markdown | Architecture, glossary, operations, and the contribution workflow.  |\n\n## How this differs\n\nThe simulators are first-class. Every later release is tested against the same\nfake instruments, so the test backbone never depends on lab hardware being\nfree. The orchestrator and preflight engine speak the same SCPI text protocol\nto the simulators that they would speak to the real scanner.\n\n## Preflight checklist\n\nThe preflight engine runs the morning scanner-verification checklist. The\nchecklist is declarative: the checks, their ranges, and their thresholds live\nin a JSON config (see `preflight/configs/default.json`), so adding a detector\nchannel or changing a limit is a config edit, not a code change. The engine\nruns every check on each pass and never stops at the first failure, so the\nreport shows every problem at once and only the final line says whether it is\nsafe to proceed.\n\nFour check types are supported:\n\n- **Detector channel range**: queries each configured channel over SCPI\n  (`MEAS:VOLT? CHAN1`, `CHAN2`, ...) and confirms the voltage is within range.\n- **Calibration file**: reads a JSON calibration file's `sensor_gain`,\n  `offset_correction`, and `last_calibrated` date, checks the gain and offset\n  ranges, and flags a calibration older than 30 days with a distinct stale\n  status.\n- **Device self test**: sends `*TST?` and flags any non-zero code.\n- **Disk space**: confirms the scan-output drive has enough free space.\n\nRun it with `python -m preflight`:\n\n```sh\npython -m preflight --config preflight/configs/default.json \\\n  --scanner 127.0.0.1:5025 \\\n  --json report.json \\\n  --junit report.xml\n```\n\nThe exit code is non-zero if any check fails. There are three output formats:\na human PASS/FAIL summary on stdout, a structured JSON report (`--json`), and a\nJUnit XML report (`--junit`) that CI can ingest as a test report.\n\nA passing run looks like this:\n\n```\n[ PASS ] Detector Channel 1: 4.97V (range 4.8-5.2V)\n[ PASS ] Detector Channel 2: 5.01V (range 4.8-5.2V)\n[ PASS ] Device Self Test: self test passed (code 0)\n[ PASS ] Calibration File: gain 1.02, offset 0.05, 14 days old\n[ PASS ] Disk Space: 6.3GB free, need 1GB\n5/5 checks passed\npreflight: PASS (proceed)\n```\n\nA run with problems reports each one before the verdict:\n\n```\n[ PASS ] Detector Channel 1: 4.97V (range 4.8-5.2V)\n[ FAIL ] Detector Channel 2: 5.01V (range 2.9-3.1V)\n[ FAIL ] Device Self Test: self test failed (code 4)\n[ FAIL ] Calibration File: calibration 144 days old, max 30 days\n[ FAIL ] Disk Space: 6.1GB free, need 10GB\n1/5 checks passed\npreflight: FAIL (do not proceed)\n```\n\n## Scan-sequence orchestrator\n\nThe orchestrator drives the motorized bed through a list of positions. The old\nscripts fired commands and moved on at once, so a capture could read the bed\nmid-travel and a run could start from whatever state a previous session left\nbehind. Those runs failed for setup reasons, not real faults, and operators just\nreran them. The orchestrator removes that class of false failure without hiding\ngenuine faults.\n\nIt runs in three phases:\n\n1. **Setup verification** runs before any scan step. It reads the bed position\n   and, if the bed is not home, sends `MOVE HOME` and polls until it confirms\n   the home position (bounded by a timeout). It lists `.dat` files in the output\n   directory and aborts, naming the file, if a previous session still holds one\n   open. It confirms the detector reports ready. A scan begins only when all\n   three pass.\n2. **Execution** is wait-and-confirm. For each step it sends `MOVE \u003cmm\u003e`, polls\n   the bed position until it is within 0.1mm of the target, then captures and\n   waits for the capture to complete. Nothing is recorded until the bed is\n   actually there.\n3. **Logging** writes a structured JSON run file: a run id and timestamp, each\n   step with its target position, confirmed position, data points, average\n   reading, and timestamp, and a final PASS or FAIL with the reason.\n\nRun it with `scanseq` against the simulators:\n\n```sh\nmake sim \u0026                                  # simulated scanner on :5025\ngo run ./sim/cmd/bed -addr 127.0.0.1:5026 \u0026 # simulated bed\ngo run ./core/cmd/scanseq \\\n  -scanner 127.0.0.1:5025 \\\n  -bed 127.0.0.1:5026 \\\n  -positions 10,20,30 \\\n  -outdir ./out\n```\n\nThe bed simulator can reproduce the documented failure modes for testing:\n`-start 47` leaves the bed off-home, `-stall 5` makes every move stop 5mm short\nof its target, and the scanner's `-notready` flag makes the detector report not\nready. The `e2e` package exercises each of these against the orchestrator.\n\n## Resilient transport and device addressing\n\nThe shared workstations had three recurring transport problems. v3.0.0 makes the\nplatform robust to all three, and proves each against the simulators.\n\n- **Timeout and retry.** The transport timeout was hardcoded at 500ms while the\n  instruments answer in 600-850ms, so a healthy device read as a failure. The\n  per-request timeout is now configurable with a 2000ms default, and a transient\n  failure is retried up to three times with doubling backoff before it counts as\n  real. The timeout stays bounded on purpose: a very long one would make every\n  genuine failure take that long to detect. The scanner's `-latency` flag drives\n  the boundary, and the tests show a 700ms device passing at 2000ms and failing\n  at 500ms.\n- **Stable addressing.** Port names are handed out by connection order and are\n  not stable, but every device has a factory serial that never changes. The\n  registry (`core/internal/registry`) binds each serial to a permanent logical\n  name and resolves a name to whatever address currently carries that serial, so\n  a reorder does not break addressing. The scanner's `-serial` flag gives two\n  sims distinct identities for the reorder test.\n- **udev rules.** The `udevgen` tool is the ops-side mirror: it turns the lab\n  device list into a Linux udev rules file pinning each serial to a fixed\n  `/dev/scanguard/\u003cname\u003e` symlink. See `docs/udev.md` and the sample in\n  `docs/examples/`.\n\n  ```sh\n  go run ./core/cmd/udevgen -devices docs/examples/devices.sample.json\n  ```\n\n- **Health and reconnect.** A health-check probe and reconnect let a run recover\n  from a mid-session link drop instead of aborting. The scanner's `-dropafter`\n  flag models the disconnect.\n\n## Observability platform\n\nv4.0.0 adds a run-history store, a REST API with Prometheus metrics, and a web\ndashboard, all reading the reports and run logs the earlier releases produce.\n\nThe API daemon (`scanguardd`) keeps an embedded SQLite store and serves:\n\n- `GET /api/preflight/latest`, `GET /api/preflight/history`\n- `GET /api/runs`, `GET /api/runs/{id}`\n- `GET /api/devices`, `GET /api/calibration`, `GET /api/trends`\n- `POST /api/preflight/reports`, `POST /api/runs` for ingest\n- `GET /metrics` in Prometheus text format\n\nThe dashboard shows the latest preflight result with each check's PASS, FAIL, or\nSTALE status and measured value, the 30-day calibration countdown with a clear\nstate as it approaches and passes expiry, scan run history with a drill-down into\neach step's target and confirmed bed position, the device registry, and a trends\nchart of reruns and the false-versus-real failure rate.\n\nBring up the whole stack against the simulators with Docker. This builds the API\ndaemon, the simulators, and the dashboard, runs the real preflight engine and\norchestrator to seed the store, and serves the dashboard on port 5173 and the API\non port 8080:\n\n```sh\ndocker compose up --build\n```\n\n## Operations\n\nThe daemon is built to be run and operated, not just demoed:\n\n- **Health and readiness.** `GET /healthz` is the liveness probe and `GET\n  /readyz` pings the store for readiness, so an orchestrator routes traffic only\n  once the daemon can serve data.\n- **Graceful shutdown.** On SIGINT or SIGTERM the daemon drains in-flight\n  requests and closes the store cleanly rather than being killed mid-write.\n- **Structured logging.** The daemon logs JSON to stderr with typed fields.\n- **Input validation.** Ingest bodies are size-capped and rejected when malformed;\n  the `limit` query parameter is bounded so a request cannot pull an unbounded\n  result set.\n- **Container stack.** The compose services declare health checks and ordered\n  startup; the runtime image runs as a non-root user with a `HEALTHCHECK`.\n\nSee `docs/OPERATIONS.md` for the full operations guide and `SECURITY.md` for the\ntrust model and how to report a vulnerability. There is no live hosted deployment:\nthe platform runs on a lab host or cluster behind the site's network controls.\n\n## Requirements\n\n- Go 1.23 or newer\n- Python 3.11 or newer\n- Node 20 or newer (for the dashboard)\n\n## How to run\n\nBuild everything and run the test suites:\n\n```sh\nmake build\nmake test\n```\n\nRun the end-to-end preflight demo against the simulated scanner. This boots the\nsim scanner on a local port, points the preflight engine at it, prints a\nPASS/FAIL summary, and writes the JSON and JUnit reports:\n\n```sh\nmake preflight\n```\n\nRun the Go orchestrator against the simulated scanner (queries identity and one\ndetector channel, writes a JSON run log):\n\n```sh\nmake sim \u0026        # start the simulated scanner\nmake run-core     # connect, query, write runlog\n```\n\nSee `docs/ARCHITECTURE.md` for how the components talk to each other and\n`docs/GLOSSARY.md` for domain terms.\n\n## License\n\nMIT. See `LICENSE`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsay-5%2Fscanguard","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsay-5%2Fscanguard","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsay-5%2Fscanguard/lists"}