Skip to content

Commit feb5a73

Browse files
andigclaude
andcommitted
feat: add protocol multiplexing support for OCPP 1.6 and 2.0.1
Enable running both OCPP 1.6 and OCPP 2.0.1 servers on a single WebSocket port using subprotocol-based message routing. Changes: - ws.Server: Add SetMessageHandlerForSubprotocol for per-subprotocol routing - ws.Server: Make Start() idempotent for shared server instances - ocppj.Server: Add SetSubprotocol to register protocol-specific handlers - ocpp1.6/ocpp2.0.1: Auto-set subprotocol in constructors - New multiplex package with MultiProtocolServer for unified API Fixes #417 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent b7f92ee commit feb5a73

12 files changed

Lines changed: 716 additions & 37 deletions

File tree

example/multiplex/main.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Example demonstrating protocol multiplexing for OCPP 1.6 and 2.0.1 on a single port.
2+
//
3+
// This server can handle both OCPP 1.6 charge points and OCPP 2.0.1 charging stations
4+
// simultaneously using WebSocket subprotocol negotiation.
5+
//
6+
// When a client connects with "Sec-WebSocket-Protocol: ocpp2.0.1, ocpp1.6",
7+
// the server negotiates the first mutually-supported protocol.
8+
package main
9+
10+
import (
11+
"fmt"
12+
"time"
13+
14+
"github.com/lorenzodonini/ocpp-go/multiplex"
15+
"github.com/lorenzodonini/ocpp-go/ocpp1.6/core"
16+
"github.com/lorenzodonini/ocpp-go/ocpp1.6/types"
17+
"github.com/lorenzodonini/ocpp-go/ocpp2.0.1/provisioning"
18+
types2 "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/types"
19+
"github.com/lorenzodonini/ocpp-go/ws"
20+
)
21+
22+
// OCPP 1.6 handler
23+
type ocpp16Handler struct{}
24+
25+
func (h *ocpp16Handler) OnAuthorize(chargePointId string, request *core.AuthorizeRequest) (*core.AuthorizeConfirmation, error) {
26+
fmt.Printf("[OCPP 1.6] Authorize from %s: %s\n", chargePointId, request.IdTag)
27+
return core.NewAuthorizationConfirmation(types.NewIdTagInfo(types.AuthorizationStatusAccepted)), nil
28+
}
29+
30+
func (h *ocpp16Handler) OnBootNotification(chargePointId string, request *core.BootNotificationRequest) (*core.BootNotificationConfirmation, error) {
31+
fmt.Printf("[OCPP 1.6] BootNotification from %s: %s %s\n", chargePointId, request.ChargePointVendor, request.ChargePointModel)
32+
return core.NewBootNotificationConfirmation(types.NewDateTime(time.Now()), 60, core.RegistrationStatusAccepted), nil
33+
}
34+
35+
func (h *ocpp16Handler) OnDataTransfer(chargePointId string, request *core.DataTransferRequest) (*core.DataTransferConfirmation, error) {
36+
fmt.Printf("[OCPP 1.6] DataTransfer from %s\n", chargePointId)
37+
return core.NewDataTransferConfirmation(core.DataTransferStatusAccepted), nil
38+
}
39+
40+
func (h *ocpp16Handler) OnHeartbeat(chargePointId string, request *core.HeartbeatRequest) (*core.HeartbeatConfirmation, error) {
41+
fmt.Printf("[OCPP 1.6] Heartbeat from %s\n", chargePointId)
42+
return core.NewHeartbeatConfirmation(types.NewDateTime(time.Now())), nil
43+
}
44+
45+
func (h *ocpp16Handler) OnMeterValues(chargePointId string, request *core.MeterValuesRequest) (*core.MeterValuesConfirmation, error) {
46+
fmt.Printf("[OCPP 1.6] MeterValues from %s\n", chargePointId)
47+
return core.NewMeterValuesConfirmation(), nil
48+
}
49+
50+
func (h *ocpp16Handler) OnStartTransaction(chargePointId string, request *core.StartTransactionRequest) (*core.StartTransactionConfirmation, error) {
51+
fmt.Printf("[OCPP 1.6] StartTransaction from %s\n", chargePointId)
52+
return core.NewStartTransactionConfirmation(types.NewIdTagInfo(types.AuthorizationStatusAccepted), 1), nil
53+
}
54+
55+
func (h *ocpp16Handler) OnStatusNotification(chargePointId string, request *core.StatusNotificationRequest) (*core.StatusNotificationConfirmation, error) {
56+
fmt.Printf("[OCPP 1.6] StatusNotification from %s: connector %d is %s\n", chargePointId, request.ConnectorId, request.Status)
57+
return core.NewStatusNotificationConfirmation(), nil
58+
}
59+
60+
func (h *ocpp16Handler) OnStopTransaction(chargePointId string, request *core.StopTransactionRequest) (*core.StopTransactionConfirmation, error) {
61+
fmt.Printf("[OCPP 1.6] StopTransaction from %s\n", chargePointId)
62+
return core.NewStopTransactionConfirmation(), nil
63+
}
64+
65+
// OCPP 2.0.1 handler
66+
type ocpp201Handler struct{}
67+
68+
func (h *ocpp201Handler) OnBootNotification(chargingStationId string, request *provisioning.BootNotificationRequest) (*provisioning.BootNotificationResponse, error) {
69+
fmt.Printf("[OCPP 2.0.1] BootNotification from %s: %s %s\n", chargingStationId, request.ChargingStation.VendorName, request.ChargingStation.Model)
70+
return provisioning.NewBootNotificationResponse(types2.NewDateTime(time.Now()), 60, provisioning.RegistrationStatusAccepted), nil
71+
}
72+
73+
func (h *ocpp201Handler) OnNotifyReport(chargingStationId string, request *provisioning.NotifyReportRequest) (*provisioning.NotifyReportResponse, error) {
74+
fmt.Printf("[OCPP 2.0.1] NotifyReport from %s\n", chargingStationId)
75+
return provisioning.NewNotifyReportResponse(), nil
76+
}
77+
78+
func main() {
79+
// Create multi-protocol server
80+
server := multiplex.NewMultiProtocolServer()
81+
82+
// Register OCPP 1.6 handlers
83+
server.OCPP16Server().SetCoreHandler(&ocpp16Handler{})
84+
85+
// Register OCPP 2.0.1 handlers
86+
server.OCPP201Server().SetProvisioningHandler(&ocpp201Handler{})
87+
88+
// Track connections and their protocol versions
89+
server.SetNewClientHandler(func(channel ws.Channel) {
90+
fmt.Printf("New client connected: %s (protocol: %s)\n", channel.ID(), channel.Subprotocol())
91+
})
92+
93+
server.SetDisconnectedClientHandler(func(channel ws.Channel) {
94+
fmt.Printf("Client disconnected: %s\n", channel.ID())
95+
})
96+
97+
// Start listening on port 8080
98+
fmt.Println("Starting multi-protocol OCPP server on :8080/ocpp/{id}")
99+
fmt.Println("Supported protocols: ocpp1.6, ocpp2.0.1")
100+
fmt.Println()
101+
fmt.Println("Clients can connect with either protocol. The server will negotiate")
102+
fmt.Println("based on the Sec-WebSocket-Protocol header.")
103+
104+
server.Start(8080, "/ocpp/{id}")
105+
}

