package ocpp import ( "crypto/rand" "encoding/hex" "encoding/json" "errors" "fmt" ) // OCPP-J message types (OCPP 1.6 ยง4.2). Every frame is a JSON array whose first // element is one of these. const ( MessageTypeCall = 2 // [2, id, action, payload] MessageTypeCallResult = 3 // [3, id, payload] MessageTypeCallError = 4 // [4, id, errorCode, errorDescription, errorDetails] ) // Standard OCPP-J error codes used when rejecting a CALL. const ( ErrNotImplemented = "NotImplemented" ErrNotSupported = "NotSupported" ErrInternalError = "InternalError" ErrProtocolError = "ProtocolError" ErrSecurityError = "SecurityError" ErrFormationViolation = "FormationViolation" ErrPropertyConstraintViolation = "PropertyConstraintViolation" ErrGenericError = "GenericError" ) // Message is a decoded OCPP-J frame; only the fields relevant to Type are set. type Message struct { Type int ID string Action string // CALL only Payload json.RawMessage // CALL and CALLRESULT ErrorCode string // CALLERROR only ErrorDescription string // CALLERROR only ErrorDetails json.RawMessage // CALLERROR only } // DecodeMessage parses one OCPP-J frame. func DecodeMessage(b []byte) (Message, error) { var arr []json.RawMessage if err := json.Unmarshal(b, &arr); err != nil { return Message{}, fmt.Errorf("ocpp: not a JSON array: %w", err) } if len(arr) < 3 { return Message{}, errors.New("ocpp: message array too short") } var typ int if err := json.Unmarshal(arr[0], &typ); err != nil { return Message{}, fmt.Errorf("ocpp: bad message type: %w", err) } var id string if err := json.Unmarshal(arr[1], &id); err != nil { return Message{}, fmt.Errorf("ocpp: bad message id: %w", err) } m := Message{Type: typ, ID: id} switch typ { case MessageTypeCall: if len(arr) != 4 { return Message{}, errors.New("ocpp: CALL must have 4 elements") } if err := json.Unmarshal(arr[2], &m.Action); err != nil { return Message{}, fmt.Errorf("ocpp: bad action: %w", err) } m.Payload = arr[3] case MessageTypeCallResult: m.Payload = arr[2] case MessageTypeCallError: if len(arr) != 5 { return Message{}, errors.New("ocpp: CALLERROR must have 5 elements") } _ = json.Unmarshal(arr[2], &m.ErrorCode) _ = json.Unmarshal(arr[3], &m.ErrorDescription) m.ErrorDetails = arr[4] default: return Message{}, fmt.Errorf("ocpp: unknown message type %d", typ) } return m, nil } // EncodeCall builds a CALL frame. A nil/empty payload is encoded as {} because // OCPP requires the payload to be a JSON object. func EncodeCall(id, action string, payload any) ([]byte, error) { p, err := payloadObject(payload) if err != nil { return nil, err } return json.Marshal([]any{MessageTypeCall, id, action, p}) } // EncodeCallResult builds a CALLRESULT frame answering the CALL with id. func EncodeCallResult(id string, payload any) ([]byte, error) { p, err := payloadObject(payload) if err != nil { return nil, err } return json.Marshal([]any{MessageTypeCallResult, id, p}) } // EncodeCallError builds a CALLERROR frame rejecting the CALL with id. func EncodeCallError(id, code, description string, details any) ([]byte, error) { d, err := payloadObject(details) if err != nil { return nil, err } return json.Marshal([]any{MessageTypeCallError, id, code, description, d}) } // payloadObject normalizes any payload to a JSON object RawMessage, mapping // nil/null to the empty object {}. func payloadObject(payload any) (json.RawMessage, error) { switch v := payload.(type) { case nil: return json.RawMessage("{}"), nil case json.RawMessage: if len(v) == 0 || string(v) == "null" { return json.RawMessage("{}"), nil } return v, nil default: b, err := json.Marshal(payload) if err != nil { return nil, err } if len(b) == 0 || string(b) == "null" { return json.RawMessage("{}"), nil } return b, nil } } // newMessageID returns a fresh unique id for an outbound CALL. func newMessageID() string { var b [8]byte _, _ = rand.Read(b[:]) return hex.EncodeToString(b[:]) }