From a64680ef70dfef858f299ca4d8f174804f4054ec Mon Sep 17 00:00:00 2001 From: shiv3 Date: Thu, 18 Jun 2026 20:10:12 +0900 Subject: [PATCH] docs: lead with the handlers package over csms.On in README and examples The typed handler interfaces + RegisterCSMS/RegisterCP are the recommended way to register handlers, so make the README quickstart and the CSMS examples use them: embed UnimplementedCSMSHandler and implement only the messages you handle. - README CSMS quickstart uses v16h.RegisterCSMS; csms.On kept as a documented single-handler/dynamic alternative. - csms-minimal, migration-after: small handler structs. - csms-full: *app implements CSMSHandler (embeds UnimplementedCSMSHandler); the per-message closures become methods registered in one RegisterCSMS call. --- README.md | 23 ++++- examples/csms-full/main.go | 163 +++++++++++++++++-------------- examples/csms-minimal/main.go | 26 +++-- examples/migration-after/main.go | 34 ++++--- 4 files changed, 140 insertions(+), 106 deletions(-) diff --git a/README.md b/README.md index dce87cc..01fe57d 100644 --- a/README.md +++ b/README.md @@ -34,19 +34,32 @@ Requires Go 1.25+. ### CSMS (central system) ```go -srv := csms.NewServer(csms.WithSubProtocols("ocpp2.1", "ocpp2.0.1", "ocpp1.6")) +import v16h "github.com/shiv3/gocpp/v16/handlers" -csms.On(srv, v16p.BootNotification, func(ctx context.Context, c *csms.Conn, req v16msg.BootNotificationRequest) (v16msg.BootNotificationResponse, error) { +// Embed Unimplemented and override only the messages you handle; the rest +// return a NotSupported CallError automatically. +type csmsHandler struct{ v16h.UnimplementedCSMSHandler } + +func (csmsHandler) OnBootNotification(ctx context.Context, c *csms.Conn, req v16msg.BootNotificationRequest) (v16msg.BootNotificationResponse, error) { return v16msg.BootNotificationResponse{ Status: v16msg.RegistrationStatusAccepted, CurrentTime: time.Now(), Interval: 300, }, nil -}) - -log.Fatal(srv.ListenAndServe(":8080")) // ws://host:8080/ocpp/{cpId} +} + +func main() { + srv := csms.NewServer(csms.WithSubProtocols("ocpp2.1", "ocpp2.0.1", "ocpp1.6")) + if err := v16h.RegisterCSMS(srv, csmsHandler{}); err != nil { + log.Fatal(err) + } + log.Fatal(srv.ListenAndServe(":8080")) // ws://host:8080/ocpp/{cpId} +} ``` +> Need just one message, or dynamic registration? The lower-level +> `csms.On(srv, v16p.BootNotification, handlerFunc)` registers a single handler. + ### Charge point (client) ```go diff --git a/examples/csms-full/main.go b/examples/csms-full/main.go index 2e8f8a9..97ba740 100644 --- a/examples/csms-full/main.go +++ b/examples/csms-full/main.go @@ -40,8 +40,8 @@ import ( "github.com/shiv3/gocpp/core/storage/memory" "github.com/shiv3/gocpp/csms" v16client "github.com/shiv3/gocpp/v16/client" + v16h "github.com/shiv3/gocpp/v16/handlers" v16msg "github.com/shiv3/gocpp/v16/messages" - v16p "github.com/shiv3/gocpp/v16/profiles" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" @@ -194,6 +194,11 @@ func setupOTel(ctx context.Context) (trace.TracerProvider, observability.Metrics } type app struct { + // Embedding UnimplementedCSMSHandler makes *app a complete v16h.CSMSHandler: + // the messages whose On* methods are defined below are handled, and every + // other CP->CSMS message returns a NotSupported CallError. + v16h.UnimplementedCSMSHandler + logger *slog.Logger srv *csms.Server txStore storage.TransactionStore @@ -203,84 +208,90 @@ type app struct { txCounter atomic.Int32 } +// idTagAccepted/idTagInvalid are the canned authorization results shared by the +// Authorize/StartTransaction/StopTransaction handlers. +var ( + idTagAccepted = v16msg.IDTagInfo{Status: v16msg.IDTagInfoStatusAccepted} + idTagInvalid = v16msg.IDTagInfo{Status: v16msg.IDTagInfoStatusInvalid} +) + +// registerHandlers wires every CP->CSMS handler *app implements in one call. +// ChangeConfiguration/GetConfiguration are SentByCSMS (CSMS -> CP); the CSMS +// *sends* those (via v16client.NewCSMS(conn)), so they are not handlers here. func (a *app) registerHandlers() { - accepted := v16msg.IDTagInfo{Status: v16msg.IDTagInfoStatusAccepted} - invalid := v16msg.IDTagInfo{Status: v16msg.IDTagInfoStatusInvalid} + must(v16h.RegisterCSMS(a.srv, a)) +} - must(csms.On(a.srv, v16p.BootNotification, func(ctx context.Context, c *csms.Conn, req v16msg.BootNotificationRequest) (v16msg.BootNotificationResponse, error) { - a.logger.Info("BootNotification", "cp", c.ID(), "vendor", req.ChargePointVendor, "model", req.ChargePointModel) - if a.autoRemoteStart { - go a.driveRemoteStart(c) - } - return v16msg.BootNotificationResponse{CurrentTime: time.Now().UTC(), Interval: 300, Status: v16msg.RegistrationStatusAccepted}, nil - })) - - must(csms.On(a.srv, v16p.Heartbeat, func(ctx context.Context, c *csms.Conn, req v16msg.HeartbeatRequest) (v16msg.HeartbeatResponse, error) { - a.logger.Info("Heartbeat", "cp", c.ID()) - return v16msg.HeartbeatResponse{CurrentTime: time.Now().UTC()}, nil - })) - - must(csms.On(a.srv, v16p.StatusNotification, func(ctx context.Context, c *csms.Conn, req v16msg.StatusNotificationRequest) (v16msg.StatusNotificationResponse, error) { - a.logger.Info("StatusNotification", "cp", c.ID(), "connector", req.ConnectorID, "status", req.Status) - return v16msg.StatusNotificationResponse{}, nil - })) - - must(csms.On(a.srv, v16p.Authorize, func(ctx context.Context, c *csms.Conn, req v16msg.AuthorizeRequest) (v16msg.AuthorizeResponse, error) { - a.logger.Info("Authorize", "cp", c.ID(), "idTag", req.IDTag) - if !knownIDTag(req.IDTag) { - return v16msg.AuthorizeResponse{IDTagInfo: invalid}, nil - } - return v16msg.AuthorizeResponse{IDTagInfo: accepted}, nil - })) - - must(csms.On(a.srv, v16p.StartTransaction, func(ctx context.Context, c *csms.Conn, req v16msg.StartTransactionRequest) (v16msg.StartTransactionResponse, error) { - txID := a.txCounter.Add(1) - err := a.txStore.Begin(ctx, storage.Transaction{ - ID: itoa(txID), - CPID: c.ID(), - EVSEID: int(req.ConnectorID), - IDTag: req.IDTag, - StartedAt: time.Now().UTC(), - MeterStart: int(req.MeterStart), - Status: storage.TransactionActive, - }) - if err != nil { - a.logger.Error("tx begin", "err", err) - } - a.logger.Info("StartTransaction", "cp", c.ID(), "tx", txID, "idTag", req.IDTag, "meterStart", req.MeterStart) - return v16msg.StartTransactionResponse{TransactionID: txID, IDTagInfo: accepted}, nil - })) +func (a *app) OnBootNotification(ctx context.Context, c *csms.Conn, req v16msg.BootNotificationRequest) (v16msg.BootNotificationResponse, error) { + a.logger.Info("BootNotification", "cp", c.ID(), "vendor", req.ChargePointVendor, "model", req.ChargePointModel) + if a.autoRemoteStart { + go a.driveRemoteStart(c) + } + return v16msg.BootNotificationResponse{CurrentTime: time.Now().UTC(), Interval: 300, Status: v16msg.RegistrationStatusAccepted}, nil +} - must(csms.On(a.srv, v16p.MeterValues, func(ctx context.Context, c *csms.Conn, req v16msg.MeterValuesRequest) (v16msg.MeterValuesResponse, error) { - if req.TransactionID != nil && len(req.MeterValue) > 0 { - a.logger.Info("MeterValues", "cp", c.ID(), "tx", *req.TransactionID, "samples", len(req.MeterValue)) - } - return v16msg.MeterValuesResponse{}, nil - })) - - must(csms.On(a.srv, v16p.StopTransaction, func(ctx context.Context, c *csms.Conn, req v16msg.StopTransactionRequest) (v16msg.StopTransactionResponse, error) { - txID := itoa(req.TransactionID) - _ = a.txStore.End(ctx, txID, storage.TransactionEnd{EndedAt: time.Now().UTC(), MeterStop: int(req.MeterStop), Status: storage.TransactionCompleted}) - a.logger.Info("StopTransaction", "cp", c.ID(), "tx", req.TransactionID, "meterStop", req.MeterStop) - return v16msg.StopTransactionResponse{IDTagInfo: &accepted}, nil - })) - - must(csms.On(a.srv, v16p.DataTransfer, func(ctx context.Context, c *csms.Conn, req v16msg.DataTransferRequest) (v16msg.DataTransferResponse, error) { - return v16msg.DataTransferResponse{Status: "Accepted"}, nil - })) - - must(csms.On(a.srv, v16p.DiagnosticsStatusNotification, func(ctx context.Context, c *csms.Conn, req v16msg.DiagnosticsStatusNotificationRequest) (v16msg.DiagnosticsStatusNotificationResponse, error) { - a.logger.Info("DiagnosticsStatusNotification", "cp", c.ID(), "status", req.Status) - return v16msg.DiagnosticsStatusNotificationResponse{}, nil - })) - - must(csms.On(a.srv, v16p.FirmwareStatusNotification, func(ctx context.Context, c *csms.Conn, req v16msg.FirmwareStatusNotificationRequest) (v16msg.FirmwareStatusNotificationResponse, error) { - a.logger.Info("FirmwareStatusNotification", "cp", c.ID(), "status", req.Status) - return v16msg.FirmwareStatusNotificationResponse{}, nil - })) - // Note: ChangeConfiguration/GetConfiguration are SentByCSMS (CSMS -> CP); the CSMS - // *sends* them (via v16client.NewCSMS(conn)), so they are not registered as - // inbound handlers here. +func (a *app) OnHeartbeat(ctx context.Context, c *csms.Conn, req v16msg.HeartbeatRequest) (v16msg.HeartbeatResponse, error) { + a.logger.Info("Heartbeat", "cp", c.ID()) + return v16msg.HeartbeatResponse{CurrentTime: time.Now().UTC()}, nil +} + +func (a *app) OnStatusNotification(ctx context.Context, c *csms.Conn, req v16msg.StatusNotificationRequest) (v16msg.StatusNotificationResponse, error) { + a.logger.Info("StatusNotification", "cp", c.ID(), "connector", req.ConnectorID, "status", req.Status) + return v16msg.StatusNotificationResponse{}, nil +} + +func (a *app) OnAuthorize(ctx context.Context, c *csms.Conn, req v16msg.AuthorizeRequest) (v16msg.AuthorizeResponse, error) { + a.logger.Info("Authorize", "cp", c.ID(), "idTag", req.IDTag) + if !knownIDTag(req.IDTag) { + return v16msg.AuthorizeResponse{IDTagInfo: idTagInvalid}, nil + } + return v16msg.AuthorizeResponse{IDTagInfo: idTagAccepted}, nil +} + +func (a *app) OnStartTransaction(ctx context.Context, c *csms.Conn, req v16msg.StartTransactionRequest) (v16msg.StartTransactionResponse, error) { + txID := a.txCounter.Add(1) + err := a.txStore.Begin(ctx, storage.Transaction{ + ID: itoa(txID), + CPID: c.ID(), + EVSEID: int(req.ConnectorID), + IDTag: req.IDTag, + StartedAt: time.Now().UTC(), + MeterStart: int(req.MeterStart), + Status: storage.TransactionActive, + }) + if err != nil { + a.logger.Error("tx begin", "err", err) + } + a.logger.Info("StartTransaction", "cp", c.ID(), "tx", txID, "idTag", req.IDTag, "meterStart", req.MeterStart) + return v16msg.StartTransactionResponse{TransactionID: txID, IDTagInfo: idTagAccepted}, nil +} + +func (a *app) OnMeterValues(ctx context.Context, c *csms.Conn, req v16msg.MeterValuesRequest) (v16msg.MeterValuesResponse, error) { + if req.TransactionID != nil && len(req.MeterValue) > 0 { + a.logger.Info("MeterValues", "cp", c.ID(), "tx", *req.TransactionID, "samples", len(req.MeterValue)) + } + return v16msg.MeterValuesResponse{}, nil +} + +func (a *app) OnStopTransaction(ctx context.Context, c *csms.Conn, req v16msg.StopTransactionRequest) (v16msg.StopTransactionResponse, error) { + txID := itoa(req.TransactionID) + _ = a.txStore.End(ctx, txID, storage.TransactionEnd{EndedAt: time.Now().UTC(), MeterStop: int(req.MeterStop), Status: storage.TransactionCompleted}) + a.logger.Info("StopTransaction", "cp", c.ID(), "tx", req.TransactionID, "meterStop", req.MeterStop) + return v16msg.StopTransactionResponse{IDTagInfo: &idTagAccepted}, nil +} + +func (a *app) OnDataTransfer(ctx context.Context, c *csms.Conn, req v16msg.DataTransferRequest) (v16msg.DataTransferResponse, error) { + return v16msg.DataTransferResponse{Status: "Accepted"}, nil +} + +func (a *app) OnDiagnosticsStatusNotification(ctx context.Context, c *csms.Conn, req v16msg.DiagnosticsStatusNotificationRequest) (v16msg.DiagnosticsStatusNotificationResponse, error) { + a.logger.Info("DiagnosticsStatusNotification", "cp", c.ID(), "status", req.Status) + return v16msg.DiagnosticsStatusNotificationResponse{}, nil +} + +func (a *app) OnFirmwareStatusNotification(ctx context.Context, c *csms.Conn, req v16msg.FirmwareStatusNotificationRequest) (v16msg.FirmwareStatusNotificationResponse, error) { + a.logger.Info("FirmwareStatusNotification", "cp", c.ID(), "status", req.Status) + return v16msg.FirmwareStatusNotificationResponse{}, nil } func knownIDTag(idTag string) bool { diff --git a/examples/csms-minimal/main.go b/examples/csms-minimal/main.go index 3fbaa02..7887365 100644 --- a/examples/csms-minimal/main.go +++ b/examples/csms-minimal/main.go @@ -8,25 +8,31 @@ import ( "time" "github.com/shiv3/gocpp/csms" + v16h "github.com/shiv3/gocpp/v16/handlers" v16msg "github.com/shiv3/gocpp/v16/messages" - v16p "github.com/shiv3/gocpp/v16/profiles" ) +// handler implements the CP->CSMS messages this server cares about. Embedding +// UnimplementedCSMSHandler makes every other message return a NotSupported +// CallError automatically. +type handler struct{ v16h.UnimplementedCSMSHandler } + +func (handler) OnBootNotification(ctx context.Context, c *csms.Conn, req v16msg.BootNotificationRequest) (v16msg.BootNotificationResponse, error) { + slog.Info("boot", "cp", c.ID(), "vendor", req.ChargePointVendor) + return v16msg.BootNotificationResponse{Status: v16msg.RegistrationStatusAccepted, CurrentTime: time.Now(), Interval: 300}, nil +} + +func (handler) OnHeartbeat(ctx context.Context, c *csms.Conn, req v16msg.HeartbeatRequest) (v16msg.HeartbeatResponse, error) { + return v16msg.HeartbeatResponse{CurrentTime: time.Now()}, nil +} + func main() { srv := csms.NewServer( csms.WithSubProtocols("ocpp1.6"), csms.WithLogger(slog.Default()), ) - if err := csms.On(srv, v16p.BootNotification, func(ctx context.Context, c *csms.Conn, req v16msg.BootNotificationRequest) (v16msg.BootNotificationResponse, error) { - slog.Info("boot", "cp", c.ID(), "vendor", req.ChargePointVendor) - return v16msg.BootNotificationResponse{Status: v16msg.RegistrationStatusAccepted, CurrentTime: time.Now(), Interval: 300}, nil - }); err != nil { - log.Fatal(err) - } - if err := csms.On(srv, v16p.Heartbeat, func(ctx context.Context, c *csms.Conn, req v16msg.HeartbeatRequest) (v16msg.HeartbeatResponse, error) { - return v16msg.HeartbeatResponse{CurrentTime: time.Now()}, nil - }); err != nil { + if err := v16h.RegisterCSMS(srv, handler{}); err != nil { log.Fatal(err) } diff --git a/examples/migration-after/main.go b/examples/migration-after/main.go index 334076e..ef7b4b0 100644 --- a/examples/migration-after/main.go +++ b/examples/migration-after/main.go @@ -10,30 +10,34 @@ import ( "time" "github.com/shiv3/gocpp/csms" + v16h "github.com/shiv3/gocpp/v16/handlers" v16msg "github.com/shiv3/gocpp/v16/messages" - v16p "github.com/shiv3/gocpp/v16/profiles" ) +// handler mirrors ocpp-go's CentralSystemHandler interface: implement the +// messages you care about and embed UnimplementedCSMSHandler for the rest. +type handler struct{ v16h.UnimplementedCSMSHandler } + +func (handler) OnBootNotification(ctx context.Context, c *csms.Conn, req v16msg.BootNotificationRequest) (v16msg.BootNotificationResponse, error) { + slog.Info("boot", "cp", c.ID(), "vendor", req.ChargePointVendor) + return v16msg.BootNotificationResponse{ + Status: v16msg.RegistrationStatusAccepted, + CurrentTime: time.Now(), + Interval: 300, + }, nil +} + +func (handler) OnHeartbeat(ctx context.Context, c *csms.Conn, req v16msg.HeartbeatRequest) (v16msg.HeartbeatResponse, error) { + return v16msg.HeartbeatResponse{CurrentTime: time.Now()}, nil +} + func main() { srv := csms.NewServer( csms.WithSubProtocols("ocpp1.6"), csms.WithLogger(slog.Default()), ) - if err := csms.On(srv, v16p.BootNotification, func(ctx context.Context, c *csms.Conn, req v16msg.BootNotificationRequest) (v16msg.BootNotificationResponse, error) { - slog.Info("boot", "cp", c.ID(), "vendor", req.ChargePointVendor) - return v16msg.BootNotificationResponse{ - Status: v16msg.RegistrationStatusAccepted, - CurrentTime: time.Now(), - Interval: 300, - }, nil - }); err != nil { - log.Fatal(err) - } - - if err := csms.On(srv, v16p.Heartbeat, func(ctx context.Context, c *csms.Conn, req v16msg.HeartbeatRequest) (v16msg.HeartbeatResponse, error) { - return v16msg.HeartbeatResponse{CurrentTime: time.Now()}, nil - }); err != nil { + if err := v16h.RegisterCSMS(srv, handler{}); err != nil { log.Fatal(err) }