No description
  • C# 99.7%
  • PowerShell 0.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
joe e02e3e2af5 Vision ops: run only if another op failed, fail if it failed, Pick best op
An op's Run condition (Always / only if <op> failed / fail if <op> failed)
lets one op back up another inside a job; a fallback that passes covers
the op it stands in for, and skipped ops don't fail the job. Pick best
takes the result of one of several ops (best score, first that passed,
all must pass, all agree). Skipped ops are recorded in the run log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 23:35:20 -05:00
Auth Initial import of PhAT 2026-10-28 21:16:52 -05:00
build Initial import of PhAT 2026-10-28 21:16:52 -05:00
Camera SmartCameras: free-running combined jobs for the F430 2026-10-28 22:22:14 -05:00
Cameras Initial import of PhAT 2026-10-28 21:16:52 -05:00
Data Initial import of PhAT 2026-10-28 21:16:52 -05:00
Devices SmartCameras: free-running combined jobs for the F430 2026-10-28 22:22:14 -05:00
Ftp Initial import of PhAT 2026-10-28 21:16:52 -05:00
Hosting Initial import of PhAT 2026-10-28 21:16:52 -05:00
Inventory Initial import of PhAT 2026-10-28 21:16:52 -05:00
Machine SmartCameras: free-running combined jobs for the F430 2026-10-28 22:22:14 -05:00
Masks Initial import of PhAT 2026-10-28 21:16:52 -05:00
Models Initial import of PhAT 2026-10-28 21:16:52 -05:00
native Initial import of PhAT 2026-10-28 21:16:52 -05:00
Networking Initial import of PhAT 2026-10-28 21:16:52 -05:00
Quantities Initial import of PhAT 2026-10-28 21:16:52 -05:00
Robots Initial import of PhAT 2026-10-28 21:16:52 -05:00
Sequencing Initial import of PhAT 2026-10-28 21:16:52 -05:00
Stages Initial import of PhAT 2026-10-28 21:16:52 -05:00
Telemetry Initial import of PhAT 2026-10-28 21:16:52 -05:00
Toolpaths Initial import of PhAT 2026-10-28 21:16:52 -05:00
Vision Vision ops: run only if another op failed, fail if it failed, Pick best op 2026-10-28 23:35:20 -05:00
.editorconfig Add .editorconfig and Directory.Build.props/.targets 2026-10-28 21:22:02 -05:00
.gitignore Initial import of PhAT 2026-10-28 21:16:52 -05:00
Directory.Build.props Add .editorconfig and Directory.Build.props/.targets 2026-10-28 21:22:02 -05:00
Directory.Build.targets Add .editorconfig and Directory.Build.props/.targets 2026-10-28 21:22:02 -05:00
PhAT.csproj Initial import of PhAT 2026-10-28 21:16:52 -05:00
README.md Initial import of PhAT 2026-10-28 21:16:52 -05:00

PhAT

The shared libraries behind the apps (MessageLog, Atlanta, PhAT.LittleBoy, PhAT.FatMan, Autoprober.Nacogdoches, and the other Autoprobers).

Project What's in it
PhAT (class library) Services and drivers, no UI.
PhAT.Razor (Razor class library) Blazor components and feature pages built on PhAT.
PhAT.Wpf (WPF class library) WPF counterparts of the vision components (image viewer, job editor, mask editor, camera view, ...); see its README. Used by PhAT.VisionTool, a desktop app for vision jobs.
PhAT.Tests (console) Checks for the Galil, networking (command server, data listener, Modbus, CCS, SCPI, UR) and Velox code against fake devices: dotnet run --project PhAT.Tests. See its README.

PhAT

