SCU — sending
An SCU initiates the association. ae.Dial negotiates it and hands back an *ae.Association; every DIMSE service is a method on that.
import "github.com/godicom-dev/gonetdicom/ae"
assoc, err := ae.Dial(ctx, cfg, "pacs.example:11112", "ANY-SCP")The last argument is the called AE title — who you are talking to. Your own is cfg.AETitle.
Verification (C-ECHO)
The whole of it, and the first thing to try against a new peer:
package main
import (
"context"
"log"
"time"
"github.com/godicom-dev/gonetdicom/ae"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
assoc, err := ae.Dial(ctx, ae.Config{AETitle: "MYSCU"}, "pacs.example:11112", "ANY-SCP")
if err != nil {
log.Fatal(err)
}
defer assoc.Abort()
if err := assoc.CEcho(ctx); err != nil {
log.Fatal(err)
}
if err := assoc.Release(ctx); err != nil {
log.Fatal(err)
}
}Release is the orderly close; Abort is the abrupt one. Deferring Abort and calling Release on the happy path means a panic or an early return still tears the association down.
Storage (C-STORE)
You must propose a presentation context that covers what you are sending — the SOP Class as abstract syntax, and at least one transfer syntax:
cfg := ae.Config{
AETitle: "STORESCU",
PresentationContexts: []ae.PresentationContext{{
ID: 1,
AbstractSyntax: "1.2.840.10008.5.1.4.1.1.7", // Secondary Capture
TransferSyntaxes: []string{"1.2.840.10008.1.2"},
}},
}
assoc, err := ae.Dial(ctx, cfg, "pacs.example:11112", "ANY-SCP")
if err != nil {
log.Fatal(err)
}
defer assoc.Abort()
res, err := assoc.CStore(ctx, ae.StoreRequest{
AffectedSOPClassUID: "1.2.840.10008.5.1.4.1.1.7",
AffectedSOPInstanceUID: "1.2.3.4.5",
Data: ds, // a godicom Dataset, or pre-encoded bytes
})AffectedSOPInstanceUID is optional — it can come from the dataset's SOPInstanceUID, or from ae.NewInstanceUID() when you are generating one.
Query and retrieve
C-FIND
matches, err := assoc.CFind(ctx, ae.FindRequest{
QueryModel: ae.PatientRootQueryRetrieveInformationModelFind,
IdentifierData: query, // a Dataset holding the matching and return keys
})C-MOVE
The peer sends the instances to a third AE, named by title. That AE has to be one the peer already knows how to reach:
matches, err := assoc.CMove(ctx, ae.MoveRequest{
QueryModel: ae.PatientRootQueryRetrieveInformationModelMove,
MoveDestination: "STORESCP",
IdentifierData: query,
})C-GET
The peer sends the instances back over the same association, which means your SCU has to act as a C-STORE SCP for the duration:
matches, err := assoc.CGet(ctx, ae.GetRequest{
QueryModel: ae.PatientRootQueryRetrieveInformationModelGet,
IdentifierData: query,
OnCStore: func(_ context.Context, req ae.StoreRequest) uint16 {
_ = req.Data // a decoded Dataset
return status.Success
},
})Role selection is not optional in practice
Against a real PACS, C-GET also needs SCP/SCU Role Selection proposed, so the peer is willing to send C-STORE to the requestor:
cfg := ae.Config{
AETitle: "GETSCU",
PresentationContexts: []ae.PresentationContext{ /* Get model + storage SOP Class */ },
RoleSelections: []pdu.RoleSelection{
ae.BuildRole(string(uid.CTImageStorage), false, true), // requestor as SCP
},
}Without it, many implementations will negotiate the association and then simply never send anything.
Cancelling
err := assoc.CCancel(ctx, msgID, 0, ae.PatientRootQueryRetrieveInformationModelFind)Works against an outstanding C-FIND, C-MOVE or C-GET, and is safe to call from another goroutine while that call is still blocked. The third argument is the presentation context ID; pass 0 and the fourth argument — the query model's abstract syntax — resolves it for you.
DIMSE-N and Storage Commitment
res, err := assoc.NAction(ctx, ae.ActionRequest{
RequestedSOPClassUID: ae.StorageCommitmentPushModelSOPClass,
RequestedSOPInstanceUID: ae.StorageCommitmentPushModelSOPInstance,
ActionTypeID: dimse.StorageCommitmentActionTypeRequest,
ActionInformationData: info,
OnNEventReport: func(_ context.Context, req ae.EventReportRequest) uint16 {
return status.Success
},
})The commitment result may come back on the same association — handled by OnNEventReport above — or later, on a new one. For that case, see EventReportRequest.AsyncDestination.
NGet, NSet, NCreate, NDelete and NEventReport are available on the same association.
TLS and timeouts
assoc, err := ae.Dial(ctx, ae.Config{
AETitle: "MYSCU",
IdleTimeout: 30 * time.Second,
TLS: &tls.Config{ServerName: "pacs.example", MinVersion: tls.VersionTLS12},
Logger: logger,
}, "pacs.example:2762", "ANY-SCP")Port 2762 is the registered DICOM-TLS port, as 11112 is the registered plain one.
User identity negotiation
assoc, err := ae.Dial(ctx, ae.Config{
AETitle: "IDSCU",
UserIdentity: ae.UsernamePasscodeIdentity("alice", "secret", false),
}, addr, "IDSCP")The final argument asks for a positive response from the peer. See SCP — serving for the other end.
Status codes
DIMSE statuses are uint16 values with meanings that vary by service. Use the named constants in status rather than literals — status.Success, status.ProcessingFailure, and the rest.