multiplex/multiplex.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Package multiplex provides protocol multiplexing support for OCPP servers.
2+
// It allows a single WebSocket server to handle both OCPP 1.6 and OCPP 2.0.1 clients
3+
// on the same port, using WebSocket subprotocol negotiation to route connections
4+
// to the appropriate handler.
5+
package multiplex
6+
7+
import (
8+
ocpp16 "github.com/lorenzodonini/ocpp-go/ocpp1.6"
9+
ocpp2 "github.com/lorenzodonini/ocpp-go/ocpp2.0.1"
10+
"github.com/lorenzodonini/ocpp-go/ws"
11+
)
12+
13+
// ProtocolVersion represents an OCPP protocol version.
14+
type ProtocolVersion string
15+
16+
const (
17+
// V16 represents OCPP 1.6
18+
V16 ProtocolVersion = "ocpp1.6"
19+
// V201 represents OCPP 2.0.1
20+
V201 ProtocolVersion = "ocpp2.0.1"
21+
)
22+
23+
// Subprotocol constants for WebSocket negotiation.
24+
const (
25+
V16Subprotocol = "ocpp1.6"
26+
V201Subprotocol = "ocpp2.0.1"
27+
)
28+
29+
// SubprotocolSelector is a callback function that allows the application to choose
30+
// which subprotocol to use when a client requests multiple subprotocols.
31+
//
32+
// Parameters:
33+
// - clientID: The identifier of the connecting client (extracted from the URL path)
34+
// - requestedSubprotocols: The list of subprotocols requested by the client
35+
// (from the Sec-WebSocket-Protocol header, e.g., ["ocpp2.0.1", "ocpp1.6"])
36+
//
37+
// Returns the subprotocol to use for this connection (e.g., "ocpp1.6" or "ocpp2.0.1").
38+
// The returned value must be one of the supported subprotocols.
39+
// If an empty string is returned, the default behavior is used (first mutually-supported protocol).
40+
//
41+
// Example:
42+
//
43+
// server.SetSubprotocolSelector(func(clientID string, requested []string) string {
44+
// // Always prefer OCPP 2.0.1 if the client supports it
45+
// for _, p := range requested {
46+
// if p == multiplex.V201Subprotocol {
47+
// return p
48+
// }
49+
// }
50+
// // Fall back to first requested
51+
// if len(requested) > 0 {
52+
// return requested[0]
53+
// }
54+
// return ""
55+
// })
56+
type SubprotocolSelector func(clientID string, requestedSubprotocols []string) string
57+
58+
// MultiProtocolServer is a server that can handle both OCPP 1.6 and OCPP 2.0.1 clients
59+
// on a single port. It uses WebSocket subprotocol negotiation to determine which
60+
// protocol version each client uses.
61+
type MultiProtocolServer interface {
62+
// Start begins listening for connections on the specified port and path.
63+
// The function blocks until Stop is called.
64+
//
65+
// Example:
66+
// go server.Start(8080, "/ocpp/{id}")
67+
Start(port int, listenPath string)
68+
69+
// Stop gracefully shuts down the server.
70+
Stop()
71+
72+
// OCPP16Server returns the underlying OCPP 1.6 Central System.
73+
// Use this to register handlers for OCPP 1.6 messages.
74+
OCPP16Server() ocpp16.CentralSystem
75+
76+
// OCPP201Server returns the underlying OCPP 2.0.1 CSMS.
77+
// Use this to register handlers for OCPP 2.0.1 messages.
78+
OCPP201Server() ocpp2.CSMS
79+
80+
// SetSubprotocolSelector sets a custom callback for choosing which subprotocol
81+
// to use when a client requests multiple subprotocols. This allows the application
82+
// to implement custom protocol selection logic (e.g., prefer OCPP 2.0.1 over 1.6).
83+
// If not set, the default behavior is used (first mutually-supported protocol).
84+
SetSubprotocolSelector(selector SubprotocolSelector)
85+
86+
// SetNewClientHandler sets a callback for all new client connections,
87+
// regardless of protocol version. The callback receives the WebSocket channel
88+
// which can be used to determine the protocol via channel.Subprotocol().
89+
SetNewClientHandler(handler func(channel ws.Channel))
90+
91+
// SetDisconnectedClientHandler sets a callback for all client disconnections,
92+
// regardless of protocol version.
93+
SetDisconnectedClientHandler(handler func(channel ws.Channel))
94+
95+
// SetBasicAuthHandler enables HTTP Basic Authentication for all connections.
96+
SetBasicAuthHandler(handler func(username, password string) bool)
97+
98+
// SetCheckClientHandler sets a validation handler for incoming connections.
99+
// Return false to reject the connection.
100+
SetCheckClientHandler(handler ws.CheckClientHandler)
101+
}

0 commit comments

Comments
 (0)