Folder / namespace Contents
Auth Users and roles (UserStore, RoleResolver), cookie login endpoints
Camera The camera host: live view, captures, live settings, grab statistics, lights
Cameras Camera drivers: Basler, The Imaging Source, Allied Vision (Vimba, FirePackage), OpenCV, simulated; autofocus and auto-exposure
Data SQLite message log and launch log
Devices Protocol clients: EtherNet/IP (CIP), Galil DMC-40x0 (no gclib; see MessageLog/docs/galil.md), Keyence LK-G5000, GC-1000 and NQ-EP4L (IO-Link master), Mecademic, Modbus TCP (client and server, Moxa functions), Opto 22 groov RIO / EPIC (groov Manage REST API), SCPI, Universal Robots (Dashboard, RTDE, URScript)
Hosting Build info (version, build time, commit) from build/PhAT.BuildInfo.targets; AppHost.CreateBuilder (standalone or Windows service, see build/README-service.md); ProcessGate (wait until another program has been up N s)
Networking Command (tester) server, data listener (files framed by content, text commands), network shares, serial request/reply. See Autoprober.Nacogdoches/docs/phat-additions.md
Ftp The in-app FTP server
Inventory Polled inventory with change detection: installed software, network addresses, instruments (USB, VISA, *IDN?), EtherNet/IP scan, NI MAX report, NI licensing
Machine Digital I/O blocks (Modbus TCP, groov RIO: see groov RIO), robots (Meca500, UR), displacement sensors, the STOP service
Masks Mask library and endpoints
Robots Robot kinematics (UR5e, FANUC LR Mate), motion planning over a plane, URScript / FANUC LS export
Sequencing The Engine: C# and .csx sequences, machine lock
Stages Stage model and controllers, stored positions and their history, probers (Velox: see Waiting for VeloxPro)
Telemetry Metrics: this PC (CPU, memory, disks, network), watched apps (process, /metrics/json), MetricsExport for an app's own .NET metrics, IMetricSink for logging
Toolpaths 2D G-code reader, regions, infill patterns
Vision Vision jobs, ops, processors, calibration, run logs; job checks (VisionJobCheck) and zip export/import (VisionJobArchive). Jobs reload when their files change on disk ("Vision:WatchJobs", default on).

PhAT references Microsoft.AspNetCore.App (hosted services, endpoints, auth) and carries the vendor SDK references (pylon, IC4, OpenCV, Tesseract, ...). It also ships libdmtx.dll, and FirePackage's DLLs when that SDK is installed. Its None items copy them into every app's output.

Waiting for VeloxPro

An app installed as a Windows service starts at boot, but FormFactor's VeloxPro only starts when someone logs in. To connect to Velox only once VeloxPro has been running for a while:

"Prober": { "Driver": "Velox", "WaitForProcess": "VeloxPro", "ProcessUpSeconds": 30 }
  • Connecting: VeloxClient.ConnectAsync waits for the program. That applies to the main connection, the stop connection and the prober's status connection.
  • Commands meanwhile: a command sent before then fails at once with VeloxNotReadyException.
  • Status text: VeloxClient.NotReady (and VeloxProber.NotReady) say why, e.g. "Waiting for VeloxPro.exe (not running)" or "Waiting for VeloxPro.exe to load (12 of 30 s)". Changes raise ConnectionChanged.
  • Tester replies: a tester reply can send "RECOVERABLE," followed by that text unchanged; the message of VeloxNotReadyException is the same text.
  • Prober stages: they show that text as their error until the program is up.
  • Exit: if VeloxPro exits, the next connection waits again.
  • Start time: it comes from the process. If a service account can't read another user's process, it counts from when the app first saw it.
  • Default: empty WaitForProcess connects at once.

Velox notifications and live positions

For the Velox drivers, AddStages also registers one shared VeloxNotifications listener. It registers as <AppName>Events with notifications on, starts with the app, and waits for the VeloxPro gate like the client. Apps subscribe to its Received and decode with:

  • AsAlert(): AlertNotification, 0xCA;
  • AsButtonPress(): 0x152;
  • AsLicense(): LicenseInfo, 0x103;
  • AsAppChange(): RegisterProberAppChange, 0x109, e.g. to know when Spectrum has registered.

