616 lines
24 KiB
C++
616 lines
24 KiB
C++
#pragma once
|
|
|
|
#include <cstdint>
|
|
#include <chrono>
|
|
#include <thread>
|
|
#include <optional>
|
|
#include <cassert>
|
|
|
|
#include "src/DebugToolDrivers/TargetInterfaces/Microchip/AVR8/Avr8DebugInterface.hpp"
|
|
#include "src/DebugToolDrivers/Microchip/Protocols/EDBG/EdbgInterface.hpp"
|
|
|
|
#include "Avr8Generic.hpp"
|
|
#include "EdbgAvr8Session.hpp"
|
|
|
|
#include "src/Targets/TargetPhysicalInterface.hpp"
|
|
#include "src/Targets/TargetMemory.hpp"
|
|
#include "src/Targets/TargetRegisterDescriptor.hpp"
|
|
#include "src/Targets/Microchip/AVR8/TargetDescriptionFile.hpp"
|
|
#include "src/Targets/Microchip/AVR8/Family.hpp"
|
|
|
|
namespace DebugToolDrivers::Microchip::Protocols::Edbg::Avr
|
|
{
|
|
/**
|
|
* The EdbgAvr8Interface implements the AVR8 Generic EDBG/CMSIS-DAP protocol, as an Avr8DebugInterface.
|
|
*
|
|
* See the "AVR8 Generic Protocol" section in the DS50002630A document by Microchip, for more information on the
|
|
* protocol.
|
|
*
|
|
* This implementation should work with any Microchip EDBG-based CMSIS-DAP debug tool (such as the Atmel-ICE,
|
|
* Power Debugger, the MPLAB SNAP debugger (in "AVR mode"), etc).
|
|
*/
|
|
class EdbgAvr8Interface: public ::DebugToolDrivers::TargetInterfaces::Microchip::Avr8::Avr8DebugInterface
|
|
{
|
|
public:
|
|
explicit EdbgAvr8Interface(
|
|
EdbgInterface* edbgInterface,
|
|
const Targets::Microchip::Avr8::TargetDescriptionFile& targetDescriptionFile,
|
|
const Targets::Microchip::Avr8::Avr8TargetConfig& targetConfig
|
|
);
|
|
|
|
/**
|
|
* Some EDBG devices don't seem to operate correctly when actioning the masked memory read EDBG command. The
|
|
* data returned in response to the command appears to be completely incorrect.
|
|
*
|
|
* Setting this flag to true will disable the EdbgAvr8Interface driver's use of the masked memory read command.
|
|
* The driver will perform the masking itself, and then issue standard read memory commands. See the
|
|
* implementation of EdbgAvr8Interface::readMemory() for more.
|
|
*
|
|
* NOTE: Masked memory read commands are only implemented for SRAM reads. EDBG debug tools report EEPROM and
|
|
* FLASH as invalid memory types, when using the masked memory read command. So any masked reads to non-SRAM
|
|
* will result in driver-side masking, regardless of the value of this flag.
|
|
*
|
|
* NOTE: We now avoid masked memory read commands by default, unless this flag is explicitly set to false.
|
|
* This means the default value of this->avoidMaskedMemoryRead is true.
|
|
*
|
|
* @param avoidMaskedMemoryRead
|
|
*/
|
|
void setAvoidMaskedMemoryRead(bool avoidMaskedMemoryRead) {
|
|
this->avoidMaskedMemoryRead = avoidMaskedMemoryRead;
|
|
}
|
|
|
|
/**
|
|
* Some EDBG AVR8 debug tools behave in a really weird way when servicing read memory requests that exceed a
|
|
* certain size. For example, the ATMEGA4809-XPRO Xplained Pro debug tool returns incorrect data for any read
|
|
* memory command that exceeds 256 bytes in the size of the read request, despite the fact that the HID report
|
|
* size is 512 bytes. The debug tool doesn't report any error, it just returns incorrect data.
|
|
*
|
|
* To address this, debug tool drivers can set a soft limit on the number of bytes this EdbgAvr8Interface instance
|
|
* will attempt to access in a single request, using the EdbgAvr8Interface::setMaximumMemoryAccessSizePerRequest()
|
|
* member function.
|
|
*
|
|
* This limit will be enforced in all forms of memory access on the AVR8 target, including register access.
|
|
* However, it will *NOT* be enforced for memory types that require page alignment, where the page size is
|
|
* greater than the set limit. In these cases, the limit will effectively be ignored. I've tested this on
|
|
* the ATXMEGAA1U-XPRO Xplained Pro debug tool, where the flash page size is 512 bytes (but the limit is set to
|
|
* 256 bytes), and flash memory access works fine. This makes me suspect that this issue may be specific to the
|
|
* ATMEGA4809-XPRO Xplained Pro debug tool.
|
|
*
|
|
* @param maximumSize
|
|
*/
|
|
void setMaximumMemoryAccessSizePerRequest(Targets::TargetMemorySize maximumSize) {
|
|
this->maximumMemoryAccessSizePerRequest = maximumSize;
|
|
}
|
|
|
|
void setReactivateJtagTargetPostProgrammingMode(bool reactivateJtagTargetPostProgrammingMode) {
|
|
this->reactivateJtagTargetPostProgrammingMode = reactivateJtagTargetPostProgrammingMode;
|
|
}
|
|
|
|
/*
|
|
* The public methods below implement the interface defined by the Avr8Interface class.
|
|
* See the comments in that class for more info on the expected behaviour of each method.
|
|
*/
|
|
|
|
/**
|
|
* Initialises the AVR8 Generic protocol interface by setting the appropriate parameters on the debug tool.
|
|
*/
|
|
void init() override;
|
|
|
|
/**
|
|
* Issues the "stop" command to the debug tool, halting target execution.
|
|
*/
|
|
void stop() override;
|
|
|
|
/**
|
|
* Issues the "run" command to the debug tool, resuming execution on the target.
|
|
*/
|
|
void run() override;
|
|
|
|
/**
|
|
* Issues the "run to" command to the debug tool, resuming execution on the target, up to a specific byte
|
|
* address. The target will dispatch an AVR BREAK event once it reaches the specified address.
|
|
*
|
|
* @param address
|
|
* The (byte) address to run to.
|
|
*/
|
|
void runTo(Targets::TargetMemoryAddress address) override;
|
|
|
|
/**
|
|
* Issues the "step" command to the debug tool, stepping the execution on the target. The stepping can be
|
|
* configured to step in, out or over. But currently we only support stepping in. The target will dispatch
|
|
* an AVR BREAK event once it reaches the next instruction.
|
|
*/
|
|
void step() override;
|
|
|
|
/**
|
|
* Issues the "reset" command to the debug tool, resetting target execution.
|
|
*/
|
|
void reset() override;
|
|
|
|
/**
|
|
* Activates the physical interface and starts a debug session on the target (via attach()).
|
|
*/
|
|
void activate() override;
|
|
|
|
/**
|
|
* Terminates any active debug session on the target and severs the connection between the debug tool and
|
|
* the target (by deactivating the physical interface).
|
|
*/
|
|
void deactivate() override;
|
|
|
|
Targets::TargetRegisterAccess getRegisterAccess(
|
|
const Targets::TargetRegisterDescriptor& registerDescriptor,
|
|
const Targets::TargetAddressSpaceDescriptor& addressSpaceDescriptor
|
|
) override;
|
|
|
|
/**
|
|
* Issues the "PC Read" command to the debug tool, to extract the current program counter.
|
|
*
|
|
* @return
|
|
*/
|
|
Targets::TargetMemoryAddress getProgramCounter() override;
|
|
|
|
/**
|
|
* Issues the "PC Write" command to the debug tool, setting the program counter on the target.
|
|
*
|
|
* @param programCounter
|
|
* The byte address to set as the program counter.
|
|
*/
|
|
void setProgramCounter(Targets::TargetMemoryAddress programCounter) override;
|
|
|
|
/**
|
|
* Issues the "Get ID" command to the debug tool, to extract the signature from the target.
|
|
*
|
|
* @return
|
|
*/
|
|
Targets::Microchip::Avr8::TargetSignature getDeviceId() override;
|
|
|
|
/**
|
|
* Issues the "Software Breakpoint Set" command to the debug tool, setting a software breakpoint at the given
|
|
* byte address.
|
|
*
|
|
* @param address
|
|
* The byte address to place the breakpoint.
|
|
*/
|
|
void setSoftwareBreakpoint(Targets::TargetMemoryAddress address) override;
|
|
|
|
/**
|
|
* Issues the "Software Breakpoint Clear" command to the debug tool, clearing any software breakpoint at the
|
|
* given byte address.
|
|
*
|
|
* @param address
|
|
* The byte address of the breakpoint to clear.
|
|
*/
|
|
void clearSoftwareBreakpoint(Targets::TargetMemoryAddress address) override;
|
|
|
|
/**
|
|
* Issues the "Hardware Breakpoint Set" command to the debug tool, setting a hardware breakpoint at the given
|
|
* byte address.
|
|
*
|
|
* @param address
|
|
* The byte address to place the breakpoint.
|
|
*/
|
|
void setHardwareBreakpoint(Targets::TargetMemoryAddress address) override;
|
|
|
|
/**
|
|
* Issues the "Hardware Breakpoint Clear" command to the debug tool, clearing any hardware breakpoint at the
|
|
* given byte address.
|
|
*
|
|
* @param address
|
|
* The byte address of the breakpoint to clear.
|
|
*/
|
|
void clearHardwareBreakpoint(Targets::TargetMemoryAddress address) override;
|
|
|
|
/**
|
|
* Issues the "Software Breakpoint Clear All" command to the debug tool, clearing all software breakpoints
|
|
* that were set *in the current debug session*.
|
|
*
|
|
* If the debug session ended before any of the set breakpoints were cleared, this will *not* clear them.
|
|
*/
|
|
void clearAllBreakpoints() override;
|
|
|
|
/**
|
|
* Reads registers from the target.
|
|
*
|
|
* @param descriptorIds
|
|
* @return
|
|
*/
|
|
Targets::TargetRegisterDescriptorAndValuePairs readRegisters(
|
|
const Targets::TargetRegisterDescriptors& descriptors
|
|
) override;
|
|
|
|
/**
|
|
* Writes registers to target.
|
|
*
|
|
* @param registers
|
|
*/
|
|
void writeRegisters(const Targets::TargetRegisterDescriptorAndValuePairs& registers) override;
|
|
|
|
/**
|
|
* This is an overloaded method.
|
|
*
|
|
* Resolves the correct Avr8MemoryType from the given TargetMemoryType and calls readMemory().
|
|
*
|
|
* @param addressSpaceDescriptor
|
|
* @param memorySegmentDescriptor
|
|
* @param startAddress
|
|
* @param bytes
|
|
* @return
|
|
*/
|
|
Targets::TargetMemoryBuffer readMemory(
|
|
const Targets::TargetAddressSpaceDescriptor& addressSpaceDescriptor,
|
|
const Targets::TargetMemorySegmentDescriptor& memorySegmentDescriptor,
|
|
Targets::TargetMemoryAddress startAddress,
|
|
Targets::TargetMemorySize bytes,
|
|
const std::set<Targets::TargetMemoryAddressRange>& excludedAddressRanges = {}
|
|
) override;
|
|
|
|
/**
|
|
* This is an overloaded method.
|
|
*
|
|
* Resolves the correct Avr8MemoryType from the given TargetMemoryType and calls writeMemory().
|
|
*
|
|
* @param addressSpaceDescriptor
|
|
* @param memorySegmentDescriptor
|
|
* @param startAddress
|
|
* @param buffer
|
|
*/
|
|
void writeMemory(
|
|
const Targets::TargetAddressSpaceDescriptor& addressSpaceDescriptor,
|
|
const Targets::TargetMemorySegmentDescriptor& memorySegmentDescriptor,
|
|
Targets::TargetMemoryAddress startAddress,
|
|
Targets::TargetMemoryBufferSpan buffer
|
|
) override;
|
|
|
|
/**
|
|
* Issues the "Erase" command to erase a particular section of program memory.
|
|
*
|
|
* @param section
|
|
*/
|
|
void eraseProgramMemory(
|
|
std::optional<Targets::Microchip::Avr8::ProgramMemorySection> section = std::nullopt
|
|
) override;
|
|
|
|
/**
|
|
* Issues the "Erase" command to erase the entire chip.
|
|
*/
|
|
void eraseChip() override;
|
|
|
|
/**
|
|
* Returns the current state of the target.
|
|
*
|
|
* @return
|
|
*/
|
|
Targets::TargetExecutionState getExecutionState() override;
|
|
|
|
/**
|
|
* Enters programming mode on the EDBG debug tool.
|
|
*/
|
|
void enableProgrammingMode() override;
|
|
|
|
/**
|
|
* Leaves programming mode on the EDBG debug tool.
|
|
*/
|
|
void disableProgrammingMode() override;
|
|
|
|
private:
|
|
/**
|
|
* The AVR8 Generic protocol is a sub-protocol of the EDBG AVR protocol, which is served via CMSIS-DAP vendor
|
|
* commands.
|
|
*
|
|
* Every EDBG based debug tool that utilises this implementation must provide access to its EDBG interface.
|
|
*/
|
|
EdbgInterface* edbgInterface = nullptr;
|
|
|
|
/**
|
|
* The active EDBG AVR8 session.
|
|
*/
|
|
EdbgAvr8Session session;
|
|
|
|
/**
|
|
* See the comment for EdbgAvr8Interface::setAvoidMaskedMemoryRead().
|
|
*/
|
|
bool avoidMaskedMemoryRead = true;
|
|
|
|
/**
|
|
* See the comment for EdbgAvr8Interface::setMaximumMemoryAccessSizePerRequest().
|
|
*/
|
|
std::optional<Targets::TargetMemorySize> maximumMemoryAccessSizePerRequest;
|
|
|
|
bool reactivateJtagTargetPostProgrammingMode = false;
|
|
|
|
/**
|
|
* We keep record of the current target execution state for caching purposes.
|
|
*
|
|
* We'll only refresh the target state if the target is running. If it has already stopped, then we assume it
|
|
* cannot transition to a running state without an instruction from us.
|
|
*
|
|
* @TODO: Review this. Is the above assumption correct? Always? Explore the option of polling the target state.
|
|
*/
|
|
Targets::TargetExecutionState cachedExecutionState = Targets::TargetExecutionState::UNKNOWN;
|
|
|
|
/**
|
|
* Upon configuration, the physical interface must be activated on the debug tool. We keep record of this to
|
|
* assist in our decision to deactivate the physical interface, when deactivate() is called.
|
|
*/
|
|
bool physicalInterfaceActivated = false;
|
|
|
|
/**
|
|
* As with activating the physical interface, we are required to issue the "attach" command to the debug
|
|
* tool, in order to start a debug session in the target.
|
|
*/
|
|
bool targetAttached = false;
|
|
|
|
bool programmingModeEnabled = false;
|
|
|
|
/**
|
|
* Every hardware breakpoint is assigned a "breakpoint number", which we need to keep track of in order to
|
|
* clear a hardware breakpoint.
|
|
*/
|
|
std::map<Targets::TargetMemoryAddress, std::uint8_t> hardwareBreakpointNumbersByAddress;
|
|
|
|
/**
|
|
* Sends the necessary target parameters to the debug tool.
|
|
*
|
|
* @param config
|
|
*/
|
|
void setTargetParameters();
|
|
|
|
/**
|
|
* Sets an AVR8 parameter on the debug tool. See the Avr8EdbgParameters class and protocol documentation
|
|
* for more on available parameters.
|
|
*
|
|
* @param parameter
|
|
* @param value
|
|
*/
|
|
void setParameter(const Avr8EdbgParameter& parameter, const std::vector<unsigned char>& value);
|
|
|
|
/**
|
|
* Overload for setting parameters with single byte values.
|
|
*
|
|
* @param parameter
|
|
* @param value
|
|
*/
|
|
void setParameter(const Avr8EdbgParameter& parameter, std::uint8_t value) {
|
|
this->setParameter(parameter, std::vector<unsigned char>(1, value));
|
|
}
|
|
|
|
/**
|
|
* Overload for setting parameters with four byte integer values.
|
|
*
|
|
* @param parameter
|
|
* @param value
|
|
*/
|
|
void setParameter(const Avr8EdbgParameter& parameter, std::uint32_t value) {
|
|
auto paramValue = std::vector<unsigned char>(4);
|
|
paramValue[0] = static_cast<unsigned char>(value);
|
|
paramValue[1] = static_cast<unsigned char>(value >> 8);
|
|
paramValue[2] = static_cast<unsigned char>(value >> 16);
|
|
paramValue[3] = static_cast<unsigned char>(value >> 24);
|
|
|
|
this->setParameter(parameter, paramValue);
|
|
}
|
|
|
|
/**
|
|
* Overload for setting parameters with two byte integer values.
|
|
*
|
|
* @param parameter
|
|
* @param value
|
|
*/
|
|
void setParameter(const Avr8EdbgParameter& parameter, std::uint16_t value) {
|
|
auto paramValue = std::vector<unsigned char>(2);
|
|
paramValue[0] = static_cast<unsigned char>(value);
|
|
paramValue[1] = static_cast<unsigned char>(value >> 8);
|
|
|
|
this->setParameter(parameter, paramValue);
|
|
}
|
|
|
|
/**
|
|
* Fetches an AV8 parameter from the debug tool.
|
|
*
|
|
* @param parameter
|
|
* @param size
|
|
* @return
|
|
*/
|
|
std::vector<unsigned char> getParameter(const Avr8EdbgParameter& parameter, std::uint8_t size);
|
|
|
|
/**
|
|
* EDBG-based debug tools require target specific parameters such as memory locations, page sizes and
|
|
* register addresses. These parameters can be sent to the tool before and during a session.
|
|
*
|
|
* What parameters we need to send depend on the physical interface (and config variant) selected by the user.
|
|
* For target parameters, the address (ID) of the parameter also varies across config variants.
|
|
*
|
|
* - The setDebugWireAndJtagParameters() function sends the required target parameters for debugWIRE and JTAG
|
|
* sessions. Both sessions are covered in a single function because they require the same parameters.
|
|
* - The setPdiParameters() function sends the required target parameters for PDI sessions.
|
|
* - The setUpdiParameters() function sends the required target parameters for UPDI sessions.
|
|
*
|
|
* We extract the required parameters from the TDF. See the constructors for the `DebugWireJtagParameters`,
|
|
* `PdiParameters` and `UpdiParameters` structs for more.
|
|
*/
|
|
void setDebugWireAndJtagParameters();
|
|
void setPdiParameters();
|
|
void setUpdiParameters();
|
|
|
|
/**
|
|
* Sends the "Activate Physical" command to the debug tool, activating the physical interface and thus enabling
|
|
* communication between the debug tool and the target.
|
|
*
|
|
* @param applyExternalReset
|
|
*/
|
|
void activatePhysical(bool applyExternalReset = false);
|
|
|
|
/**
|
|
* Sends the "Deactivate Physical" command to the debug tool, which will result in the connection between the
|
|
* debug tool and the target being severed.
|
|
*/
|
|
void deactivatePhysical();
|
|
|
|
/**
|
|
* Sends the "Attach" command to the debug tool, which starts a debug session on the target.
|
|
*/
|
|
void attach();
|
|
|
|
/**
|
|
* Sends the "Detach" command to the debug tool, which terminates any active debug session on the target.
|
|
*/
|
|
void detach();
|
|
|
|
/**
|
|
* Fetches any queued events belonging to the AVR8 Generic protocol (such as target break events).
|
|
*
|
|
* @return
|
|
*/
|
|
std::unique_ptr<AvrEvent> getAvrEvent();
|
|
|
|
/**
|
|
* Clears (discards) any queued AVR8 Generic protocol events on the debug tool.
|
|
*/
|
|
void clearEvents();
|
|
|
|
Avr8MemoryType getRegisterMemoryType(const Targets::TargetRegisterDescriptor& descriptor);
|
|
|
|
/**
|
|
* Checks if alignment is required for memory access via a given Avr8MemoryType.
|
|
*
|
|
* @param memoryType
|
|
* @return
|
|
*/
|
|
bool alignmentRequired(Avr8MemoryType memoryType);
|
|
|
|
/**
|
|
* Aligns a memory address for a given memory type's page size.
|
|
*
|
|
* @param memoryType
|
|
* @param address
|
|
* @return
|
|
*/
|
|
Targets::TargetMemoryAddress alignMemoryAddress(Avr8MemoryType memoryType, Targets::TargetMemoryAddress address);
|
|
|
|
/**
|
|
* Aligns a number of bytes used to address memory, for a given memory type's page size.
|
|
*
|
|
* @param memoryType
|
|
* @param bytes
|
|
* @return
|
|
*/
|
|
Targets::TargetMemorySize alignMemoryBytes(Avr8MemoryType memoryType, Targets::TargetMemorySize bytes);
|
|
|
|
/**
|
|
* Determines the maximum memory access size imposed on the given Avr8MemoryType.
|
|
*
|
|
* @param memoryType
|
|
*
|
|
* @return
|
|
*/
|
|
Targets::TargetMemorySize maximumMemoryAccessSize(Avr8MemoryType memoryType);
|
|
|
|
/**
|
|
* Reads memory on the target.
|
|
*
|
|
* This method will handle any alignment requirements for the selected memory type.
|
|
*
|
|
* See the Avr8MemoryType enum for list of supported AVR8 memory types.
|
|
*
|
|
* @param type
|
|
* The type of memory to access (Flash, EEPROM, SRAM, etc). See protocol documentation for more on this.
|
|
*
|
|
* @param startAddress
|
|
* The start address (byte address)
|
|
*
|
|
* @param bytes
|
|
* Number of bytes to access.
|
|
*
|
|
* @param excludedAddresses
|
|
* A set of addresses to exclude from the read operation. This is used to read memory ranges that could
|
|
* involve accessing an illegal address, like the OCDDR address.
|
|
*
|
|
* @return
|
|
*/
|
|
Targets::TargetMemoryBuffer readMemory(
|
|
Avr8MemoryType type,
|
|
Targets::TargetMemoryAddress startAddress,
|
|
Targets::TargetMemorySize bytes,
|
|
const std::set<Targets::TargetMemoryAddress>& excludedAddresses = {}
|
|
);
|
|
|
|
/**
|
|
* Writes memory to the target.
|
|
*
|
|
* This method will handle any alignment requirements for the selected memory type.
|
|
*
|
|
* See the Avr8MemoryType enum for list of supported AVR8 memory types.
|
|
*
|
|
* @param type
|
|
* @param address
|
|
* @param buffer
|
|
*/
|
|
void writeMemory(
|
|
Avr8MemoryType type,
|
|
Targets::TargetMemoryAddress address,
|
|
Targets::TargetMemoryBufferSpan buffer
|
|
);
|
|
|
|
/**
|
|
* Fetches the current target state.
|
|
*
|
|
* This currently uses AVR BREAK events to determine if a target has stopped. The lack of any
|
|
* queued BREAK events leads to the assumption that the target is still running.
|
|
*/
|
|
void refreshTargetState();
|
|
|
|
/**
|
|
* Temporarily disables the debugWIRE module on the target. This does not affect the DWEN fuse. The module
|
|
* will be reactivated upon the cycling of the target power.
|
|
*/
|
|
void disableDebugWire();
|
|
|
|
/**
|
|
* Waits for an AVR event of a specific type.
|
|
*
|
|
* @tparam AvrEventType
|
|
* Type of AVR event to wait for. See AvrEvent class for more.
|
|
*
|
|
* @param maximumAttempts
|
|
* Maximum number of attempts to poll the debug tool for the expected event.
|
|
*
|
|
* @return
|
|
* If an event is found before maximumAttempts is reached, the event will be returned. Otherwise a nullptr
|
|
* will be returned.
|
|
*/
|
|
template <class AvrEventType>
|
|
std::unique_ptr<AvrEventType> waitForAvrEvent(int maximumAttempts = 20) {
|
|
int attemptCount = 0;
|
|
|
|
while (attemptCount <= maximumAttempts) {
|
|
auto genericEvent = this->getAvrEvent();
|
|
|
|
if (genericEvent != nullptr) {
|
|
// Attempt to downcast event
|
|
auto event = std::unique_ptr<AvrEventType>(dynamic_cast<AvrEventType*>(genericEvent.release()));
|
|
|
|
if (event != nullptr) {
|
|
return event;
|
|
}
|
|
}
|
|
|
|
std::this_thread::sleep_for(std::chrono::milliseconds{50});
|
|
attemptCount++;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
/**
|
|
* Waits for an AVR BREAK event.
|
|
*
|
|
* This simply wraps a call to waitForAvrEvent<BreakEvent>(). An exception will be thrown if the call doesn't
|
|
* return a BreakEvent.
|
|
*
|
|
* This should only be used when a BreakEvent is always expected.
|
|
*/
|
|
void waitForStoppedEvent();
|
|
};
|
|
}
|