[VOL-1349] EPON ONU adapter (package B)
Change-Id: I609ba349c429bc7e87c74b66bb1121841f9caef6
diff --git a/internal/pkg/onuadaptercore/onu_device_entry.go b/internal/pkg/onuadaptercore/onu_device_entry.go
new file mode 100644
index 0000000..5d888b1
--- /dev/null
+++ b/internal/pkg/onuadaptercore/onu_device_entry.go
@@ -0,0 +1,436 @@
+/*
+ * Copyright 2020-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+//Package adaptercoreonu provides the utility for onu devices, flows and statistics
+package adaptercoreonu
+
+import (
+ "context"
+ "errors"
+ "time"
+
+ "github.com/opencord/omci-lib-go"
+ me "github.com/opencord/omci-lib-go/generated"
+
+
+ "github.com/looplab/fsm"
+ "github.com/opencord/voltha-lib-go/v3/pkg/adapters/adapterif"
+ "github.com/opencord/voltha-lib-go/v3/pkg/db"
+
+ "github.com/opencord/voltha-lib-go/v3/pkg/log"
+)
+
+const (
+ ulEvStart = "ulEvStart"
+ ulEvResetMib = "ulEvResetMib"
+ ulEvGetVendorAndSerial = "ulEvGetVendorAndSerial"
+ ulEvGetEquipmentID = "ulEvGetEquipmentId"
+ ulEvGetFirstSwVersion = "ulEvGetFirstSwVersion"
+ ulEvGetSecondSwVersion = "ulEvGetSecondSwVersion"
+ ulEvGetMacAddress = "ulEvGetMacAddress"
+ ulEvGetMibTemplate = "ulEvGetMibTemplate"
+ ulEvUploadMib = "ulEvUploadMib"
+ ulEvExamineMds = "ulEvExamineMds"
+ ulEvSuccess = "ulEvSuccess"
+ ulEvMismatch = "ulEvMismatch"
+ ulEvAuditMib = "ulEvAuditMib"
+ ulEvForceResync = "ulEvForceResync"
+ ulEvDiffsFound = "ulEvDiffsFound"
+ ulEvTimeout = "ulEvTimeout"
+ ulEvStop = "ulEvStop"
+)
+const (
+ ulStDisabled = "ulStDisabled"
+ ulStStarting = "ulStStarting"
+ ulStResettingMib = "ulStResettingMib"
+ ulStGettingVendorAndSerial = "ulStGettingVendorAndSerial"
+ ulStGettingEquipmentID = "ulStGettingEquipmentID"
+ ulStGettingFirstSwVersion = "ulStGettingFirstSwVersion"
+ ulStGettingSecondSwVersion = "ulStGettingSecondSwVersion"
+ ulStGettingMacAddress = "ulStGettingMacAddress"
+ ulStGettingMibTemplate = "ulStGettingMibTemplate"
+ ulStUploading = "ulStUploading"
+ ulStInSync = "ulStInSync"
+ ulStExaminingMds = "ulStExaminingMds"
+ ulStResynchronizing = "ulStResynchronizing"
+ ulStAuditing = "ulStAuditing"
+ ulStOutOfSync = "ulStOutOfSync"
+)
+
+const (
+ dlEvStart = "dlEvStart"
+ dlEvCreateGal = "dlEvCreateGal"
+ dlEvRxGalResp = "dlEvRxGalResp"
+ dlEvRxOnu2gResp = "dlEvRxOnu2gResp"
+ dlEvRxBridgeResp = "dlEvRxBridgeResp"
+ dlEvTimeoutSimple = "dlEvTimeoutSimple"
+ dlEvTimeoutBridge = "dlEvTimeoutBridge"
+ dlEvReset = "dlEvReset"
+ dlEvRestart = "dlEvRestart"
+)
+const (
+ dlStDisabled = "dlStDisabled"
+ dlStStarting = "dlStStarting"
+ dlStCreatingGal = "dlStCreatingGal"
+ dlStSettingOnu2g = "dlStSettingOnu2g"
+ dlStBridgeInit = "dlStBridgeInit"
+ dlStDownloaded = "dlStDownloaded"
+ dlStResetting = "dlStResetting"
+)
+
+const (
+ cBasePathMibTemplateKvStore = "service/voltha/omci_mibs/go_templates"
+ cSuffixMibTemplateKvStore = "%s/%s/%s"
+)
+
+// OnuDeviceEvent - event of interest to Device Adapters and OpenOMCI State Machines
+type OnuDeviceEvent int
+
+const (
+
+ DeviceStatusInit OnuDeviceEvent = 0
+ MibDatabaseSync OnuDeviceEvent = 1
+ OmciCapabilitiesDone OnuDeviceEvent = 2
+ MibDownloadDone OnuDeviceEvent = 3
+ UniLockStateDone OnuDeviceEvent = 4
+ UniUnlockStateDone OnuDeviceEvent = 5
+ UniAdminStateDone OnuDeviceEvent = 6
+ PortLinkUp OnuDeviceEvent = 7
+ PortLinkDw OnuDeviceEvent = 8
+ OmciAniConfigDone OnuDeviceEvent = 9
+ OmciVlanFilterDone OnuDeviceEvent = 10
+)
+
+type activityDescr struct {
+ databaseClass func() error
+ auditDelay uint16
+}
+
+// OmciDeviceFsms - FSM event mapping to database class and time to wait between audits
+type OmciDeviceFsms map[string]activityDescr
+
+// AdapterFsm - Adapter FSM details including channel, event and device
+type AdapterFsm struct {
+ fsmName string
+ deviceID string
+ commChan chan Message
+ pFsm *fsm.FSM
+}
+
+//NewAdapterFsm - FSM details including event, device and channel.
+func NewAdapterFsm(aName string, aDeviceID string, aCommChannel chan Message) *AdapterFsm {
+ aFsm := &AdapterFsm{
+ fsmName: aName,
+ deviceID: aDeviceID,
+ commChan: aCommChannel,
+ }
+ return aFsm
+}
+
+//Start starts (logs) the omci agent
+func (oo *AdapterFsm) logFsmStateChange(e *fsm.Event) {
+ logger.Debugw("FSM state change", log.Fields{"device-id": oo.deviceID, "FSM name": oo.fsmName,
+ "event name": string(e.Event), "src state": string(e.Src), "dst state": string(e.Dst)})
+}
+
+//OntDeviceEntry structure holds information about the attached FSM'as and their communication
+
+const (
+ firstSwImageMeID = 0
+ secondSwImageMeID = 1
+)
+const onugMeID = 0
+const onu2gMeID = 0
+const ipHostConfigDataMeID = 1
+const onugSerialNumberLen = 8
+const omciMacAddressLen = 6
+
+type swImages struct {
+ version string
+ isActive uint8
+}
+
+// OnuDeviceEntry - ONU device info and FSM events.
+type OnuDeviceEntry struct {
+ deviceID string
+ baseDeviceHandler *deviceHandler
+ coreProxy adapterif.CoreProxy
+ adapterProxy adapterif.AdapterProxy
+ started bool
+ PDevOmciCC *omciCC
+ pOnuDB *onuDeviceDB
+ mibTemplateKVStore *db.Backend
+ vendorID string
+ serialNumber string
+ equipmentID string
+ swImages [secondSwImageMeID + 1]swImages
+ activeSwVersion string
+ macAddress string
+ mibDbClass func() error
+ supportedFsms OmciDeviceFsms
+ devState OnuDeviceEvent
+ mibAuditDelay uint16
+ mibDebugLevel string
+
+ pMibUploadFsm *AdapterFsm //could be handled dynamically and more general as pAdapterFsm - perhaps later
+ pMibDownloadFsm *AdapterFsm //could be handled dynamically and more general as pAdapterFsm - perhaps later
+ omciMessageReceived chan bool //seperate channel needed by DownloadFsm
+ omciRebootMessageReceivedChannel chan Message // channel needed by Reboot request
+}
+
+//newOnuDeviceEntry returns a new instance of a OnuDeviceEntry
+//mib_db (as well as not inluded alarm_db not really used in this code? VERIFY!!)
+func newOnuDeviceEntry(ctx context.Context, deviceID string, kVStoreHost string, kVStorePort int, kvStoreType string, deviceHandler *deviceHandler,
+ coreProxy adapterif.CoreProxy, adapterProxy adapterif.AdapterProxy,
+ supportedFsmsPtr *OmciDeviceFsms) *OnuDeviceEntry {
+ logger.Infow("init-onuDeviceEntry", log.Fields{"device-id": deviceID})
+ var onuDeviceEntry OnuDeviceEntry
+ onuDeviceEntry.started = false
+ onuDeviceEntry.deviceID = deviceID
+ onuDeviceEntry.baseDeviceHandler = deviceHandler
+ onuDeviceEntry.coreProxy = coreProxy
+ onuDeviceEntry.adapterProxy = adapterProxy
+ onuDeviceEntry.devState = DeviceStatusInit
+ onuDeviceEntry.omciRebootMessageReceivedChannel = make(chan Message, 2048)
+ if supportedFsmsPtr != nil {
+ onuDeviceEntry.supportedFsms = *supportedFsmsPtr
+ } else {
+ //var mibSyncFsm = NewMibSynchronizer()
+ // use some internaö defaults, if not defined from outside
+ onuDeviceEntry.supportedFsms = OmciDeviceFsms{
+ "mib-synchronizer": {
+ //mibSyncFsm, // Implements the MIB synchronization state machine
+ onuDeviceEntry.mibDbVolatileDict, // Implements volatile ME MIB database
+ //true, // Advertise events on OpenOMCI event bus
+ 60, // Time to wait between MIB audits. 0 to disable audits.
+ // map[string]func() error{
+ // "mib-upload": onuDeviceEntry.MibUploadTask,
+ // "mib-template": onuDeviceEntry.MibTemplateTask,
+ // "get-mds": onuDeviceEntry.GetMdsTask,
+ // "mib-audit": onuDeviceEntry.GetMdsTask,
+ // "mib-resync": onuDeviceEntry.MibResyncTask,
+ // "mib-reconcile": onuDeviceEntry.MibReconcileTask,
+ // },
+ },
+ }
+ }
+ onuDeviceEntry.mibDbClass = onuDeviceEntry.supportedFsms["mib-synchronizer"].databaseClass
+ logger.Debug("access2mibDbClass")
+ go onuDeviceEntry.mibDbClass()
+ onuDeviceEntry.mibAuditDelay = onuDeviceEntry.supportedFsms["mib-synchronizer"].auditDelay
+ logger.Debugw("MibAudit is set to", log.Fields{"Delay": onuDeviceEntry.mibAuditDelay})
+
+ onuDeviceEntry.mibDebugLevel = "normal" //set to "verbose" if you want to have all output, possibly later also per config option!
+ mibUploadChan := make(chan Message, 2048)
+ onuDeviceEntry.pMibUploadFsm = NewAdapterFsm("MibUpload", deviceID, mibUploadChan)
+ onuDeviceEntry.pMibUploadFsm.pFsm = fsm.NewFSM(
+ ulStDisabled,
+ fsm.Events{
+
+ {Name: ulEvStart, Src: []string{ulStDisabled}, Dst: ulStStarting},
+
+ {Name: ulEvResetMib, Src: []string{ulStStarting}, Dst: ulStResettingMib},
+ {Name: ulEvGetVendorAndSerial, Src: []string{ulStResettingMib}, Dst: ulStGettingVendorAndSerial},
+ {Name: ulEvGetEquipmentID, Src: []string{ulStGettingVendorAndSerial}, Dst: ulStGettingEquipmentID},
+ {Name: ulEvGetFirstSwVersion, Src: []string{ulStGettingEquipmentID}, Dst: ulStGettingFirstSwVersion},
+ {Name: ulEvGetSecondSwVersion, Src: []string{ulStGettingFirstSwVersion}, Dst: ulStGettingSecondSwVersion},
+ {Name: ulEvGetMacAddress, Src: []string{ulStGettingSecondSwVersion}, Dst: ulStGettingMacAddress},
+ {Name: ulEvGetMibTemplate, Src: []string{ulStGettingMacAddress}, Dst: ulStGettingMibTemplate},
+
+ {Name: ulEvUploadMib, Src: []string{ulStGettingMibTemplate}, Dst: ulStUploading},
+ {Name: ulEvExamineMds, Src: []string{ulStStarting}, Dst: ulStExaminingMds},
+
+ {Name: ulEvSuccess, Src: []string{ulStGettingMibTemplate}, Dst: ulStInSync},
+ {Name: ulEvSuccess, Src: []string{ulStUploading}, Dst: ulStInSync},
+
+ {Name: ulEvSuccess, Src: []string{ulStExaminingMds}, Dst: ulStInSync},
+ {Name: ulEvMismatch, Src: []string{ulStExaminingMds}, Dst: ulStResynchronizing},
+
+ {Name: ulEvAuditMib, Src: []string{ulStInSync}, Dst: ulStAuditing},
+
+ {Name: ulEvSuccess, Src: []string{ulStOutOfSync}, Dst: ulStInSync},
+ {Name: ulEvAuditMib, Src: []string{ulStOutOfSync}, Dst: ulStAuditing},
+
+ {Name: ulEvSuccess, Src: []string{ulStAuditing}, Dst: ulStInSync},
+ {Name: ulEvMismatch, Src: []string{ulStAuditing}, Dst: ulStResynchronizing},
+ {Name: ulEvForceResync, Src: []string{ulStAuditing}, Dst: ulStResynchronizing},
+
+ {Name: ulEvSuccess, Src: []string{ulStResynchronizing}, Dst: ulStInSync},
+ {Name: ulEvDiffsFound, Src: []string{ulStResynchronizing}, Dst: ulStOutOfSync},
+
+ {Name: ulEvTimeout, Src: []string{ulStResettingMib, ulStGettingVendorAndSerial, ulStGettingEquipmentID, ulStGettingFirstSwVersion,
+ ulStGettingSecondSwVersion, ulStGettingMacAddress, ulStGettingMibTemplate, ulStUploading, ulStResynchronizing, ulStExaminingMds,
+ ulStInSync, ulStOutOfSync, ulStAuditing}, Dst: ulStStarting},
+
+ {Name: ulEvStop, Src: []string{ulStStarting, ulStResettingMib, ulStGettingVendorAndSerial, ulStGettingEquipmentID, ulStGettingFirstSwVersion,
+ ulStGettingSecondSwVersion, ulStGettingMacAddress, ulStGettingMibTemplate, ulStUploading, ulStResynchronizing, ulStExaminingMds,
+ ulStInSync, ulStOutOfSync, ulStAuditing}, Dst: ulStDisabled},
+ },
+
+ fsm.Callbacks{
+ "enter_state": func(e *fsm.Event) { onuDeviceEntry.pMibUploadFsm.logFsmStateChange(e) },
+ ("enter_" + ulStStarting): func(e *fsm.Event) { onuDeviceEntry.enterStartingState(e) },
+ ("enter_" + ulStResettingMib): func(e *fsm.Event) { onuDeviceEntry.enterResettingMibState(e) },
+ ("enter_" + ulStGettingVendorAndSerial): func(e *fsm.Event) { onuDeviceEntry.enterGettingVendorAndSerialState(e) },
+ ("enter_" + ulStGettingEquipmentID): func(e *fsm.Event) { onuDeviceEntry.enterGettingEquipmentIDState(e) },
+ ("enter_" + ulStGettingFirstSwVersion): func(e *fsm.Event) { onuDeviceEntry.enterGettingFirstSwVersionState(e) },
+ ("enter_" + ulStGettingSecondSwVersion): func(e *fsm.Event) { onuDeviceEntry.enterGettingSecondSwVersionState(e) },
+ ("enter_" + ulStGettingMacAddress): func(e *fsm.Event) { onuDeviceEntry.enterGettingMacAddressState(e) },
+ ("enter_" + ulStGettingMibTemplate): func(e *fsm.Event) { onuDeviceEntry.enterGettingMibTemplate(e) },
+ ("enter_" + ulStUploading): func(e *fsm.Event) { onuDeviceEntry.enterUploadingState(e) },
+ ("enter_" + ulStExaminingMds): func(e *fsm.Event) { onuDeviceEntry.enterExaminingMdsState(e) },
+ ("enter_" + ulStResynchronizing): func(e *fsm.Event) { onuDeviceEntry.enterResynchronizingState(e) },
+ ("enter_" + ulStAuditing): func(e *fsm.Event) { onuDeviceEntry.enterAuditingState(e) },
+ ("enter_" + ulStOutOfSync): func(e *fsm.Event) { onuDeviceEntry.enterOutOfSyncState(e) },
+ ("enter_" + ulStInSync): func(e *fsm.Event) { onuDeviceEntry.enterInSyncState(e) },
+ },
+ )
+ mibDownloadChan := make(chan Message, 2048)
+ onuDeviceEntry.pMibDownloadFsm = NewAdapterFsm("MibDownload", deviceID, mibDownloadChan)
+ onuDeviceEntry.pMibDownloadFsm.pFsm = fsm.NewFSM(
+ dlStDisabled,
+ fsm.Events{
+
+ {Name: dlEvStart, Src: []string{dlStDisabled}, Dst: dlStStarting},
+
+ {Name: dlEvCreateGal, Src: []string{dlStStarting}, Dst: dlStCreatingGal},
+ {Name: dlEvRxGalResp, Src: []string{dlStCreatingGal}, Dst: dlStSettingOnu2g},
+ {Name: dlEvRxOnu2gResp, Src: []string{dlStSettingOnu2g}, Dst: dlStBridgeInit},
+ // the bridge state is used for multi ME config for alle UNI related ports
+ // maybe such could be reflected in the state machine as well (port number parametrized)
+ // but that looks not straightforward here - so we keep it simple here for the beginning(?)
+ {Name: dlEvRxBridgeResp, Src: []string{dlStBridgeInit}, Dst: dlStDownloaded},
+
+ {Name: dlEvTimeoutSimple, Src: []string{dlStCreatingGal, dlStSettingOnu2g}, Dst: dlStStarting},
+ {Name: dlEvTimeoutBridge, Src: []string{dlStBridgeInit}, Dst: dlStStarting},
+
+ {Name: dlEvReset, Src: []string{dlStStarting, dlStCreatingGal, dlStSettingOnu2g,
+ dlStBridgeInit, dlStDownloaded}, Dst: dlStResetting},
+ // exceptional treatment for all states except dlStResetting
+ {Name: dlEvRestart, Src: []string{dlStStarting, dlStCreatingGal, dlStSettingOnu2g,
+ dlStBridgeInit, dlStDownloaded, dlStResetting}, Dst: dlStDisabled},
+ },
+
+ fsm.Callbacks{
+ "enter_state": func(e *fsm.Event) { onuDeviceEntry.pMibDownloadFsm.logFsmStateChange(e) },
+ ("enter_" + dlStStarting): func(e *fsm.Event) { onuDeviceEntry.enterDLStartingState(e) },
+ ("enter_" + dlStCreatingGal): func(e *fsm.Event) { onuDeviceEntry.enterCreatingGalState(e) },
+ ("enter_" + dlStSettingOnu2g): func(e *fsm.Event) { onuDeviceEntry.enterSettingOnu2gState(e) },
+ ("enter_" + dlStBridgeInit): func(e *fsm.Event) { onuDeviceEntry.enterBridgeInitState(e) },
+ ("enter_" + dlStDownloaded): func(e *fsm.Event) { onuDeviceEntry.enterDownloadedState(e) },
+ ("enter_" + dlStResetting): func(e *fsm.Event) { onuDeviceEntry.enterResettingState(e) },
+ },
+ )
+ if onuDeviceEntry.pMibDownloadFsm == nil || onuDeviceEntry.pMibDownloadFsm.pFsm == nil {
+ logger.Error("MibDownloadFsm could not be instantiated!!")
+ // some specifc error treatment - or waiting for crash ???
+ }
+
+ onuDeviceEntry.mibTemplateKVStore = onuDeviceEntry.baseDeviceHandler.setBackend(cBasePathMibTemplateKvStore)
+ if onuDeviceEntry.mibTemplateKVStore == nil {
+ logger.Errorw("Failed to setup mibTemplateKVStore", log.Fields{"device-id": deviceID})
+ }
+
+ return &onuDeviceEntry
+}
+
+//start starts (logs) the omci agent
+func (oo *OnuDeviceEntry) start(ctx context.Context) error {
+ logger.Info("starting-OnuDeviceEntry")
+
+ oo.PDevOmciCC = newOmciCC(ctx, oo, oo.deviceID, oo.baseDeviceHandler,
+ oo.coreProxy, oo.adapterProxy)
+ if oo.PDevOmciCC == nil {
+ logger.Errorw("Could not create devOmciCc - abort", log.Fields{"for device-id": oo.deviceID})
+ return errors.New("could not create devOmciCc")
+ }
+
+ oo.started = true
+ logger.Info("OnuDeviceEntry-started")
+ return nil
+}
+
+//stop terminates the session
+func (oo *OnuDeviceEntry) stop(ctx context.Context) error {
+ logger.Info("stopping-OnuDeviceEntry")
+ oo.started = false
+ logger.Info("OnuDeviceEntry-stopped")
+ return nil
+}
+
+func (oo *OnuDeviceEntry) reboot(ctx context.Context) error {
+ logger.Info("reboot-OnuDeviceEntry")
+ if err := oo.PDevOmciCC.sendReboot(context.TODO(), ConstDefaultOmciTimeout, true, oo.omciRebootMessageReceivedChannel); err != nil {
+ logger.Errorw("onu didn't reboot", log.Fields{"for device-id": oo.deviceID})
+ return err
+ }
+ logger.Info("OnuDeviceEntry-reboot")
+ return nil
+}
+
+func (oo *OnuDeviceEntry) waitForRebootResponse(responseChannel chan Message) error {
+ select {
+ case <-time.After(3 * time.Second): //3s was detected to be to less in 8*8 bbsim test with debug Info/Debug
+ logger.Warnw("Reboot timeout", log.Fields{"for device-id": oo.deviceID})
+ return errors.New("rebootTimeout")
+ case data := <-responseChannel:
+ switch data.Data.(OmciMessage).OmciMsg.MessageType {
+ case omci.RebootResponseType:
+ {
+ msgLayer := (*data.Data.(OmciMessage).OmciPacket).Layer(omci.LayerTypeRebootResponse)
+ if msgLayer == nil {
+ return errors.New("omci Msg layer could not be detected for RebootResponseType")
+ }
+ msgObj, msgOk := msgLayer.(*omci.GetResponse)
+ if !msgOk {
+ return errors.New("omci Msg layer could not be assigned for RebootResponseType")
+ }
+ logger.Debugw("CreateResponse Data", log.Fields{"device-id": oo.deviceID, "data-fields": msgObj})
+ if msgObj.Result != me.Success {
+ logger.Errorw("Omci RebootResponseType Error ", log.Fields{"Error": msgObj.Result})
+ // possibly force FSM into abort or ignore some errors for some messages? store error for mgmt display?
+ return errors.New("omci RebootResponse Result Error indication")
+ }
+ return nil
+ }
+ }
+ logger.Warnw("Reboot response error", log.Fields{"for device-id": oo.deviceID})
+ return errors.New("unexpected OmciResponse type received")
+ }
+}
+
+//Relay the InSync message via Handler to Rw core - Status update
+func (oo *OnuDeviceEntry) transferSystemEvent(devEvent OnuDeviceEvent) {
+ logger.Debugw("relaying system-event", log.Fields{"Event": devEvent})
+ if devEvent == MibDatabaseSync {
+ if oo.devState < MibDatabaseSync { //devState has not been synced yet
+ oo.devState = MibDatabaseSync
+ go oo.baseDeviceHandler.deviceProcStatusUpdate(devEvent)
+ //TODO!!! device control: next step: start MIB capability verification from here ?!!!
+ } else {
+ logger.Debugw("mibinsync-event in some already synced state - ignored", log.Fields{"state": oo.devState})
+ }
+ } else if devEvent == MibDownloadDone {
+ if oo.devState < MibDownloadDone { //devState has not been synced yet
+ oo.devState = MibDownloadDone
+ go oo.baseDeviceHandler.deviceProcStatusUpdate(devEvent)
+ } else {
+ logger.Debugw("mibdownloaddone-event was already seen - ignored", log.Fields{"state": oo.devState})
+ }
+ } else {
+ logger.Warnw("device-event not yet handled", log.Fields{"state": devEvent})
+ }
+}