Each returns null for other types.

  • Alerts: VeloxAlert gives the alert id, Value(i) and Letter(i). Letters may arrive as their ASCII code.
  • Which stage: Target is 'C', 'S' or 'P' with the positioner number.
  • Position alerts: AffectsPosition marks XY, Z, theta and index moves, load, new homes, contact / focus heights, and a prober reset.
  • Frame: per the manual's alert table, the positions in alerts are in the zero frame.

Live positions: VeloxProber raises PositionsChanged on those alerts, and ProberService then reads the positions at once in the home frame, at most every 50 ms. So moves made from Velox's own UI or the joystick show up on the stages straight away. The 1 s idle poll and PollMs while moving stay as a fallback.

The VeloxSimulator driver sends these notifications for moves, homes, contact heights and app (un)registration, as Cmd=<id>:<name>:<data> lines.

Not yet seen on a real prober: the line format and the alert values come from the manual.

groov RIO

Opto 22 groov RIO channels as named I/O, through the groov Manage REST API. It also works for a groov EPIC: set Module to the slot.

  • How it reads: one request per module reads all its digital channels, and one reads all its analog channels, every PollMs.
  • Quality errors: a channel the RIO flags as bad reads as unknown.
  • Writes: outputs are written at once.

In appsettings.json:

"GroovRio": [
  {
    "Name": "RIO1",
    "Host": "192.168.1.50",
    "ApiKey": "<API key of a groov user with Manage permission>",
    "PollMs": 100,
    "Points": [
      { "Name": "DoorClosed", "Kind": "DigitalInput", "Channel": 0 },
      { "Name": "Beacon", "Kind": "DigitalOutput", "Channel": 7 },
      { "Name": "ChillerTemp", "Kind": "AnalogInput", "Channel": 5, "Unit": "°C" },
      { "Name": "Valve", "Kind": "AnalogOutput", "Channel": 8 }
    ]
  }
]
  • Program.cs: call builder.Services.AddGroovRio(builder.Configuration) before AddStages. The RIO becomes the app's IDigitalIo / IAnalogIo if no other I/O was registered first. Only one I/O source is used per app for now, as with AddModbusIo and AddGalil.
  • Channels and values:
    • Channel is the channel number shown in groov Manage.
    • Kind must match how the channel is configured on the RIO.
    • Analog values arrive already scaled by the RIO, in its configured units.
    • Inverted flips a digital point.
  • Certificate: the RIO serves HTTPS with a self-signed certificate. Without CertificateThumbprint any certificate is accepted (still encrypted), and the log shows the thumbprint seen. Copy it into CertificateThumbprint to accept only that RIO.
  • API key: create it in groov Manage → Accounts, for a user with Manage permission. A wrong key is reported as 401.
  • Status: GroovRioIo.Units shows the connection state, the module (e.g. GRV-R7-MM1001-10) and the thumbprint. Points shows each value with the channel's name on the RIO.

Tested only against a fake RIO in PhAT.Tests (groovrio), not a real device.

Stored position history

StoredPositionStore (stages) and RobotPositionStore (robots) record every add, edit and removal.

  • Where: a JSON Lines file beside the positions file, one change per line, only ever appended: stage-positions.history.jsonl and robot-positions.history.jsonl.
  • What each change holds: the time, who made it, and the position before and after. "Before" is empty for an add and "after" for a removal. An edit that changes nothing isn't recorded.
  • Who: Add, Update and Remove take an optional user. A stage's Map and the robot Map pass the signed-in user's name.
  • Who may change them: on the Maps, only engineers and admins add, edit, remove or roll back. Operators can view the positions and their history, and move to a position.
  • Rolling back: Rollback(changeId, user) puts the one position that change touched back as it was before it, and leaves every other position alone.
    • Rolling back an add removes the position; rolling back a removal re-adds it with the same Id.
    • Any later edits to that position are replaced too. The rollback is recorded as a change of its own (RollbackOf), so it can be rolled back in turn.
    • It is refused if the position is already that way, or (robots) if its old label now belongs to another position.
  • In the UI: the History button beside "Add current position" lists the changes, newest first, each with a Roll back button and a confirmation.

