# Go Client The Go client module contains the generated MXAccess Gateway protobuf bindings, a small handwritten `mxgateway` package, and the `mxgw-go` test CLI scaffold. The module uses the shared proto inputs documented in `../../docs/ClientProtoGeneration.md` so gateway and client contracts stay in sync. ## Layout ```text clients/go/ go.mod generate-proto.ps1 internal/generated/ mxgateway/ cmd/mxgw-go/ ``` `internal/generated` contains code produced by `protoc`, `protoc-gen-go`, and `protoc-gen-go-grpc`. Do not edit generated files by hand. ## Regenerating Protobuf Bindings Run generation after the shared `.proto` files or the Go output path changes: ```powershell ./generate-proto.ps1 ``` The script uses the tool paths recorded in `../../docs/ToolchainLinks.md`. ## Build And Test Run the Go module checks from `clients/go`: ```powershell go test ./... go build ./... go vet ./... ``` The tests parse the shared JSON fixtures, exercise value and status conversion, use `bufconn` for fake gateway auth and streaming behavior, and cover CLI JSON redaction. ## Packaging Build a local CLI executable from `clients/go`: ```powershell New-Item -ItemType Directory -Force ../../artifacts/clients/go | Out-Null go build -o ../../artifacts/clients/go/mxgw-go.exe ./cmd/mxgw-go ``` Install the CLI into the active `GOBIN` or `GOPATH/bin`: ```powershell go install ./cmd/mxgw-go ``` Other Go modules can consume the library package with the module path `gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/mxgateway`. ## Client API Use `mxgateway.Dial` with `mxgateway.Options` to configure plaintext or TLS transport, API-key metadata, dial timeout, and per-call timeout: ```go client, err := mxgateway.Dial(ctx, mxgateway.Options{ Endpoint: "localhost:5000", APIKey: os.Getenv("MXGATEWAY_API_KEY"), Plaintext: true, }) ``` The gateway can auto-generate its own self-signed certificate (it has no PKI), so the client is **lenient by default**: a TLS connection (`Plaintext: false`) with no `CACertFile`/`TLSConfig` accepts whatever certificate the gateway presents (`InsecureSkipVerify`, with `ServerNameOverride` as the SNI when set). To verify instead, set `CACertFile` to pin a CA, or set `RequireCertificateValidation: true` to verify against the OS/system trust roots without pinning. See [Gateway Configuration](../../docs/GatewayConfiguration.md#automatic-self-signed-certificate). `Client.OpenSession` returns a `Session` with helpers for `Register`, `AddItem`, `AddItem2`, `Advise`, `Write`, `Events`, and `Close`. Prefer `SubscribeEvents` or `SubscribeEventsAfter` for long-running streams because the returned subscription owns cancellation and exposes `Close` for deterministic goroutine cleanup. Raw protobuf messages remain available through the `mxgateway` package aliases and the `Raw` helper methods. Typed errors support `errors.As` for `GatewayError`, `CommandError`, and `MxAccessError`; command errors preserve the raw reply. For alarms, the package exposes `Client.QueryActiveAlarms` for one-shot snapshots, `Client.StreamAlarms` for the server-streaming feed, and `Client.AcknowledgeAlarm` to ack an alarm by full reference. The streaming call returns a `StreamAlarmsClient`; cancel its context to terminate the stream. All three pass straight through to the gateway's central alarm monitor. ## Galaxy Repository browse The `GalaxyRepository` service (proto package `galaxy_repository.v1`) is a read-only metadata-only browse over the AVEVA System Platform Galaxy Repository. It uses the same API-key authentication as the MXAccess Gateway and requires the `metadata:read` scope. Use `mxgateway.DialGalaxy` to obtain a `*GalaxyClient` that mirrors the connection-management conventions of `Client`: ```go galaxy, err := mxgateway.DialGalaxy(ctx, mxgateway.Options{ Endpoint: "localhost:5000", APIKey: os.Getenv("MXGATEWAY_API_KEY"), Plaintext: true, }) if err != nil { return err } defer galaxy.Close() ok, err := galaxy.TestConnection(ctx) deployTime, present, err := galaxy.GetLastDeployTime(ctx) objects, err := galaxy.DiscoverHierarchy(ctx) ``` `GetLastDeployTime` returns `(time.Time{}, false, nil)` when the server reports `present=false` (no deploy recorded). `DiscoverHierarchy` returns the generated `*GalaxyObject` slice with each object's dynamic attributes populated for direct contract access. ### Browsing lazily For UI trees or OPC UA bridges, use `BrowseChildren` to walk one level at a time instead of loading the full hierarchy. Pass an empty request for root objects; subsequent calls set `ParentGobjectId`, `ParentTagName`, or `ParentContainedPath`. Filter fields match `DiscoverHierarchy`. Each response pairs `Children` with `ChildHasChildren` so you know which nodes to expand. See [Galaxy Repository](../../docs/GalaxyRepository.md#browsechildren) for full request and filter semantics. ```go import pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated/galaxy_repository/v1" reply, err := galaxy.BrowseChildren(ctx, &pb.BrowseChildrenRequest{}) if err != nil { return err } for i, child := range reply.GetChildren() { fmt.Printf("%s expand=%v\n", child.GetTagName(), reply.GetChildHasChildren()[i]) } ``` #### High-level walker For UI trees, the client provides a `LazyBrowseNode` walker that handles sibling pagination and the `child_has_children` hint for you: ```go galaxy, err := mxgateway.DialGalaxy(ctx, mxgateway.Options{ Endpoint: "localhost:5000", APIKey: os.Getenv("MXGATEWAY_API_KEY"), Plaintext: true, }) if err != nil { log.Fatal(err) } defer galaxy.Close() roots, err := galaxy.Browse(ctx, nil) if err != nil { log.Fatal(err) } for _, root := range roots { if root.HasChildrenHint() { if err := root.Expand(ctx); err != nil { log.Fatal(err) } } for _, child := range root.Children() { kind := "leaf" if child.HasChildrenHint() { kind = "has children" } fmt.Printf("%s (%s)\n", child.Object().GetTagName(), kind) } } ``` `Expand` is idempotent — calling it twice fires only one RPC, and is safe under concurrent callers. To refresh after a Galaxy redeploy, call `Browse` again from the root. ### Watching deploy events `WatchDeployEvents` opens a server-streaming subscription. The server emits a bootstrap event with the current Galaxy state immediately on subscribe, then one `DeployEvent` per new deploy. `Sequence` is monotonic per server start; gaps signal dropped events. Pass a non-nil `lastSeenDeployTime` to suppress the bootstrap event when resuming from a known checkpoint: ```go streamCtx, cancel := context.WithCancel(ctx) defer cancel() events, errs, err := galaxy.WatchDeployEvents(streamCtx, nil) if err != nil { return err } for { select { case ev, ok := <-events: if !ok { return nil // stream completed (server EOF or ctx cancelled) } log.Printf("seq=%d objects=%d attrs=%d", ev.GetSequence(), ev.GetObjectCount(), ev.GetAttributeCount()) case streamErr := <-errs: if streamErr != nil { return streamErr // *GatewayError } case <-ctx.Done(): return ctx.Err() } } ``` Cancel the supplied context to tear down the stream cleanly. Both channels close after EOF, cancellation, or a terminal error; surfaced errors are wrapped in `*GatewayError`. The CLI exposes the same RPC via `galaxy-watch`: ```powershell go run ./cmd/mxgw-go galaxy-watch -plaintext go run ./cmd/mxgw-go galaxy-watch -plaintext -json go run ./cmd/mxgw-go galaxy-watch -plaintext -last-seen-deploy-time 2026-04-28T10:00:00Z go run ./cmd/mxgw-go galaxy-watch -plaintext -limit 5 ``` The command runs until Ctrl+C (or the optional `-limit` is reached) and prints one line per event in text mode or one JSON object per event with `-json`. ## CLI The `mxgw-go` CLI emits JSON with redacted API keys for commands that connect to the gateway: ```powershell go run ./cmd/mxgw-go version -json go run ./cmd/mxgw-go open-session -endpoint localhost:5000 -plaintext -json go run ./cmd/mxgw-go register -session-id -client-name mxgw-go -plaintext -json go run ./cmd/mxgw-go add-item -session-id -server-handle 1 -item Area001.Tag.Value -plaintext -json go run ./cmd/mxgw-go advise -session-id -server-handle 1 -item-handle 1 -plaintext -json go run ./cmd/mxgw-go write -session-id -server-handle 1 -item-handle 1 -type int32 -value 123 -plaintext -json go run ./cmd/mxgw-go stream-events -session-id -plaintext -json go run ./cmd/mxgw-go smoke -item Area001.Tag.Value -plaintext -json go run ./cmd/mxgw-go galaxy-test-connection -plaintext -json go run ./cmd/mxgw-go galaxy-last-deploy -plaintext -json go run ./cmd/mxgw-go galaxy-discover -plaintext -json go run ./cmd/mxgw-go galaxy-watch -plaintext -json ``` Use `-api-key-env MXGATEWAY_API_KEY` or `-api-key ` when authentication is enabled. CLI output redacts the key value and never writes the raw secret. Use TLS options for a secured gateway: ```powershell go run ./cmd/mxgw-go smoke -endpoint mxgateway.example.local:5001 -ca-cert C:\certs\mxgateway-ca.pem -server-name-override mxgateway.example.local -api-key-env MXGATEWAY_API_KEY -item Area001.Tag.Value -json ``` ## Integration Checks Run live checks only when a gateway and MXAccess-backed worker are available: ```powershell $env:MXGATEWAY_INTEGRATION = '1' $env:MXGATEWAY_ENDPOINT = 'localhost:5000' $env:MXGATEWAY_API_KEY = '' $env:MXGATEWAY_TEST_ITEM = 'Area001.Tag.Value' go run ./cmd/mxgw-go smoke -endpoint $env:MXGATEWAY_ENDPOINT -plaintext -api-key-env MXGATEWAY_API_KEY -item $env:MXGATEWAY_TEST_ITEM -json ``` ## Installing the Go client The module is resolved directly from the git repo — no package registry: ````bash go get gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go@v0.1.0 ```` Then import: ````go import "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/mxgateway" ```` If your build environment cannot reach `gitea.dohertylan.com` directly, configure `GOPROXY` to point at an internal proxy that fronts the Gitea repo, or use `GONOSUMCHECK` + `GOPRIVATE` to bypass the checksum database for the internal module path. ## Releasing a new version Go modules in monorepo subdirectories use prefixed tags. To tag a release from this repo: ````bash pwsh scripts/tag-go-module.ps1 -Version v0.1.1 -Push ```` The script validates semver, refuses to tag with uncommitted tracked changes, creates an annotated tag `clients/go/v0.1.1`, and (with `-Push`) pushes it to origin. ## Related Documentation - [Client Packaging](../../docs/ClientPackaging.md) - [Client Proto Generation](../../docs/ClientProtoGeneration.md) - [Go Client Detailed Design](./GoClientDesign.md)