[VOL-3880] Correctly reporting software image status in OMCI Get
[VOL-3900] OMCI ONU Software Image Download

Change-Id: I8d91be832f3a89404d0af0dd98e6b53359e6a738
diff --git a/internal/bbsim/api/onus_handler.go b/internal/bbsim/api/onus_handler.go
index 276cddb..8f606d0 100644
--- a/internal/bbsim/api/onus_handler.go
+++ b/internal/bbsim/api/onus_handler.go
@@ -37,13 +37,17 @@
 	for _, pon := range olt.Pons {
 		for _, o := range pon.Onus {
 			onu := bbsim.ONU{
-				ID:            int32(o.ID),
-				SerialNumber:  o.Sn(),
-				OperState:     o.OperState.Current(),
-				InternalState: o.InternalState.Current(),
-				PonPortID:     int32(o.PonPortID),
-				PortNo:        int32(o.PortNo),
-				Services:      convertBBsimServicesToProtoServices(o.Services),
+				ID:                            int32(o.ID),
+				SerialNumber:                  o.Sn(),
+				OperState:                     o.OperState.Current(),
+				InternalState:                 o.InternalState.Current(),
+				PonPortID:                     int32(o.PonPortID),
+				PortNo:                        int32(o.PortNo),
+				Services:                      convertBBsimServicesToProtoServices(o.Services),
+				ImageSoftwareReceivedSections: int32(o.ImageSoftwareReceivedSections),
+				ImageSoftwareExpectedSections: int32(o.ImageSoftwareExpectedSections),
+				ActiveImageEntityId:           int32(o.ActiveImageEntityId),
+				CommittedImageEntityId:        int32(o.CommittedImageEntityId),
 			}
 			onus.Items = append(onus.Items, &onu)
 		}
diff --git a/internal/bbsim/devices/olt.go b/internal/bbsim/devices/olt.go
index f6a4b40..f9ef936 100644
--- a/internal/bbsim/devices/olt.go
+++ b/internal/bbsim/devices/olt.go
@@ -47,6 +47,17 @@
 	"module": "OLT",
 })
 
+const (
+	onuIdStart     = 1
+	onuIdEnd       = 127
+	allocIdStart   = 1024
+	allocIdEnd     = 16383
+	gemportIdStart = 1024
+	gemportIdEnd   = 65535
+	flowIdStart    = 1
+	flowIdEnd      = 65535
+)
+
 type OltDevice struct {
 	sync.Mutex
 	OltServer *grpc.Server
@@ -288,7 +299,7 @@
 			}
 
 			for _, onu := range pon.Onus {
-				_ = onu.InternalState.Event("disable")
+				_ = onu.InternalState.Event(OnuTxDisable)
 			}
 		}
 	} else {
@@ -297,7 +308,7 @@
 			// ONUs are not automatically disabled when a PON goes down
 			// as it's possible that it's an admin down and in that case the ONUs need to keep their state
 			for _, onu := range pon.Onus {
-				_ = onu.InternalState.Event("disable")
+				_ = onu.InternalState.Event(OnuTxDisable)
 			}
 		}
 	}
@@ -821,12 +832,12 @@
 			"OnuId":  _onu.ID,
 		}).Infof("Failed to transition ONU.OperState to enabled state: %s", err.Error())
 	}
-	if err := _onu.InternalState.Event("enable"); err != nil {
+	if err := _onu.InternalState.Event(OnuTxEnable); err != nil {
 		oltLogger.WithFields(log.Fields{
 			"IntfId": _onu.PonPortID,
 			"OnuSn":  _onu.Sn(),
 			"OnuId":  _onu.ID,
-		}).Infof("Failed to transition ONU to enabled state: %s", err.Error())
+		}).Infof("Failed to transition ONU to %s state: %s", OnuStateEnabled, err.Error())
 	}
 
 	// NOTE we need to immediately activate the ONU or the OMCI state machine won't start
@@ -862,12 +873,12 @@
 		}).Error("Can't find Onu")
 	}
 
-	if err := _onu.InternalState.Event("disable"); err != nil {
+	if err := _onu.InternalState.Event(OnuTxDisable); err != nil {
 		oltLogger.WithFields(log.Fields{
 			"IntfId": _onu.PonPortID,
 			"OnuSn":  _onu.Sn(),
 			"OnuId":  _onu.ID,
-		}).Infof("Failed to transition ONU to disabled state: %s", err.Error())
+		}).Infof("Failed to transition ONU to %s state: %s", OnuStateDisabled, err.Error())
 	}
 
 	// ONU Re-Discovery
@@ -1084,14 +1095,23 @@
 
 		// if its ONU flow remove it from ONU also
 		if storedFlow.AccessIntfId != -1 {
-			pon := o.Pons[uint32(storedFlow.AccessIntfId)]
+			pon, err := o.GetPonById(uint32(storedFlow.AccessIntfId))
+			if err != nil {
+				oltLogger.WithFields(log.Fields{
+					"OnuId":  storedFlow.OnuId,
+					"IntfId": storedFlow.AccessIntfId,
+					"PONs":   olt.Pons,
+					"err":    err,
+				}).Error("PON-port-not-found")
+				return new(openolt.Empty), nil
+			}
 			onu, err := pon.GetOnuById(uint32(storedFlow.OnuId))
 			if err != nil {
 				oltLogger.WithFields(log.Fields{
 					"OnuId":  storedFlow.OnuId,
 					"IntfId": storedFlow.AccessIntfId,
 					"err":    err,
-				}).Error("ONU not found")
+				}).Error("ONU-not-found")
 				return new(openolt.Empty), nil
 			}
 			onu.DeleteFlow(flowKey)