Launch units

The launches table stores X, Y, Z and ScopeZ in µm and θ in degrees, as it always has. The Launches page, its analysis and its distributions show them in mils, or in "Launches:LengthUnit" (default "Stages:DefaultLengthUnit"). Sequences record a launch with quantities, in any unit:

await RecordLaunchAsync(carrier, lid, pocket, port.Position, scope.Position.Z, pass, power, vision, msn: msn);

The older overload, which takes plain numbers in µm and degrees, still works.

Network shares from the page

Admins add, edit and remove shares on the Network shares page: name, UNC path, optional drive letter, user and password.

  • Where they're kept: NetworkShares:SharesFile (default network-shares.json), with no passwords in it.
  • Passwords: encrypted with DPAPI for this PC in NetworkShares:SecretsFile, as before.
  • Shares from appsettings.json: still connected. The page marks them "appsettings" and only changes their passwords.
  • Code: NetworkShareService.SaveAsync / RemoveAsync / Problem (validation).

Vision job pass rule

A job's Pass when setting (VisionJob.PassWhen, in the job editor's header) decides VisionJobResult.Success, and with it the run log's pass/fail and the Vision logs page:

  • All (the default): every op must succeed.
  • AnyRead: the job passes when at least one text extractor op reads. Use it for jobs whose ops are alternative reads (barcode-1…7, then text); the other ops' failures don't fail it.

Runs logged before the rule was set keep their pass/fail (it is in the log file name).

Vision job "Run only if another failed"

A job's Run setting (VisionJob.RunIfFailed, beside "Pass when" in the job editor's header) makes it a fallback: "Only if "Read A" failed" runs the job only when Read A's most recent run failed. Otherwise the job is skipped. "Always" (the default, nothing in the JSON) runs it as before.

  • Which run of the other job counts: its most recent run in the same batch.
    • A sequence run is one batch, sub-sequences included (SequenceContext.VisionHistory). Run the job first, then its fallback:

      using var a = await RunVisionAsync("Read A");
      using var b = await RunVisionAsync("Read A fallback"); // skipped unless Read A failed just now
      
    • Each VisionManagerService.RunAllAsync(names) call is one batch. A fallback waits for the job it names when that job is listed before it.

    • VisionManagerService.RunAsync(name) with no batch uses the most recent run of the other job by name in the app.

  • When it's skipped: the other job passed, was skipped itself, or hasn't run in the batch. So in a chain (B if A failed, C if B failed), C runs only when A and B both failed.
  • If the other job doesn't exist: the run fails with an error. Renaming a job updates the jobs that name it.
  • How a skip shows:
    • The result has Skipped and SkipReason, no images or ops, and Success false. It isn't a failure: Outcome is Skipped.
    • The sequence log says "Vision "X": Skipped: "Read A" passed (runs only if it failed)."
    • The run log line has "skipped": true and skipReason.
    • The vision log records only JSON, named ..._skip.json.
    • The Vision logs page shows the run as Skipped (grey), with its own filter. Skipped runs aren't counted in the pass %.
  • The editor's Run button runs the job whatever this setting says (trial runs).

Stage ↔ pixel calibration

The Calibration page's "Stage ↔ pixel" tab works out how image pixels map to stage positions.

  • How it runs: moves a stage over a grid (start, step and end per axis), or over a list of offsets, around an origin. The origin is the current position or a Map position. At each point it runs a vision job.
  • The fit: where the chosen op found the target, fitted against where the stage was. It's a full 2D affine fit (rotation and separate X/Y scales), checked against a minimum R² per axis.
  • Saving: engineers save the fit by name in stage-calibrations.json ("Vision:StageCalibrationsFile").
  • Using it in a sequence:
var cal = Get<StageCalibrationStore>().Find("Launch", "Target", "Scope");
var (dx, dy) = cal!.Delta(found.Center, reference.Center); // stage move between two pixels
var (x, y) = cal.ToStage(pixel);                            // stage position for a pixel

PhAT.Razor

Everything lives under Components/ (namespace PhAT.Razor.Components.*):

  • Shared components: ImageViewer, MessageLogGrid, CameraImageViewer, VisionResultOverlay, and the Auth, Devices (NqMasterPanel), Launches, Masks, Stages and Vision folders.
  • Layout: ReconnectModal.
  • Feature pages (Pages/): Calibration, Commands (command server), Controller, Diagnostics, Engine, FTP, Launches, Login, Masks, Inventory, Paths, Shares (network shares), Telemetry (metrics dashboard), Templates, Users, Vision, Vision logs.
  • Footer: BuildInfoLabel (version, commit, build time).
  • Telemetry: Gauge, Sparkline and MetricFormat (Telemetry/).
  • Scope viewer: ScopeViewer (shown/hidden, picked area) and ScopeAreaOverlay.
  • Themes (Theme/): PhatTheme (applies the theme), ThemePicker (the header button) and ThemeState. See Themes.

Collocated .razor.js modules are served at _content/PhAT.Razor/Components/.... Scoped CSS reaches the app through its <App>.styles.css bundle.

Themes

Light, dark or "Windows" (follows the PC's setting), plus an accent color.

  • Each instance has a default in appsettings. A different accent per station makes them easy to tell apart:

    "Theme": { "Mode": "Dark", "Accent": "#CA5010", "AllowUserChoice": true }
    
    • Mode: Light (the default, the look from before themes), Dark or System.
    • Accent: #rrggbb. Empty means Fluent's default blue.
    • AllowUserChoice: when false, the button is hidden and everyone sees the instance's theme.
    • Changes apply without a restart.
  • Each user can override it with the header's theme button (sun / moon). The choice is saved in that browser (localStorage phat.theme). "Station default" goes back to the instance's theme.

  • Colors in components: use Fluent's tokens (--neutral-layer-1, --neutral-foreground-rest, --accent-fill-rest, --foreground-on-accent-rest, ...), which switch on their own. For status colors use PhAT's tokens from wwwroot/phat-theme.css, with the light value as fallback, e.g. color: var(--phat-error-fg, #a4262c):

    • --phat-error-fg / -bg, --phat-success-fg / -bg, --phat-warning-fg / -bg, --phat-info-fg / -bg;
    • --phat-canvas (behind images).

    Image overlays and strong fills (STOP, pass / fail badges) keep their fixed colors.

  • Known limit: a saved dark choice applies once the page connects, so a dark user sees the light page for a moment on load.

Using it in an app

  1. Reference the library: <ProjectReference Include="..\PhAT.Razor\PhAT.Razor.csproj" />. This brings in PhAT too.
  2. Routes.razor: <Router AppAssembly="typeof(Program).Assembly" AdditionalAssemblies="[PhAT.Razor.PhATRazor.Assembly]" ...>.
  3. Program.cs:
    • Add .AddAdditionalAssemblies(PhAT.Razor.PhATRazor.Assembly) after MapRazorComponents<App>().
    • Register the services the pages you link to need (AddVisionJobs, AddStages, AddEngine, ...). A page whose services aren't registered fails when it's opened.
  4. _Imports.razor: add @using PhAT.Razor.Components.Layout for <ReconnectModal />, plus whichever PhAT.* namespaces the app uses.
  5. Themes:
    • in Program.cs: builder.Services.AddThemes(builder.Configuration);;
    • in App.razor, before app.css: <link rel="stylesheet" href="@Assets["_content/PhAT.Razor/phat-theme.css"]" />;
    • in MainLayout: <PhatTheme />, plus <ThemePicker /> in the header (@using PhAT.Razor.Components.Theme).
    • The layout also needs <FluentMenuProvider />: the data grids' column-header menus use it.

The apps keep their own Program.cs, App.razor, Routes.razor, MainLayout / NavPanel, home page, appsettings.json / Instances/ and data (TestFrames, VisionJobs, VisionModels, databases).