@@ -1157,24 +1177,67 @@
 
 func (o *OltDevice) GetDeviceInfo(context.Context, *openolt.Empty) (*openolt.DeviceInfo, error) {
 
-	devinfo := new(openolt.DeviceInfo)
-	devinfo.Vendor = common.Config.Olt.Vendor
-	devinfo.Model = common.Config.Olt.Model
-	devinfo.HardwareVersion = common.Config.Olt.HardwareVersion
-	devinfo.FirmwareVersion = common.Config.Olt.FirmwareVersion
-	devinfo.Technology = common.Config.Olt.Technology
-	devinfo.PonPorts = uint32(o.NumPon)
-	devinfo.OnuIdStart = 1
-	devinfo.OnuIdEnd = 255
-	devinfo.AllocIdStart = 1024
-	devinfo.AllocIdEnd = 16383
-	devinfo.GemportIdStart = 1024
-	devinfo.GemportIdEnd = 65535
-	devinfo.FlowIdStart = 1
-	devinfo.FlowIdEnd = 16383
-	devinfo.DeviceSerialNumber = o.SerialNumber
-	devinfo.DeviceId = common.Config.Olt.DeviceId
-	devinfo.PreviouslyConnected = o.PreviouslyConnected
+	oltLogger.WithFields(log.Fields{
+		"oltId":    o.ID,
+		"PonPorts": o.NumPon,
+	}).Info("OLT receives GetDeviceInfo call from VOLTHA")
+
+	intfIDs := []uint32{}
+	for i := 0; i < o.NumPon; i++ {
+		intfIDs = append(intfIDs, uint32(i))
+	}
+
+	devinfo := &openolt.DeviceInfo{
+		Vendor:              common.Config.Olt.Vendor,
+		Model:               common.Config.Olt.Model,
+		HardwareVersion:     common.Config.Olt.HardwareVersion,
+		FirmwareVersion:     common.Config.Olt.FirmwareVersion,
+		Technology:          common.Config.Olt.Technology,
+		PonPorts:            uint32(o.NumPon),
+		OnuIdStart:          onuIdStart,
+		OnuIdEnd:            onuIdEnd,
+		AllocIdStart:        allocIdStart,
+		AllocIdEnd:          allocIdEnd,
+		GemportIdStart:      gemportIdStart,
+		GemportIdEnd:        gemportIdEnd,
+		FlowIdStart:         flowIdStart,
+		FlowIdEnd:           flowIdEnd,
+		DeviceSerialNumber:  o.SerialNumber,
+		DeviceId:            common.Config.Olt.DeviceId,
+		PreviouslyConnected: o.PreviouslyConnected,
+		Ranges: []*openolt.DeviceInfo_DeviceResourceRanges{
+			{
+				IntfIds:    intfIDs,
+				Technology: common.Config.Olt.Technology,
+				Pools: []*openolt.DeviceInfo_DeviceResourceRanges_Pool{
+					{
+						Type:    openolt.DeviceInfo_DeviceResourceRanges_Pool_ONU_ID,
+						Sharing: openolt.DeviceInfo_DeviceResourceRanges_Pool_DEDICATED_PER_INTF,
+						Start:   onuIdStart,
+						End:     onuIdEnd,
+					},
+					{
+						Type:    openolt.DeviceInfo_DeviceResourceRanges_Pool_ALLOC_ID,
+						Sharing: openolt.DeviceInfo_DeviceResourceRanges_Pool_DEDICATED_PER_INTF,
+						Start:   allocIdStart,
+						End:     allocIdEnd,
+					},
+					{
+						Type:    openolt.DeviceInfo_DeviceResourceRanges_Pool_GEMPORT_ID,
+						Sharing: openolt.DeviceInfo_DeviceResourceRanges_Pool_DEDICATED_PER_INTF,
+						Start:   gemportIdStart,
+						End:     gemportIdEnd,
+					},
+					{
+						Type:    openolt.DeviceInfo_DeviceResourceRanges_Pool_FLOW_ID,
+						Sharing: openolt.DeviceInfo_DeviceResourceRanges_Pool_SHARED_BY_ALL_INTF_ALL_TECH,
+						Start:   flowIdStart,
+						End:     flowIdEnd,
+					},
+				},
+			},
+		},
+	}
 
 	oltLogger.WithFields(log.Fields{
 		"Vendor":              devinfo.Vendor,
diff --git a/internal/bbsim/devices/onu.go b/internal/bbsim/devices/onu.go
index 9bfe805..fd7eca0 100644
--- a/internal/bbsim/devices/onu.go
+++ b/internal/bbsim/devices/onu.go
@@ -47,6 +47,41 @@
 	"module": "ONU",
 })
 
+const (
+	// ONU transitions
+	OnuTxInitialize            = "initialize"
+	OnuTxDiscover              = "discover"
+	OnuTxEnable                = "enable"
+	OnuTxDisable               = "disable"
+	OnuTxPonDisable            = "pon_disable"
+	OnuTxStartImageDownload    = "start_image_download"
+	OnuTxProgressImageDownload = "progress_image_download"
+	OnuTxCompleteImageDownload = "complete_image_download"
+	OnuTxFailImageDownload     = "fail_image_download"
+	OnuTxActivateImage         = "activate_image"
+	OnuTxCommitImage           = "commit_image"
+
+	// ONU States
+	OnuStateCreated                 = "created"
+	OnuStateInitialized             = "initialized"
+	OnuStateDiscovered              = "discovered"
+	OnuStateEnabled                 = "enabled"
+	OnuStateDisabled                = "disabled"
+	OnuStatePonDisabled             = "pon_disabled"
+	OnuStateImageDownloadStarted    = "image_download_started"
+	OnuStateImageDownloadInProgress = "image_download_in_progress"
+	OnuStateImageDownloadComplete   = "image_download_completed"
+	OnuStateImageDownloadError      = "image_download_error"
+	OnuStateImageActivated          = "software_image_activated"
+	OnuStateImageCommitted          = "software_image_committed"
+
+	// BBR ONU States and Transitions
+	BbrOnuTxSendEapolFlow    = "send_eapol_flow"
+	BbrOnuStateEapolFlowSent = "eapol_flow_sent"
+	BbrOnuTxSendDhcpFlow     = "send_dhcp_flow"
+	BbrOnuStateDhcpFlowSent  = "dhcp_flow_sent"
+)
+
 type FlowKey struct {
 	ID        uint64
 	Direction string
@@ -79,7 +114,11 @@
 	Channel chan bbsim.Message // this Channel is to track state changes OMCI messages, EAPOL and DHCP packets
 
 	// OMCI params
-	MibDataSync uint8
+	MibDataSync                   uint8
+	ImageSoftwareExpectedSections int
+	ImageSoftwareReceivedSections int
+	ActiveImageEntityId           uint16
+	CommittedImageEntityId        uint16
 
 	// OMCI params (Used in BBR)
 	tid       uint16
@@ -97,19 +136,22 @@
 func CreateONU(olt *OltDevice, pon *PonPort, id uint32, delay time.Duration, isMock bool) *Onu {
 
 	o := Onu{
-		ID:                  id,
-		PonPortID:           pon.ID,
-		PonPort:             pon,
-		PortNo:              0,
-		tid:                 0x1,
-		hpTid:               0x8000,
-		seqNumber:           0,
-		DoneChannel:         make(chan bool, 1),
-		GemPortAdded:        false,
-		DiscoveryRetryDelay: 60 * time.Second, // this is used to send OnuDiscoveryIndications until an activate call is received
-		Flows:               []FlowKey{},
-		DiscoveryDelay:      delay,
-		MibDataSync:         0,
+		ID:                            id,
+		PonPortID:                     pon.ID,
+		PonPort:                       pon,
+		PortNo:                        0,
+		tid:                           0x1,
+		hpTid:                         0x8000,
+		seqNumber:                     0,
+		DoneChannel:                   make(chan bool, 1),
+		DiscoveryRetryDelay:           60 * time.Second, // this is used to send OnuDiscoveryIndications until an activate call is received
+		Flows:                         []FlowKey{},
+		DiscoveryDelay:                delay,
+		MibDataSync:                   0,
+		ImageSoftwareExpectedSections: 0, // populated during OMCI StartSoftwareDownloadRequest
+		ImageSoftwareReceivedSections: 0,
+		ActiveImageEntityId:           0, // when we start the SoftwareImage with ID 0 is active and committed
+		CommittedImageEntityId:        0,
 	}
 	o.SerialNumber = o.NewSN(olt.ID, pon.ID, id)
 	// NOTE this state machine is used to track the operational
@@ -122,20 +164,27 @@
 
 	// NOTE this state machine is used to activate the OMCI, EAPOL and DHCP clients
 	o.InternalState = fsm.NewFSM(
-		"created",
+		OnuStateCreated,
 		fsm.Events{
 			// DEVICE Lifecycle
-			{Name: "initialize", Src: []string{"created", "disabled", "pon_disabled"}, Dst: "initialized"},
-			{Name: "discover", Src: []string{"initialized"}, Dst: "discovered"},
-			{Name: "enable", Src: []string{"discovered", "pon_disabled"}, Dst: "enabled"},
+			{Name: OnuTxInitialize, Src: []string{OnuStateCreated, OnuStateDisabled, OnuStatePonDisabled}, Dst: OnuStateInitialized},
+			{Name: OnuTxDiscover, Src: []string{OnuStateInitialized}, Dst: OnuStateDiscovered},
+			{Name: OnuTxEnable, Src: []string{OnuStateDiscovered, OnuStatePonDisabled}, Dst: OnuStateEnabled},
 			// NOTE should disabled state be different for oper_disabled (emulating an error) and admin_disabled (received a disabled call via VOLTHA)?
-			{Name: "disable", Src: []string{"enabled", "pon_disabled"}, Dst: "disabled"},
+			{Name: OnuTxDisable, Src: []string{OnuStateEnabled, OnuStatePonDisabled, OnuStateImageActivated, OnuStateImageDownloadError, OnuStateImageCommitted}, Dst: OnuStateDisabled},
 			// ONU state when PON port is disabled but ONU is power ON(more states should be added in src?)
-			{Name: "pon_disabled", Src: []string{"enabled"}, Dst: "pon_disabled"},
+			{Name: OnuTxPonDisable, Src: []string{OnuStateEnabled, OnuStateImageActivated, OnuStateImageDownloadError, OnuStateImageCommitted}, Dst: OnuStatePonDisabled},
+			// Software Image Download related states
+			{Name: OnuTxStartImageDownload, Src: []string{OnuStateEnabled, OnuStateImageDownloadComplete, OnuStateImageDownloadError}, Dst: OnuStateImageDownloadStarted},
+			{Name: OnuTxProgressImageDownload, Src: []string{OnuStateImageDownloadStarted}, Dst: OnuStateImageDownloadInProgress},
+			{Name: OnuTxCompleteImageDownload, Src: []string{OnuStateImageDownloadInProgress}, Dst: OnuStateImageDownloadComplete},
+			{Name: OnuTxFailImageDownload, Src: []string{OnuStateImageDownloadInProgress}, Dst: OnuStateImageDownloadError},
+			{Name: OnuTxActivateImage, Src: []string{OnuStateImageDownloadComplete}, Dst: OnuStateImageActivated},
+			{Name: OnuTxCommitImage, Src: []string{OnuStateEnabled}, Dst: OnuStateImageCommitted}, // the image is committed after a ONU reboot
 			// BBR States
 			// TODO add start OMCI state
-			{Name: "send_eapol_flow", Src: []string{"initialized"}, Dst: "eapol_flow_sent"},
-			{Name: "send_dhcp_flow", Src: []string{"eapol_flow_sent"}, Dst: "dhcp_flow_sent"},
+			{Name: BbrOnuTxSendEapolFlow, Src: []string{OnuStateInitialized}, Dst: BbrOnuStateEapolFlowSent},
+			{Name: BbrOnuTxSendDhcpFlow, Src: []string{BbrOnuStateEapolFlowSent}, Dst: BbrOnuStateDhcpFlowSent},
 		},
 		fsm.Callbacks{
 			"enter_state": func(e *fsm.Event) {
@@ -145,7 +194,7 @@
 				// create new channel for ProcessOnuMessages Go routine
 				o.Channel = make(chan bbsim.Message, 2048)
 
-				if err := o.OperState.Event("enable"); err != nil {
+				if err := o.OperState.Event(OnuTxEnable); err != nil {
 					onuLogger.WithFields(log.Fields{
 						"OnuId":  o.ID,
 						"IntfId": o.PonPortID,
@@ -421,7 +470,7 @@
 	// after DiscoveryRetryDelay check if the state is the same and in case send a new OnuDiscIndication
 	go func(delay time.Duration) {
 		time.Sleep(delay)
-		if o.InternalState.Current() == "discovered" {
+		if o.InternalState.Current() == OnuStateDiscovered {
 			o.sendOnuDiscIndication(msg, stream)
 		}
 	}(o.DiscoveryRetryDelay)
@@ -490,7 +539,7 @@
 
 	// TODO if it's the last ONU on the PON, then send a PON LOS
 
-	if err := o.InternalState.Event("disable"); err != nil {
+	if err := o.InternalState.Event(OnuTxDisable); err != nil {
 		onuLogger.WithFields(log.Fields{
 			"OnuId":  o.ID,
 			"IntfId": o.PonPortID,
@@ -506,8 +555,8 @@
 	intitalState := o.InternalState.Current()
 
 	// initialize the ONU
-	if intitalState == "created" || intitalState == "disabled" {
-		if err := o.InternalState.Event("initialize"); err != nil {
+	if intitalState == OnuStateCreated || intitalState == OnuStateDisabled {
+		if err := o.InternalState.Event(OnuTxInitialize); err != nil {
 			onuLogger.WithFields(log.Fields{
 				"OnuId":  o.ID,
 				"IntfId": o.PonPortID,
@@ -534,7 +583,7 @@
 	}
 
 	// Send a ONU Discovery indication
-	if err := o.InternalState.Event("discover"); err != nil {
+	if err := o.InternalState.Event(OnuTxDiscover); err != nil {
 		onuLogger.WithFields(log.Fields{
 			"OnuId":  o.ID,
 			"IntfId": o.PonPortID,
@@ -546,8 +595,8 @@
 	// move o directly to enable state only when its a powercycle case
 	// in case of first time o poweron o will be moved to enable on
 	// receiving ActivateOnu request from openolt adapter
-	if intitalState == "disabled" {
-		if err := o.InternalState.Event("enable"); err != nil {
+	if intitalState == OnuStateDisabled {
+		if err := o.InternalState.Event(OnuTxEnable); err != nil {
 			onuLogger.WithFields(log.Fields{
 				"OnuId":  o.ID,
 				"IntfId": o.PonPortID,
@@ -627,7 +676,7 @@
 		log.WithFields(log.Fields{
 			"IntfId":       o.PonPortID,
 			"SerialNumber": o.Sn(),
-			"omciPacket":   msg.OmciMsg.Pkt,
+			"omciPacket":   omcilib.HexDecode(msg.OmciMsg.Pkt),
 		}).Error("cannot-parse-OMCI-packet")
 	}
 
@@ -656,7 +705,7 @@
 	case omci.MibUploadNextRequestType:
 		responsePkt, _ = omcilib.CreateMibUploadNextResponse(omciPkt, omciMsg, o.MibDataSync)
 	case omci.GetRequestType:
-		responsePkt, _ = omcilib.CreateGetResponse(omciPkt, omciMsg, o.SerialNumber, o.MibDataSync)
+		responsePkt, _ = omcilib.CreateGetResponse(omciPkt, omciMsg, o.SerialNumber, o.MibDataSync, o.ActiveImageEntityId, o.CommittedImageEntityId)
 	case omci.SetRequestType:
 		if responsePkt, errResp = omcilib.CreateSetResponse(omciPkt, omciMsg); errResp == nil {
 			o.MibDataSync++
@@ -699,20 +748,17 @@
 		responsePkt, _ = omcilib.CreateRebootResponse(omciPkt, omciMsg)
 
 		// powercycle the ONU
+		// we run this in a separate goroutine so that
+		// the RebootRequestResponse is sent to VOLTHA
 		go func() {
-			// we run this in a separate goroutine so that
-			// the RebootRequestResponse is sent to VOLTHA
-			onuLogger.WithFields(log.Fields{
-				"IntfId":       o.PonPortID,
-				"SerialNumber": o.Sn(),
-			}).Debug("shutting-down-onu-for-omci-reboot")
-			_ = o.HandleShutdownONU()
-			time.Sleep(10 * time.Second)
-			onuLogger.WithFields(log.Fields{
-				"IntfId":       o.PonPortID,
-				"SerialNumber": o.Sn(),
-			}).Debug("power-on-onu-for-omci-reboot")
-			_ = o.HandlePowerOnONU()
+			if err := o.Reboot(10 * time.Second); err != nil {
+				log.WithFields(log.Fields{
+					"IntfId":       o.PonPortID,
+					"OnuId":        o.ID,
+					"SerialNumber": o.Sn(),
+					"err":          err,
+				}).Error("cannot-reboot-onu-after-omci-reboot-request")
+			}
 		}()
 	case omci.TestRequestType:
 
@@ -726,6 +772,7 @@
 			if sendErr := o.sendTestResult(msg, stream); sendErr != nil {
 				onuLogger.WithFields(log.Fields{
 					"IntfId":       o.PonPortID,
+					"OnuId":        o.ID,
 					"SerialNumber": o.Sn(),
 					"omciPacket":   msg.OmciMsg.Pkt,
 					"msg":          msg,
@@ -736,8 +783,187 @@
 	case omci.SynchronizeTimeRequestType:
 		// MDS counter increment is not required for this message type
 		responsePkt, _ = omcilib.CreateSyncTimeResponse(omciPkt, omciMsg)
+	case omci.StartSoftwareDownloadRequestType:
+
+		o.ImageSoftwareReceivedSections = 0
+
+		o.ImageSoftwareExpectedSections = omcilib.ComputeDownloadSectionsCount(omciPkt)
+
+		if responsePkt, errResp = omcilib.CreateStartSoftwareDownloadResponse(omciPkt, omciMsg); errResp == nil {
+			o.MibDataSync++
+			if err := o.InternalState.Event(OnuTxStartImageDownload); err != nil {
+				onuLogger.WithFields(log.Fields{
+					"OnuId":  o.ID,
+					"IntfId": o.PonPortID,
+					"OnuSn":  o.Sn(),
+					"Err":    err.Error(),
+				}).Errorf("cannot-change-onu-internal-state-to-%s", OnuStateImageDownloadStarted)
+			}
+		} else {
+			onuLogger.WithFields(log.Fields{
+				"OmciMsgType":  omciMsg.MessageType,
+				"TransCorrId":  omciMsg.TransactionID,
+				"Err":          err.Error(),
+				"IntfId":       o.PonPortID,
+				"SerialNumber": o.Sn(),
+			}).Error("error-while-processing-start-software-download-request")
+		}
+	case omci.DownloadSectionRequestType:
+		if msgObj, err := omcilib.ParseDownloadSectionRequest(omciPkt); err == nil {
+			onuLogger.WithFields(log.Fields{
+				"OmciMsgType":    omciMsg.MessageType,
+				"TransCorrId":    omciMsg.TransactionID,
+				"EntityInstance": msgObj.EntityInstance,
+				"SectionNumber":  msgObj.SectionNumber,
+				"SectionData":    msgObj.SectionData,
+			}).Trace("received-download-section-request")
+			o.ImageSoftwareReceivedSections++
+			if o.InternalState.Current() != OnuStateImageDownloadInProgress {
+				if err := o.InternalState.Event(OnuTxProgressImageDownload); err != nil {
+					onuLogger.WithFields(log.Fields{
+						"OnuId":  o.ID,
+						"IntfId": o.PonPortID,
+						"OnuSn":  o.Sn(),
+						"Err":    err.Error(),
+					}).Errorf("cannot-change-onu-internal-state-to-%s", OnuStateImageDownloadInProgress)
+				}
+			}
+		}
+	case omci.DownloadSectionRequestWithResponseType:
+		// NOTE we only need to respond if an ACK is requested
+		responsePkt, err = omcilib.CreateDownloadSectionResponse(omciPkt, omciMsg)
+		if err != nil {
+			onuLogger.WithFields(log.Fields{
+				"OmciMsgType":  omciMsg.MessageType,
+				"TransCorrId":  omciMsg.TransactionID,
+				"Err":          err.Error(),
+				"IntfId":       o.PonPortID,
+				"SerialNumber": o.Sn(),
+			}).Error("error-while-processing-create-download-section-response")
+			return
+		}
+		o.ImageSoftwareReceivedSections++
+
+	case omci.EndSoftwareDownloadRequestType:
+
+		// In the startSoftwareDownload we get the image size and the window size.
+		// We calculate how many DownloadSection we should receive and validate
+		// that we got the correct amount when we receive this message
+		success := true
+		if o.ImageSoftwareExpectedSections != o.ImageSoftwareReceivedSections {
+			onuLogger.WithFields(log.Fields{
+				"OnuId":            o.ID,
+				"IntfId":           o.PonPortID,
+				"OnuSn":            o.Sn(),
+				"ExpectedSections": o.ImageSoftwareExpectedSections,
+				"ReceivedSections": o.ImageSoftwareReceivedSections,
+			}).Errorf("onu-did-not-receive-all-image-sections")
+			success = false
+		}
+
+		if success {
+			if responsePkt, errResp = omcilib.CreateEndSoftwareDownloadResponse(omciPkt, omciMsg, me.Success); errResp == nil {
+				o.MibDataSync++
+				if err := o.InternalState.Event(OnuTxCompleteImageDownload); err != nil {
+					onuLogger.WithFields(log.Fields{
+						"OnuId":  o.ID,
+						"IntfId": o.PonPortID,
+						"OnuSn":  o.Sn(),
+						"Err":    err.Error(),
+					}).Errorf("cannot-change-onu-internal-state-to-%s", OnuStateImageDownloadComplete)
+				}
+			} else {
+				onuLogger.WithFields(log.Fields{
+					"OmciMsgType":  omciMsg.MessageType,
+					"TransCorrId":  omciMsg.TransactionID,
+					"Err":          err.Error(),
+					"IntfId":       o.PonPortID,
+					"SerialNumber": o.Sn(),
+				}).Error("error-while-processing-end-software-download-request")
+			}
+		} else {
+			if responsePkt, errResp = omcilib.CreateEndSoftwareDownloadResponse(omciPkt, omciMsg, me.ProcessingError); errResp == nil {
+				if err := o.InternalState.Event(OnuTxFailImageDownload); err != nil {
+					onuLogger.WithFields(log.Fields{
+						"OnuId":  o.ID,
+						"IntfId": o.PonPortID,
+						"OnuSn":  o.Sn(),
+						"Err":    err.Error(),
+					}).Errorf("cannot-change-onu-internal-state-to-%s", OnuStateImageDownloadError)
+				}
+			}
+		}
+
+	case omci.ActivateSoftwareRequestType:
+		if responsePkt, errResp = omcilib.CreateActivateSoftwareResponse(omciPkt, omciMsg); errResp == nil {
+			o.MibDataSync++
+			if err := o.InternalState.Event(OnuTxActivateImage); err != nil {
+				onuLogger.WithFields(log.Fields{
+					"OnuId":  o.ID,
+					"IntfId": o.PonPortID,
+					"OnuSn":  o.Sn(),
+					"Err":    err.Error(),
+				}).Errorf("cannot-change-onu-internal-state-to-%s", OnuStateImageActivated)
+			}
+			if msgObj, err := omcilib.ParseActivateSoftwareRequest(omciPkt); err == nil {
+				o.ActiveImageEntityId = msgObj.EntityInstance
+			} else {
+				onuLogger.Errorf("something-went-wrong-while-activating: %s", err)
+			}
+			onuLogger.WithFields(log.Fields{
+				"OnuId":                  o.ID,
+				"IntfId":                 o.PonPortID,
+				"OnuSn":                  o.Sn(),
+				"ActiveImageEntityId":    o.ActiveImageEntityId,
+				"CommittedImageEntityId": o.CommittedImageEntityId,
+			}).Info("onu-software-image-activated")
+
+			// powercycle the ONU
+			// we run this in a separate goroutine so that
+			// the ActivateSoftwareResponse is sent to VOLTHA
+			// NOTE do we need to wait before rebooting?
+			go func() {
+				if err := o.Reboot(10 * time.Second); err != nil {
+					log.WithFields(log.Fields{
+						"IntfId":       o.PonPortID,
+						"OnuId":        o.ID,
+						"SerialNumber": o.Sn(),
+						"err":          err,
+					}).Error("cannot-reboot-onu-after-omci-activate-software-request")
+				}
+			}()
+		}
+	case omci.CommitSoftwareRequestType:
+		if responsePkt, errResp = omcilib.CreateCommitSoftwareResponse(omciPkt, omciMsg); errResp == nil {
+			o.MibDataSync++
+			if msgObj, err := omcilib.ParseCommitSoftwareRequest(omciPkt); err == nil {
+				// TODO validate that the image to commit is:
+				// - active
+				// - not already committed
+				o.CommittedImageEntityId = msgObj.EntityInstance
+			} else {
+				onuLogger.Errorf("something-went-wrong-while-committing: %s", err)
+			}
+			if err := o.InternalState.Event(OnuTxCommitImage); err != nil {
+				onuLogger.WithFields(log.Fields{
+					"OnuId":  o.ID,
+					"IntfId": o.PonPortID,
+					"OnuSn":  o.Sn(),
+					"Err":    err.Error(),
+				}).Errorf("cannot-change-onu-internal-state-to-%s", OnuStateImageCommitted)
+			}
+			onuLogger.WithFields(log.Fields{
+				"OnuId":                  o.ID,
+				"IntfId":                 o.PonPortID,
+				"OnuSn":                  o.Sn(),
+				"ActiveImageEntityId":    o.ActiveImageEntityId,
+				"CommittedImageEntityId": o.CommittedImageEntityId,
+			}).Info("onu-software-image-committed")
+		}
 	default:
-		log.WithFields(log.Fields{
+		onuLogger.WithFields(log.Fields{
+			"omciBytes":    hex.EncodeToString(omciPkt.Data()),
+			"omciPkt":      omciPkt,
 			"omciMsgType":  omciMsg.MessageType,
 			"transCorrId":  omciMsg.TransactionID,
 			"IntfId":       o.PonPortID,
@@ -901,12 +1127,33 @@
 
 		// check if ONU delete is performed and
 		// terminate the ONU's ProcessOnuMessages Go routine
-		if o.InternalState.Current() == "disabled" {
+		if o.InternalState.Current() == OnuStateDisabled {
 			close(o.Channel)
 		}
 	}
 }
 
+func (o *Onu) Reboot(timeout time.Duration) error {
+	onuLogger.WithFields(log.Fields{
+		"IntfId":       o.PonPortID,
+		"OnuId":        o.ID,
+		"SerialNumber": o.Sn(),
+	}).Debug("shutting-down-onu")
+	if err := o.HandleShutdownONU(); err != nil {
+		return err
+	}
+	time.Sleep(timeout)
+	onuLogger.WithFields(log.Fields{
+		"IntfId":       o.PonPortID,
+		"OnuId":        o.ID,
+		"SerialNumber": o.Sn(),
+	}).Debug("power-on-onu")
+	if err := o.HandlePowerOnONU(); err != nil {
+		return err
+	}
+	return nil
+}
+
 // BBR methods
 
 func sendOmciMsg(pktBytes []byte, intfId uint32, onuId uint32, sn *openolt.SerialNumber, msgType string, client openolt.OpenoltClient) {
@@ -1017,7 +1264,7 @@
 			sendOmciMsg(gemReq, o.PonPortID, o.ID, o.SerialNumber, "CreateGemPortRequest", client)
 			o.GemPortAdded = true
 		} else {
-			if err := o.InternalState.Event("send_eapol_flow"); err != nil {
+			if err := o.InternalState.Event(BbrOnuTxSendEapolFlow); err != nil {
 				onuLogger.WithFields(log.Fields{
 					"OnuId":  o.ID,
 					"IntfId": o.PonPortID,
@@ -1144,20 +1391,20 @@
 	}).Debug("Send ONU Re-Discovery")
 
 	// ONU Re-Discovery
-	if err := onu.InternalState.Event("initialize"); err != nil {
+	if err := onu.InternalState.Event(OnuTxInitialize); err != nil {
 		log.WithFields(log.Fields{
 			"IntfId": onu.PonPortID,
 			"OnuSn":  onu.Sn(),
 			"OnuId":  onu.ID,
-		}).Infof("Failed to transition ONU to initialized state: %s", err.Error())
+		}).Infof("Failed to transition ONU to %s state: %s", OnuStateInitialized, err.Error())
 	}
 
-	if err := onu.InternalState.Event("discover"); err != nil {
+	if err := onu.InternalState.Event(OnuTxDiscover); err != nil {
 		log.WithFields(log.Fields{
 			"IntfId": onu.PonPortID,
 			"OnuSn":  onu.Sn(),
 			"OnuId":  onu.ID,
-		}).Infof("Failed to transition ONU to discovered state: %s", err.Error())
+		}).Infof("Failed to transition ONU to %s state: %s", OnuStateDiscovered, err.Error())
 	}
 }
 
diff --git a/internal/bbsim/devices/onu_indications_test.go b/internal/bbsim/devices/onu_indications_test.go
index 1a34da1..04af678 100644
--- a/internal/bbsim/devices/onu_indications_test.go
+++ b/internal/bbsim/devices/onu_indications_test.go
@@ -58,8 +58,8 @@
 	}
 	ctx, cancel := context.WithCancel(context.TODO())
 	go onu.ProcessOnuMessages(ctx, stream, nil)
-	onu.InternalState.SetState("initialized")
-	_ = onu.InternalState.Event("discover")
+	onu.InternalState.SetState(OnuTxInitialize)
+	_ = onu.InternalState.Event(OnuTxDiscover)
 
 	select {
 	default:
@@ -83,8 +83,8 @@
 	}
 	ctx, cancel := context.WithCancel(context.TODO())
 	go onu.ProcessOnuMessages(ctx, stream, nil)
-	onu.InternalState.SetState("initialized")
-	_ = onu.InternalState.Event("discover")
+	onu.InternalState.SetState(OnuStateInitialized)
+	_ = onu.InternalState.Event(OnuTxDiscover)
 
 	select {
 	default:
@@ -106,8 +106,8 @@
 	}
 	ctx, cancel := context.WithCancel(context.TODO())
 	go onu.ProcessOnuMessages(ctx, stream, nil)
-	onu.InternalState.SetState("initialized")
-	_ = onu.InternalState.Event("discover")
+	onu.InternalState.SetState(OnuStateInitialized)
+	_ = onu.InternalState.Event(OnuTxDiscover)
 
 	go func() {
 		for calls := range stream.channel {
diff --git a/internal/bbsim/devices/onu_state_machine_test.go b/internal/bbsim/devices/onu_state_machine_test.go
index b48b286..6f62540 100644
--- a/internal/bbsim/devices/onu_state_machine_test.go
+++ b/internal/bbsim/devices/onu_state_machine_test.go
@@ -28,13 +28,13 @@
 	_ = onu.InternalState.Event("discover")
 	assert.Equal(t, onu.InternalState.Current(), "discovered")
 	_ = onu.InternalState.Event("enable")
-	assert.Equal(t, onu.InternalState.Current(), "enabled")
+	assert.Equal(t, onu.InternalState.Current(), OnuStateEnabled)
 }
 
 func Test_Onu_StateMachine_disable(t *testing.T) {
 	onu := createTestOnu()
-	onu.InternalState.SetState("enabled")
-	assert.Equal(t, onu.InternalState.Current(), "enabled")
+	onu.InternalState.SetState(OnuStateEnabled)
+	assert.Equal(t, onu.InternalState.Current(), OnuStateEnabled)
 
 	onu.PortNo = 16
 	onu.GemPortAdded = true
@@ -43,8 +43,8 @@
 		{ID: 2, Direction: "downstream"},
 	}
 
-	_ = onu.InternalState.Event("disable")
-	assert.Equal(t, onu.InternalState.Current(), "disabled")
+	_ = onu.InternalState.Event(OnuTxDisable)
+	assert.Equal(t, onu.InternalState.Current(), OnuStateDisabled)
 
 	assert.Equal(t, onu.GemPortAdded, false)
 	assert.Equal(t, onu.PortNo, uint32(0))
@@ -58,15 +58,15 @@
 	t.Skip("Needs to be moved in the Service struct")
 	onu := createTestOnu()
 
-	onu.InternalState.SetState("enabled")
-	assert.Equal(t, onu.InternalState.Current(), "enabled")
+	onu.InternalState.SetState(OnuStateEnabled)
+	assert.Equal(t, onu.InternalState.Current(), OnuStateEnabled)
 
 	// fail as no EapolFlow has been received
 	err := onu.InternalState.Event("start_auth")
 	if err == nil {
 		t.Fatal("can't start EAPOL without EapolFlow")
 	}
-	assert.Equal(t, onu.InternalState.Current(), "enabled")
+	assert.Equal(t, onu.InternalState.Current(), OnuStateEnabled)
 	assert.Equal(t, err.Error(), "transition canceled with error: cannot-go-to-auth-started-as-eapol-flow-is-missing")
 }
 
@@ -74,15 +74,15 @@
 	t.Skip("Needs to be moved in the Service struct")
 	onu := createTestOnu()
 
-	onu.InternalState.SetState("enabled")
-	assert.Equal(t, onu.InternalState.Current(), "enabled")
+	onu.InternalState.SetState(OnuStateEnabled)
+	assert.Equal(t, onu.InternalState.Current(), OnuStateEnabled)
 
 	// fail has no GemPort has been set
 	err := onu.InternalState.Event("start_auth")
 	if err == nil {
 		t.Fatal("can't start EAPOL without GemPort")
 	}
-	assert.Equal(t, onu.InternalState.Current(), "enabled")
+	assert.Equal(t, onu.InternalState.Current(), OnuStateEnabled)
 	assert.Equal(t, err.Error(), "transition canceled with error: cannot-go-to-auth-started-as-gemport-is-missing")
 
 }
@@ -91,8 +91,8 @@
 	t.Skip("Needs to be moved in the Service struct")
 	onu := createTestOnu()
 
-	onu.InternalState.SetState("enabled")
-	assert.Equal(t, onu.InternalState.Current(), "enabled")
+	onu.InternalState.SetState(OnuStateEnabled)
+	assert.Equal(t, onu.InternalState.Current(), OnuStateEnabled)
 
 	// succeed
 	onu.GemPortAdded = true
@@ -133,14 +133,14 @@
 	t.Skip("Needs to be moved in the Service struct")
 	onu := createTestOnu()
 
-	onu.InternalState.SetState("enabled")
-	assert.Equal(t, onu.InternalState.Current(), "enabled")
+	onu.InternalState.SetState(OnuStateEnabled)
+	assert.Equal(t, onu.InternalState.Current(), OnuStateEnabled)
 
 	err := onu.InternalState.Event("start_dhcp")
 	if err == nil {
 		t.Fail()
 	}
-	assert.Equal(t, onu.InternalState.Current(), "enabled")
+	assert.Equal(t, onu.InternalState.Current(), OnuStateEnabled)
 	assert.Equal(t, err.Error(), "transition canceled with error: cannot-go-to-dhcp-started-as-authentication-is-required")
 }
 
diff --git a/internal/bbsim/devices/onu_test_helpers.go b/internal/bbsim/devices/onu_test_helpers.go
index c08d9a8..f0d5dab 100644
--- a/internal/bbsim/devices/onu_test_helpers.go
+++ b/internal/bbsim/devices/onu_test_helpers.go
@@ -161,7 +161,7 @@
 	}
 	onu := CreateONU(&olt, &pon, 1, time.Duration(1*time.Millisecond), true)
 	// NOTE we need this in order to create the OnuChannel
-	_ = onu.InternalState.Event("initialize")
+	_ = onu.InternalState.Event(OnuTxInitialize)
 	onu.DiscoveryRetryDelay = 100 * time.Millisecond
 	return onu
 }
diff --git a/internal/bbsim/devices/pon.go b/internal/bbsim/devices/pon.go
index 6910ece..1e0483d 100644
--- a/internal/bbsim/devices/pon.go
+++ b/internal/bbsim/devices/pon.go
@@ -65,11 +65,11 @@
 				if e.Src == "created" {
 					if olt.ControlledActivation == Default || olt.ControlledActivation == OnlyPON {
 						for _, onu := range ponPort.Onus {
-							if err := onu.InternalState.Event("initialize"); err != nil {
+							if err := onu.InternalState.Event(OnuTxInitialize); err != nil {
 								log.Errorf("Error initializing ONU: %v", err)
 								continue
 							}
-							if err := onu.InternalState.Event("discover"); err != nil {
+							if err := onu.InternalState.Event(OnuTxDiscover); err != nil {
 								log.Errorf("Error discover ONU: %v", err)
 							}
 						}
@@ -78,7 +78,7 @@
 					if ponPort.Olt.ControlledActivation == OnlyONU || ponPort.Olt.ControlledActivation == Both {
 						// if ONUs are manually activated then only initialize them
 						for _, onu := range ponPort.Onus {
-							if err := onu.InternalState.Event("initialize"); err != nil {
+							if err := onu.InternalState.Event(OnuTxInitialize); err != nil {
 								log.WithFields(log.Fields{
 									"Err":    err,
 									"OnuSn":  onu.Sn(),
@@ -89,16 +89,16 @@
 						}
 					} else {
 						for _, onu := range ponPort.Onus {
-							if onu.InternalState.Current() == "pon_disabled" {
-								if err := onu.InternalState.Event("enable"); err != nil {
+							if onu.InternalState.Current() == OnuStatePonDisabled {
+								if err := onu.InternalState.Event(OnuStateEnabled); err != nil {
 									log.WithFields(log.Fields{
 										"Err":    err,
 										"OnuSn":  onu.Sn(),
 										"IntfId": onu.PonPortID,
 									}).Error("Error enabling ONU")
 								}
-							} else if onu.InternalState.Current() == "disabled" {
-								if err := onu.InternalState.Event("initialize"); err != nil {
+							} else if onu.InternalState.Current() == OnuStateDisabled {
+								if err := onu.InternalState.Event(OnuTxInitialize); err != nil {
 									log.WithFields(log.Fields{
 										"Err":    err,
 										"OnuSn":  onu.Sn(),
@@ -106,15 +106,15 @@
 									}).Error("Error initializing ONU")
 									continue
 								}
-								if err := onu.InternalState.Event("discover"); err != nil {
+								if err := onu.InternalState.Event(OnuTxDiscover); err != nil {
 									log.WithFields(log.Fields{
 										"Err":    err,
 										"OnuSn":  onu.Sn(),
 										"IntfId": onu.PonPortID,
 									}).Error("Error discovering ONU")
 								}
-							} else if onu.InternalState.Current() == "initialized" {
-								if err := onu.InternalState.Event("discover"); err != nil {
+							} else if onu.InternalState.Current() == OnuStateInitialized {
+								if err := onu.InternalState.Event(OnuTxDiscover); err != nil {
 									log.WithFields(log.Fields{
 										"Err":    err,
 										"OnuSn":  onu.Sn(),
@@ -135,11 +135,11 @@
 			},
 			"enter_disabled": func(e *fsm.Event) {
 				for _, onu := range ponPort.Onus {
-					if onu.InternalState.Current() == "initialized" || onu.InternalState.Current() == "disabled" {
+					if onu.InternalState.Current() == OnuStateInitialized || onu.InternalState.Current() == OnuStateDisabled {
 						continue
 					}
-					if err := onu.InternalState.Event("pon_disabled"); err != nil {
-						oltLogger.Errorf("Failed to move ONU in pon_disabled states: %v", err)
+					if err := onu.InternalState.Event(OnuTxPonDisable); err != nil {
+						oltLogger.Errorf("Failed to move ONU in %s states: %v", OnuStatePonDisabled, err)
 					}
 				}
 			},
@@ -192,7 +192,7 @@
 func (p PonPort) GetNumOfActiveOnus() uint32 {
 	var count uint32 = 0
 	for _, onu := range p.Onus {
-		if onu.InternalState.Current() == "initialized" || onu.InternalState.Current() == "created" || onu.InternalState.Current() == "disabled" {
+		if onu.InternalState.Current() == OnuStateInitialized || onu.InternalState.Current() == OnuStateCreated || onu.InternalState.Current() == OnuStateDisabled {
 			continue
 		}
 		count++
diff --git a/internal/bbsim/responders/sadis/sadis.go b/internal/bbsim/responders/sadis/sadis.go
index 5b71a94..4b9ccb3 100644
--- a/internal/bbsim/responders/sadis/sadis.go
+++ b/internal/bbsim/responders/sadis/sadis.go
@@ -20,7 +20,6 @@
 	"encoding/json"
 	"net/http"
 	"strings"
-	"sync"
 
 	"github.com/gorilla/mux"
 	"github.com/opencord/bbsim/internal/bbsim/devices"
@@ -32,8 +31,8 @@
 	"module": "SADIS",
 })
 
-type sadisServer struct {
-	olt *devices.OltDevice
+type SadisServer struct {
+	Olt *devices.OltDevice
 }
 
 // bandwidthProfiles contains some dummy profiles
@@ -228,7 +227,7 @@
 	return bwp
 }
 
-func (s *sadisServer) ServeBaseConfig(w http.ResponseWriter, r *http.Request) {
+func (s *SadisServer) ServeBaseConfig(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
 	w.WriteHeader(http.StatusOK)
 	vars := mux.Vars(r)
@@ -239,7 +238,7 @@
 		return
 	}
 
-	sadisConf := GetSadisConfig(s.olt, vars["version"])
+	sadisConf := GetSadisConfig(s.Olt, vars["version"])
 
 	sadisJSON, _ := json.Marshal(sadisConf)
 
@@ -247,17 +246,17 @@
 
 }
 
-func (s *sadisServer) ServeStaticConfig(w http.ResponseWriter, r *http.Request) {
+func (s *SadisServer) ServeStaticConfig(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
 	w.WriteHeader(http.StatusOK)
 	vars := mux.Vars(r)
-	sadisConf := GetSadisConfig(s.olt, vars["version"])
+	sadisConf := GetSadisConfig(s.Olt, vars["version"])
 
 	sadisConf.Sadis.Integration.URL = ""
-	for i := range s.olt.Pons {
-		for _, onu := range s.olt.Pons[i].Onus {
+	for i := range s.Olt.Pons {
+		for _, onu := range s.Olt.Pons[i].Onus {
 			if vars["version"] == "v2" {
-				sonuV2, _ := GetOnuEntryV2(s.olt, onu, "1")
+				sonuV2, _ := GetOnuEntryV2(s.Olt, onu, "1")
 				sadisConf.Sadis.Entries = append(sadisConf.Sadis.Entries, sonuV2)
 			}
 		}
@@ -273,17 +272,17 @@
 
 }
 
-func (s *sadisServer) ServeEntry(w http.ResponseWriter, r *http.Request) {
+func (s *SadisServer) ServeEntry(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
 	vars := mux.Vars(r)
 
 	// check if the requested ID is for the OLT
-	if s.olt.SerialNumber == vars["ID"] {
+	if s.Olt.SerialNumber == vars["ID"] {
 		sadisLogger.WithFields(log.Fields{
-			"OltSn": s.olt.SerialNumber,
+			"OltSn": s.Olt.SerialNumber,
 		}).Debug("Received SADIS OLT request")
 
-		sadisConf, _ := GetOltEntry(s.olt)
+		sadisConf, _ := GetOltEntry(s.Olt)
 
 		w.WriteHeader(http.StatusOK)
 		_ = json.NewEncoder(w).Encode(sadisConf)
@@ -299,7 +298,7 @@
 	}
 	sn, uni := i[0], i[len(i)-1]
 
-	onu, err := s.olt.FindOnuBySn(sn)
+	onu, err := s.Olt.FindOnuBySn(sn)
 	if err != nil {
 		w.WriteHeader(http.StatusNotFound)
 		_, _ = w.Write([]byte("{}"))
@@ -322,13 +321,13 @@
 		_ = json.NewEncoder(w).Encode("Sadis v1 is not supported anymore, please go back to an earlier BBSim version")
 	} else if vars["version"] == "v2" {
 		w.WriteHeader(http.StatusOK)
-		sadisConf, _ := GetOnuEntryV2(s.olt, onu, uni)
+		sadisConf, _ := GetOnuEntryV2(s.Olt, onu, uni)
 		_ = json.NewEncoder(w).Encode(sadisConf)
 	}
 
 }
 
-func (s *sadisServer) ServeBWPEntry(w http.ResponseWriter, r *http.Request) {
+func (s *SadisServer) ServeBWPEntry(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
 	vars := mux.Vars(r)
 	id := vars["ID"]
@@ -352,22 +351,3 @@
 	w.WriteHeader(http.StatusNotFound)
 	_, _ = w.Write([]byte("{}"))
 }
-
-// StartRestServer starts REST server which returns a SADIS configuration for the currently simulated OLT
-func StartRestServer(olt *devices.OltDevice, wg *sync.WaitGroup) {
-	addr := common.Config.BBSim.SadisRestAddress
-	sadisLogger.Infof("SADIS server listening on %s", addr)
-	s := &sadisServer{
-		olt: olt,
-	}
-
-	router := mux.NewRouter().StrictSlash(true)
-	router.HandleFunc("/{version}/cfg", s.ServeBaseConfig)
-	router.HandleFunc("/{version}/static", s.ServeStaticConfig)
-	router.HandleFunc("/{version}/subscribers/{ID}", s.ServeEntry)
-	router.HandleFunc("/{version}/bandwidthprofiles/{ID}", s.ServeBWPEntry)
-
-	log.Fatal(http.ListenAndServe(addr, router))
-
-	wg.Done()
-}
diff --git a/internal/bbsim/responders/webserver/webserver.go b/internal/bbsim/responders/webserver/webserver.go
new file mode 100644
index 0000000..2c5d29b
--- /dev/null
+++ b/internal/bbsim/responders/webserver/webserver.go
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2018-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 webserver
+
+import (
+	"github.com/gorilla/mux"
+	"github.com/opencord/bbsim/internal/bbsim/devices"
+	"github.com/opencord/bbsim/internal/bbsim/responders/sadis"
+	"github.com/opencord/bbsim/internal/common"
+	log "github.com/sirupsen/logrus"
+	"net/http"
+	"sync"
+)
+
+var logger = log.WithFields(log.Fields{
+	"module": "WEBSERVER",
+})
+
+// StartRestServer starts REST server which esposes:
+// - a SADIS configuration for the currently simulated OLT
+// - static files for software image download
+func StartRestServer(olt *devices.OltDevice, wg *sync.WaitGroup) {
+	addr := common.Config.BBSim.SadisRestAddress
+	logger.Infof("WEBSERVER server listening on %s", addr)
+	s := &sadis.SadisServer{
+		Olt: olt,
+	}
+
+	router := mux.NewRouter().StrictSlash(true)
+
+	// sadis routes
+	router.HandleFunc("/{version}/cfg", s.ServeBaseConfig)
+	router.HandleFunc("/{version}/static", s.ServeStaticConfig)
+	router.HandleFunc("/{version}/subscribers/{ID}", s.ServeEntry)
+	router.HandleFunc("/{version}/bandwidthprofiles/{ID}", s.ServeBWPEntry)
+
+	// Choose the folder to serve (this is the location inside the container)
+	staticDir := "/app/configs/"
+
+	// Create the route
+	router.
+		PathPrefix("/images/").
+		Handler(http.StripPrefix("/images/", http.FileServer(http.Dir(staticDir))))
+
+	log.Fatal(http.ListenAndServe(addr, router))
+
+	wg.Done()
+}
diff --git a/internal/bbsimctl/commands/onu.go b/internal/bbsimctl/commands/onu.go
index da841cd..78923e0 100644
--- a/internal/bbsimctl/commands/onu.go
+++ b/internal/bbsimctl/commands/onu.go
@@ -34,8 +34,8 @@
 )
 
 const (
-	DEFAULT_ONU_DEVICE_HEADER_FORMAT               = "table{{ .PonPortID }}\t{{ .ID }}\t{{ .PortNo }}\t{{ .SerialNumber }}\t{{ .OperState }}\t{{ .InternalState }}"
-	DEFAULT_ONU_DEVICE_HEADER_FORMAT_WITH_SERVICES = "table{{ .PonPortID }}\t{{ .ID }}\t{{ .PortNo }}\t{{ .SerialNumber }}\t{{ .OperState }}\t{{ .InternalState }}\t{{ .Services }}"
+	DEFAULT_ONU_DEVICE_HEADER_FORMAT               = "table{{ .PonPortID }}\t{{ .ID }}\t{{ .PortNo }}\t{{ .SerialNumber }}\t{{ .OperState }}\t{{ .InternalState }}\t{{ .ImageSoftwareExpectedSections }}\t{{ .ImageSoftwareReceivedSections }}\t{{ .ActiveImageEntityId }}\t{{ .CommittedImageEntityId }}"
+	DEFAULT_ONU_DEVICE_HEADER_FORMAT_WITH_SERVICES = "table{{ .PonPortID }}\t{{ .ID }}\t{{ .PortNo }}\t{{ .SerialNumber }}\t{{ .OperState }}\t{{ .InternalState }}\t{{ .ImageSoftwareExpectedSections }}\t{{ .ImageSoftwareReceivedSections }}\t{{ .ActiveImageEntityId }}\t{{ .CommittedImageEntityId }}\t{{ .Services }}"
 )
 
 type OnuSnString string
diff --git a/internal/common/omci/get.go b/internal/common/omci/get.go
index e47dc0c..5172f60 100644
--- a/internal/common/omci/get.go
+++ b/internal/common/omci/get.go
@@ -45,7 +45,7 @@
 	return msgObj, nil
 }
 
-func CreateGetResponse(omciPkt gopacket.Packet, omciMsg *omci.OMCI, onuSn *openolt.SerialNumber, mds uint8) ([]byte, error) {
+func CreateGetResponse(omciPkt gopacket.Packet, omciMsg *omci.OMCI, onuSn *openolt.SerialNumber, mds uint8, activeImageEntityId uint16, committedImageEntityId uint16) ([]byte, error) {
 
 	msgObj, err := ParseGetRequest(omciPkt)
 
@@ -66,7 +66,7 @@
 	case me.OnuGClassID:
 		response = createOnugResponse(msgObj.AttributeMask, msgObj.EntityInstance, onuSn)
 	case me.SoftwareImageClassID:
-		response = createSoftwareImageResponse(msgObj.AttributeMask, msgObj.EntityInstance)
+		response = createSoftwareImageResponse(msgObj.AttributeMask, msgObj.EntityInstance, activeImageEntityId, committedImageEntityId)
 	case me.IpHostConfigDataClassID:
 		response = createIpHostResponse(msgObj.AttributeMask, msgObj.EntityInstance)
 	case me.UniGClassID:
@@ -200,9 +200,24 @@
 	//}
 }
 
-func createSoftwareImageResponse(attributeMask uint16, entityInstance uint16) *omci.GetResponse {
+func createSoftwareImageResponse(attributeMask uint16, entityInstance uint16, activeImageEntityId uint16, committedImageEntityId uint16) *omci.GetResponse {
+
+	omciLogger.WithFields(log.Fields{
+		"EntityInstance": entityInstance,
+	}).Info("received-get-software-image-request")
+
+	// Only one image can be active and committed
+	committed := 0
+	active := 0
+	if entityInstance == activeImageEntityId {
+		active = 1
+	}
+	if entityInstance == committedImageEntityId {
+		committed = 1
+	}
+
 	// NOTE that we need send the response for the correct ME Instance or the adapter won't process it
-	return &omci.GetResponse{
+	res := &omci.GetResponse{
 		MeBasePacket: omci.MeBasePacket{
 			EntityClass:    me.SoftwareImageClassID,
 			EntityInstance: entityInstance,
@@ -210,8 +225,8 @@
 		Attributes: me.AttributeValueMap{
 			"ManagedEntityId": 0,
 			"Version":         toOctets("00000000000001", 14),
-			"IsCommitted":     1,
-			"IsActive":        1,
+			"IsCommitted":     committed,
+			"IsActive":        active,
 			"IsValid":         1,
 			"ProductCode":     toOctets("product-code", 25),
 			"ImageHash":       toOctets("broadband-sim", 16),
@@ -219,6 +234,15 @@
 		Result:        me.Success,
 		AttributeMask: attributeMask,
 	}
+
+	omciLogger.WithFields(log.Fields{
+		"omciMessage": res,
+		"entityId":    entityInstance,
+		"active":      active,
+		"committed":   committed,
+	}).Info("Reporting SoftwareImage")
+
+	return res
 }
 
 func createIpHostResponse(attributeMask uint16, entityInstance uint16) *omci.GetResponse {
diff --git a/internal/common/omci/image.go b/internal/common/omci/image.go
new file mode 100644
index 0000000..8480ecb
--- /dev/null
+++ b/internal/common/omci/image.go
@@ -0,0 +1,309 @@
+/*
+ * Copyright 2018-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 omci
+
+import (
+	"encoding/hex"
+	"errors"
+	"github.com/google/gopacket"
+	"github.com/opencord/omci-lib-go"
+	me "github.com/opencord/omci-lib-go/generated"
+	log "github.com/sirupsen/logrus"
+	"math"
+	"strconv"
+)
+
+func ParseStartSoftwareDownloadRequest(omciPkt gopacket.Packet) (*omci.StartSoftwareDownloadRequest, error) {
+	msgLayer := omciPkt.Layer(omci.LayerTypeStartSoftwareDownloadRequest)
+	if msgLayer == nil {
+		err := "omci Msg layer could not be detected for LayerTypeStartSoftwareDownloadRequest"
+		omciLogger.Error(err)
+		return nil, errors.New(err)
+	}
+	msgObj, msgOk := msgLayer.(*omci.StartSoftwareDownloadRequest)
+	if !msgOk {
+		err := "omci Msg layer could not be assigned for LayerTypeStartSoftwareDownloadRequest"
+		omciLogger.Error(err)
+		return nil, errors.New(err)
+	}
+	return msgObj, nil
+}
+
+func ParseDownloadSectionRequest(omciPkt gopacket.Packet) (*omci.DownloadSectionRequest, error) {
+	msgLayer := omciPkt.Layer(omci.LayerTypeDownloadSectionRequest)
+	if msgLayer == nil {
+		err := "omci Msg layer could not be detected for LayerTypeDownloadSectionRequest"
+		omciLogger.Error(err)
+		return nil, errors.New(err)
+	}
+	msgObj, msgOk := msgLayer.(*omci.DownloadSectionRequest)
+	if !msgOk {
+		err := "omci Msg layer could not be assigned for LayerTypeDownloadSectionRequest"
+		omciLogger.Error(err)
+		return nil, errors.New(err)
+	}
+	return msgObj, nil
+}
+
+func ParseEndSoftwareDownloadRequest(omciPkt gopacket.Packet) (*omci.EndSoftwareDownloadRequest, error) {
+	msgLayer := omciPkt.Layer(omci.LayerTypeEndSoftwareDownloadRequest)
+	if msgLayer == nil {
+		err := "omci Msg layer could not be detected for LayerTypeEndSoftwareDownloadRequest"
+		omciLogger.Error(err)
+		return nil, errors.New(err)
+	}
+	msgObj, msgOk := msgLayer.(*omci.EndSoftwareDownloadRequest)
+	if !msgOk {
+		err := "omci Msg layer could not be assigned for LayerTypeEndSoftwareDownloadRequest"
+		omciLogger.Error(err)
+		return nil, errors.New(err)
+	}
+	return msgObj, nil
+}
+
+func ParseActivateSoftwareRequest(omciPkt gopacket.Packet) (*omci.ActivateSoftwareRequest, error) {
+	msgLayer := omciPkt.Layer(omci.LayerTypeActivateSoftwareRequest)
+	if msgLayer == nil {
+		err := "omci Msg layer could not be detected for LayerTypeActivateSoftwareRequest"
+		omciLogger.Error(err)
+		return nil, errors.New(err)
+	}
+	msgObj, msgOk := msgLayer.(*omci.ActivateSoftwareRequest)
+	if !msgOk {
+		err := "omci Msg layer could not be assigned for LayerTypeActivateSoftwareRequest"
+		omciLogger.Error(err)
+		return nil, errors.New(err)
+	}
+	return msgObj, nil
+}
+
+func ParseCommitSoftwareRequest(omciPkt gopacket.Packet) (*omci.CommitSoftwareRequest, error) {
+	msgLayer := omciPkt.Layer(omci.LayerTypeCommitSoftwareRequest)
+	if msgLayer == nil {
+		err := "omci Msg layer could not be detected for LayerTypeCommitSoftwareRequest"
+		omciLogger.Error(err)
+		return nil, errors.New(err)
+	}
+	msgObj, msgOk := msgLayer.(*omci.CommitSoftwareRequest)
+	if !msgOk {
+		err := "omci Msg layer could not be assigned for LayerTypeCommitSoftwareRequest"
+		omciLogger.Error(err)
+		return nil, errors.New(err)
+	}
+	return msgObj, nil
+}
+
+func CreateStartSoftwareDownloadResponse(omciPkt gopacket.Packet, omciMsg *omci.OMCI) ([]byte, error) {
+	responeCode := me.Success
+	msgObj, err := ParseStartSoftwareDownloadRequest(omciPkt)
+	if err != nil {
+		responeCode = me.ProcessingError
+	}
+
+	omciLogger.WithFields(log.Fields{
+		"OmciMsgType":          omciMsg.MessageType,
+		"TransCorrId":          omciMsg.TransactionID,
+		"EntityInstance":       msgObj.EntityInstance,
+		"WindowSize":           msgObj.WindowSize,
+		"ImageSize":            msgObj.ImageSize,
+		"NumberOfCircuitPacks": msgObj.NumberOfCircuitPacks,
+		"CircuitPacks":         msgObj.CircuitPacks,
+	}).Debug("received-start-software-download-request")
+
+	response := &omci.StartSoftwareDownloadResponse{
+		MeBasePacket: omci.MeBasePacket{
+			EntityClass:    msgObj.EntityClass,
+			EntityInstance: msgObj.EntityInstance,
+		},
+		WindowSize:        msgObj.WindowSize,
+		Result:            responeCode,
+		NumberOfInstances: 0,                        // NOTE this can't be bigger than 0 this we can populate downloadResults
+		MeResults:         []omci.DownloadResults{}, // FIXME downloadResults is not exported
+	}
+
+	pkt, err := Serialize(omci.StartSoftwareDownloadResponseType, response, omciMsg.TransactionID)
+	if err != nil {
+		omciLogger.WithFields(log.Fields{
+			"Err": err,
+		}).Error("cannot-Serialize-CreateResponse")
+		return nil, err
+	}
+
+	log.WithFields(log.Fields{
+		"TxID": strconv.FormatInt(int64(omciMsg.TransactionID), 16),
+		"pkt":  hex.EncodeToString(pkt),
+	}).Trace("omci-start-software-download-response")
+
+	return pkt, nil
+}
+
+func CreateDownloadSectionResponse(omciPkt gopacket.Packet, omciMsg *omci.OMCI) ([]byte, error) {
+
+	msgObj, err := ParseDownloadSectionRequest(omciPkt)
+
+	if err != nil {
+		return nil, err
+	}
+
+	omciLogger.WithFields(log.Fields{
+		"OmciMsgType":    omciMsg.MessageType,
+		"TransCorrId":    omciMsg.TransactionID,
+		"EntityInstance": msgObj.EntityInstance,
+		"SectionNumber":  msgObj.SectionNumber,
+	}).Debug("received-download-section-request")
+
+	response := &omci.DownloadSectionResponse{
+		MeBasePacket: omci.MeBasePacket{
+			EntityClass:    msgObj.EntityClass,
+			EntityInstance: msgObj.EntityInstance,
+		},
+		Result:        me.Success,
+		SectionNumber: msgObj.SectionNumber,
+	}
+
+	pkt, err := Serialize(omci.DownloadSectionResponseType, response, omciMsg.TransactionID)
+	if err != nil {
+		return nil, err
+	}
+
+	log.WithFields(log.Fields{
+		"TxID": strconv.FormatInt(int64(omciMsg.TransactionID), 16),
+		"pkt":  hex.EncodeToString(pkt),
+	}).Trace("omci-download-section-with-response-response")
+
+	return pkt, nil
+}
+
+func CreateEndSoftwareDownloadResponse(omciPkt gopacket.Packet, omciMsg *omci.OMCI, status me.Results) ([]byte, error) {
+
+	msgObj, err := ParseEndSoftwareDownloadRequest(omciPkt)
+
+	if err != nil {
+		return nil, err
+	}
+
+	omciLogger.WithFields(log.Fields{
+		"OmciMsgType":       omciMsg.MessageType,
+		"TransCorrId":       omciMsg.TransactionID,
+		"EntityInstance":    msgObj.EntityInstance,
+		"Crc32":             msgObj.CRC32,
+		"ImageSize":         msgObj.ImageSize,
+		"NumberOfInstances": msgObj.NumberOfInstances,
+		"ImageInstances":    msgObj.ImageInstances,
+	}).Debug("received-end-software-download-request")
+
+	response := &omci.EndSoftwareDownloadResponse{
+		MeBasePacket: omci.MeBasePacket{
+			EntityClass:    msgObj.EntityClass,
+			EntityInstance: msgObj.EntityInstance,
+		},
+		Result:            status,
+		NumberOfInstances: 0, // NOTE this can't be bigger than 0 this we can populate downloadResults
+		//MeResults: []omci.downloadResults{}, // FIXME downloadResults is not exported
+	}
+
+	pkt, err := Serialize(omci.EndSoftwareDownloadResponseType, response, omciMsg.TransactionID)
+	if err != nil {
+		return nil, err
+	}
+
+	log.WithFields(log.Fields{
+		"TxID": strconv.FormatInt(int64(omciMsg.TransactionID), 16),
+		"pkt":  hex.EncodeToString(pkt),
+	}).Trace("omci-end-software-download-response")
+
+	return pkt, nil
+}
+
+func CreateActivateSoftwareResponse(omciPkt gopacket.Packet, omciMsg *omci.OMCI) ([]byte, error) {
+
+	msgObj, err := ParseActivateSoftwareRequest(omciPkt)
+
+	if err != nil {
+		return nil, err
+	}
+
+	omciLogger.WithFields(log.Fields{
+		"OmciMsgType":    omciMsg.MessageType,
+		"TransCorrId":    omciMsg.TransactionID,
+		"EntityInstance": msgObj.EntityInstance,
+		"ActivateFlags":  msgObj.ActivateFlags,
+	}).Debug("received-activate-software-request")
+
+	response := &omci.ActivateSoftwareResponse{
+		MeBasePacket: omci.MeBasePacket{
+			EntityClass:    msgObj.EntityClass,
+			EntityInstance: msgObj.EntityInstance,
+		},
+		Result: me.Success,
+	}
+
+	pkt, err := Serialize(omci.ActivateSoftwareResponseType, response, omciMsg.TransactionID)
+	if err != nil {
+		return nil, err
+	}
+
+	log.WithFields(log.Fields{
+		"TxID": strconv.FormatInt(int64(omciMsg.TransactionID), 16),
+		"pkt":  hex.EncodeToString(pkt),
+	}).Trace("omci-activate-software-response")
+
+	return pkt, nil
+}
+
+func CreateCommitSoftwareResponse(omciPkt gopacket.Packet, omciMsg *omci.OMCI) ([]byte, error) {
+
+	msgObj, err := ParseCommitSoftwareRequest(omciPkt)
+
+	if err != nil {
+		return nil, err
+	}
+
+	omciLogger.WithFields(log.Fields{
+		"OmciMsgType":    omciMsg.MessageType,
+		"TransCorrId":    omciMsg.TransactionID,
+		"EntityInstance": msgObj.EntityInstance,
+	}).Debug("received-commit-software-request")
+
+	response := &omci.CommitSoftwareResponse{
+		MeBasePacket: omci.MeBasePacket{
+			EntityClass:    msgObj.EntityClass,
+			EntityInstance: msgObj.EntityInstance,
+		},
+	}
+
+	pkt, err := Serialize(omci.CommitSoftwareResponseType, response, omciMsg.TransactionID)
+	if err != nil {
+		return nil, err
+	}
+
+	log.WithFields(log.Fields{
+		"TxID": strconv.FormatInt(int64(omciMsg.TransactionID), 16),
+		"pkt":  hex.EncodeToString(pkt),
+	}).Trace("omci-commit-software-response")
+
+	return pkt, nil
+}
+
+func ComputeDownloadSectionsCount(pkt gopacket.Packet) int {
+	msgObj, err := ParseStartSoftwareDownloadRequest(pkt)
+	if err != nil {
+		omciLogger.Error("cannot-parse-start-software-download-request")
+	}
+
+	return int(math.Ceil(float64(msgObj.ImageSize) / float64(msgObj.WindowSize)))
+}
diff --git a/internal/common/omci/image_test.go b/internal/common/omci/image_test.go
new file mode 100644
index 0000000..ad07eac
--- /dev/null
+++ b/internal/common/omci/image_test.go
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2018-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 omci
+
+import (
+	"github.com/google/gopacket"
+	"github.com/opencord/omci-lib-go"
+	me "github.com/opencord/omci-lib-go/generated"
+	"gotest.tools/assert"
+	"testing"
+)
+
+func omciToStartSoftwareDownloadResponse(t *testing.T, omciPkt *gopacket.Packet) *omci.StartSoftwareDownloadResponse {
+	msgLayer := (*omciPkt).Layer(omci.LayerTypeStartSoftwareDownloadResponse)
+	if msgLayer == nil {
+		t.Fatal("omci Msg layer could not be detected for StartSoftwareDownloadResponse")
+	}
+	msgObj, msgOk := msgLayer.(*omci.StartSoftwareDownloadResponse)
+	if !msgOk {
+		t.Fatal("omci Msg layer could not be assigned for StartSoftwareDownloadResponse")
+	}
+	return msgObj
+}
+
+func TestCreateStartSoftwareDownloadResponse(t *testing.T) {
+	omciReq := &omci.StartSoftwareDownloadRequest{
+		MeBasePacket: omci.MeBasePacket{
+			EntityClass:    me.SoftwareImageClassID,
+			EntityInstance: 1,
+		},
+		ImageSize:            32768,
+		NumberOfCircuitPacks: 1,
+		WindowSize:           31,
+		CircuitPacks:         []uint16{0},
+	}
+
+	omciReqPkt, err := Serialize(omci.StartSoftwareDownloadRequestType, omciReq, 66)
+	if err != nil {
+		t.Fatal(err.Error())
+	}
+
+	omciReqPkt, _ = HexEncode(omciReqPkt)
+
+	// start test
+	pkt, msg, _ := ParseOpenOltOmciPacket(omciReqPkt)
+	responsePkt, err := CreateStartSoftwareDownloadResponse(pkt, msg)
+	assert.NilError(t, err)
+
+	omciResponseMsg, omciResponsePkt := omciBytesToMsg(t, responsePkt)
+	assert.Equal(t, omciResponseMsg.MessageType, omci.StartSoftwareDownloadResponseType)
+
+	getResponseLayer := omciToStartSoftwareDownloadResponse(t, omciResponsePkt)
+
+	assert.Equal(t, getResponseLayer.Result, me.Success)
+}
+
+func TestComputeDownloadSectionsCount(t *testing.T) {
+	omciReq := &omci.StartSoftwareDownloadRequest{
+		MeBasePacket: omci.MeBasePacket{
+			EntityClass:    me.SoftwareImageClassID,
+			EntityInstance: 1,
+		},
+		ImageSize:            32768,
+		NumberOfCircuitPacks: 1,
+		WindowSize:           31,
+		CircuitPacks:         []uint16{0},
+	}
+
+	omciReqPkt, err := Serialize(omci.StartSoftwareDownloadRequestType, omciReq, 66)
+	if err != nil {
+		t.Fatal(err.Error())
+	}
+
+	omciReqPkt, _ = HexEncode(omciReqPkt)
+	pkt, _, _ := ParseOpenOltOmciPacket(omciReqPkt)
+
+	count := ComputeDownloadSectionsCount(pkt)
+	assert.Equal(t, count, 1058)
+}
diff --git a/internal/common/omci/mib_test.go b/internal/common/omci/mib_test.go
index f569bce..a4ffbb7 100644
--- a/internal/common/omci/mib_test.go
+++ b/internal/common/omci/mib_test.go
@@ -60,7 +60,7 @@
 
 func createTestMibUploadNextArgs(t *testing.T, tid uint16, seqNumber uint16) mibArgs {
 	mibUploadNext, _ := CreateMibUploadNextRequest(tid, seqNumber)
-	mibUploadNext = hexDecode(mibUploadNext)
+	mibUploadNext = HexDecode(mibUploadNext)
 	mibUploadNextMsg, mibUploadNextPkt := omciBytesToMsg(t, mibUploadNext)
 
 	return mibArgs{
diff --git a/internal/common/omci/omci_base.go b/internal/common/omci/omci_base.go
index 7a9f499..c5caf41 100644
--- a/internal/common/omci/omci_base.go
+++ b/internal/common/omci/omci_base.go
@@ -29,7 +29,7 @@
 // ParseOpenOltOmciPacket receive an OMCI packet in the openolt format and returns
 // an OMCI Layer as per omci-lib-go
 func ParseOpenOltOmciPacket(pkt []byte) (gopacket.Packet, *omci.OMCI, error) {
-	rxMsg := hexDecode(pkt)
+	rxMsg := HexDecode(pkt)
 
 	// NOTE this is may not be needed, VOLTHA sends the correct message
 	if len(rxMsg) >= 44 {
@@ -63,8 +63,8 @@
 	return packet, parsed, nil
 }
 
-// hexDecode converts the hex encoding to binary
-func hexDecode(pkt []byte) []byte {
+// HexDecode converts the hex encoding to binary
+func HexDecode(pkt []byte) []byte {
 	p := make([]byte, len(pkt)/2)
 	for i, j := 0, 0; i < len(pkt); i, j = i+2, j+1 {
 		// Go figure this ;)