classdef WaveformAnalyzer < WaveformAnalysisEngine
%WaveformAnalyzer Detect, decode and analyze WLAN packets within a waveform
%   ANALYZER = WaveformAnalyzer creates a WLAN waveform analyzer. The
%   waveform analyzer detects and analyze multiple WLAN packets within a
%   waveform.
%
%   ANALYZER = WaveformAnalyzer(Name,Value) creates a WLAN waveform
%   analyzer with the specified property Name set to the specified Value.
%   You can specify additional name-value pair arguments in any order as
%   (Name1,Value1, ...,NameN,ValueN).
%
%   WaveformAnalyzer properties:
%
%   DCBlocking                    - Enable DC blocking
%   EnergyDetection               - Enable energy detection
%   EnergyDetectionThreshold      - Energy detection threshold in dBW
%   LLTFSNRDetectionThreshold     - L-LTF SNR detection threshold
%   PacketDetectionThreshold      - Packet detection algorithm threshold
%   PilotTimeTracking             - Data field pilot time (sample rate offset) tracking
%   PilotGainTracking             - Data field pilot gain tracking
%   PilotTrackingWindow           - Data field pilot tracking averaging window
%   EqualizationMethod            - Data field symbols equalization method
%   SearchWithinUnsupportedPacket - Search for packets within detected unsupported packets
%   SuppressWarnings              - Suppress warnings during processing
%
%   WaveformAnalyzer methods:
%
%   process           - Detect, decode and analyze WLAN packets within a waveform
%   detectionSummary  - Display the summary of detected packets
%   macSummary        - Display MAC summary of the selected packet
%   plotWaveform      - Plot time domain samples and signal spectrum of the selected packet
%   fieldSummary      - Display field summary of the selected packet
%   signalingSummary  - Display signaling field summary of the selected packet
%   ruSummary         - Display resource unit summary of the selected packet
%   userSummary       - Display user summary of the selected packet
%   userEVM           - Display EVM per spatial stream for all users of the selected packet
%   plotConstellation - Plot equalized data field symbols for each user of the selected packet
%   plotEVM           - Plot EVM per subcarrier and data field symbols of the selected packet
%   getResults        - Returns analysis results

%   Copyright 2019-2020 The MathWorks, Inc.

    properties (Access=private)
        Results;
    end

    methods
        function obj = WaveformAnalyzer(varargin)
            obj = obj@WaveformAnalysisEngine(varargin{:});
        end

        function T = detectionSummary(obj)
        %detectionSummary Display the summary of detected packets
        %
        %   detectionSummary(ANALYZER) displays the summary of detected
        %   packets.
        %
        %   The table contains the following columns:
        %
        %   Packet Number   Number of the detected packet
        %
        %   Format          Specifies the detected format of the packet
        %                   as one of: Non-HT, HT-MF, HT-GF, VHT, HE-SU,
        %                   HE-EXT-SU, HE-MU, and HE-TB.
        %
        %   PHY Status      Specifies the PHY recovery status of the
        %                   packet. If a PSDU is recovered the status
        %                   is Success. Otherwise, status is one of:
        %                   * Incomplete packet
        %                   * Unsupported format - Format other than HE or
        %                     Non-HT detected
        %                   * Non-HT duplicate not supported
        %                   * HE midamble not supported
        %                   * L-SIG check fail
        %                   * HE-SIG-A FailCRC
        %                   * Invalid HE-SIG-A contents - CRC passed but
        %                     contents invalid
        %                   * HE-SIG-B Common Fail
        %                   * HE-SIG-B User Fail
        %                   * Unexpected channel bandwidth decoded - The
        %                     decoded channel bandwidth does not match the
        %                     channel bandwidth provided by the user
        %                     therefore the packet cannot be recovered.
        %
        %   Power           Power of the detected packet in dBm.
        %
        %   CFO             Carrier frequency offset in hertz.
        %
        %   Packet Offset   Offset in samples from the start of the
        %                   input waveform to the start of the detected
        %                   preamble.
        %
        %   MAC Contents    Specifies the MAC contents of the detected
        %                   packet as one of: A-MPDU, RTS, CTS, ACK,
        %                   BlockAck, Data, Null, QoS Data, QoS Null,
        %                   Beacon or Unknown. If all users A-MPDUs are
        %                   deaggregated successfully then display MAC
        %                   contents as A-MPDU otherwise display Unknown.
        %                   For all not supported MAC frames an Unknown is
        %                   displayed.
        %
        %   T = detectionSummary(ANALYZER) returns the summary of the
        %   detected packet in a table.

            checkWaveformProcessed(obj);
            x = obj.Results;
            numPkt = numel(x);
            pktFormat = strings(numPkt,1);
            pktStatus = strings(numPkt,1);
            pktPower = zeros(numPkt,1);
            cfo = zeros(numPkt,1);
            pktOffset = zeros(numPkt,1);
            mpduType = strings(numPkt,1);
            for nPkt=1:numPkt
                pktFormat(nPkt) = x{nPkt}.Format;
                pktStatus(nPkt) = x{nPkt}.Status;
                pktPower(nPkt) = powerdBm(x{nPkt}.PacketPower);
                cfo(nPkt) = x{nPkt}.Preamble.CFOEstimate;
                pktOffset(nPkt) = x{nPkt}.PacketOffset;
                mpduType(nPkt) = x{nPkt}.PacketContents;
            end
            if isempty(pktFormat)
                fprintf([pad(' ',50),'<strong>No Packet Detected</strong>\n\n']);
                T = [];
                return;
            else
                fprintf([pad(' ',44),'<strong>Summary of the Detected Packets</strong>\n\n']);
            end
            detSummary = table((1:numPkt)',pktFormat,pktStatus,pktPower,cfo,pktOffset,mpduType);
            detSummary.Properties.VariableNames{1} = 'Packet Number';
            detSummary.Properties.VariableNames{2} = 'Format';
            detSummary.Properties.VariableNames{3} = 'PHY Status';
            detSummary.Properties.VariableNames{4} = 'Power (dBm)';
            detSummary.Properties.VariableNames{5} = 'CFO (Hz)';
            detSummary.Properties.VariableNames{6} = 'Packet Offset (samples)';
            detSummary.Properties.VariableNames{7} = 'MAC Contents' %#ok<NOPRT>

            if nargout
                T = detSummary;
            end
        end

        function T = macSummary(obj,pktNum)
        %macSummary Display MAC summary of the selected packet
        %
        %   macSummary(ANALYZER,PKTNUM) displays the MAC contents of the
        %   selected packet, PKTNUM.
        %
        %   The table contains the following columns:
        %
        %   AMPDU/MPDU Number          The AMPDU and associated MPDU
        %                              number within a MAC packet. This
        %                              column is only displayed for an HE
        %                              packet format.
        %
        %   STAID                      The STAID refer to the association
        %                              identifier (AID) of an HE-MU user.
        %                              The STAID column is only displayed
        %                              for an HE-MU packet format.
        %
        %   Address1                   Receiver address.
        %
        %   Address2                   Transmitter address.
        %
        %   AMPDU/MPDU Decode Status   Represents the result of MPDU
        %                              decoding, based on the enumeration
        %                              value of type <a href="matlab:help('wlanMACDecodeStatus')">wlanMACDecodeStatus</a>.
        %                              Any value of status other than
        %                              'Success' (0) indicates that the
        %                              MPDU decoding has failed. This
        %                              column is only displayed for an HE
        %                              packet format.
        %
        %   MPDU Decode Status         Represents the result of MPDU
        %                              decoding, based on the enumeration
        %                              value of type <a href="matlab:help('wlanMACDecodeStatus')">wlanMACDecodeStatus</a>.
        %                              Any value of status other than
        %                              'Success' (0) indicates that the
        %                              MPDU decoding has failed. This
        %                              column is only displayed for a
        %                              Non-HT packet.
        %
        %   MAC Frame Type             Specifies the MAC contents of the
        %                              detected packet as one of: A-MPDU,
        %                              RTS, CTS, ACK, BlockAck, Data, Null,
        %                              QoS Data, QoS Null, Beacon or
        %                              Unknown. If all users A-MPDUs are
        %                              deaggregated successfully then
        %                              display MAC contents as A-MPDU
        %                              otherwise display Unknown. If no
        %                              aggregation is used the MPDU frame
        %                              type is displayed. If an unsupported
        %                              MAC frame type or subtype is decoded
        %                              Unknown is displayed.
        %
        %   T = macSummary(ANALYZER,PKTNUM) returns the summary of the
        %   selected packet in a table.

            checkWaveformProcessed(obj);
            if isempty(obj.Results)
                T = [];
                return;
            end
            validatePktNum(obj.Results,pktNum);
            x = obj.Results{pktNum};
 
            if ~x.MAC(1).Processed
                % MAC information is not displayed for an unsupported
                % packet format or unsuccessful(PHY) status. No MAC is
                % displayed for an NDP packet.
                T = [];
                return;
            end

            if any(strcmp(x.Format,{'HE-SU','HE-MU','HE-EXT-SU'}))
                numUsers = numel(x.PHYConfig);
                count = 0;
                staID = [];
                ampduIndex = [];

                for u=1:numUsers
                    cfgRx = x.PHYConfig(u);
                    STAID = cfgRx.STAID; % Only applicable for HE MU
                    macInfo = x.MAC(u); % MAC info per user
                    deagStatus = macInfo.DeagStatus;
                    if strcmp(deagStatus,"Success") % If deaggregation is a success
                        numMPDU = numel(macInfo.MPDU);
                        for m=1:numMPDU
                            mpduDecodeStatus(count+m,1) = string(macInfo.MPDU(m).DecodeStatus); %#ok<*AGROW>
                            if strcmp(mpduDecodeStatus(count+m,1),"Success")
                                macFrameType(count+m,1) = string(x.MAC(u).MPDU(m).Config.FrameType);
                                address1(count+m,1) = string(macInfo.MPDU(m).Config.Address1);
                                if any(strcmp(macFrameType(count+m,1),{'CTS','ACK'}))
                                    address2(count+m,1) = "NA";
                                else
                                    address2(count+m,1) = string(macInfo.MPDU(m).Config.Address2);
                                end
                            else
                                macFrameType(count+m,1) = "Unknown";
                                address1(count+m,1) = "Unknown";
                                address2(count+m,1) = "Unknown";
                                if ~strcmp(mpduDecodeStatus(count+m,1),"FCSFailed")
                                    mpduContents = x.MAC.MPDUList(1,count+m);
                                    decOctets = hex2dec(mpduContents{1});
                                    mpduBits = reshape(de2bi(decOctets, 8)',[],1);
                                    if mpduBits(15) == true
                                        % Check the protected bit status,
                                        % if set the mpduDecodeStatus is
                                        % set to Success.
                                        mpduDecodeStatus(count+m,1) = "Success";
                                        address1(count+m,1) = string(macInfo.MPDU(m).Config.Address1);
                                        address2(count+m,1) = string(macInfo.MPDU(m).Config.Address2);
                                        macFrameType(count+m,1) = string(x.MAC(u).MPDU(m).Config.FrameType);
                                    end
                                end
                            end
                            ampduIndex = [ampduIndex; string(sprintf('AMPDU%d_MPDU%d',u,m))];
                        end
                        count = count+numMPDU;
                    else
                        address1(count+1,1) = "Unknown";
                        address2(count+1,1) = "Unknown";
                        macFrameType(count+1,1) = "Unknown";
                        mpduDecodeStatus(count+1,1) = macInfo.DeagStatus;
                        numMPDU = 1; % No information on number of MPDUs
                        ampduIndex = [ampduIndex; string(sprintf('AMPDU%d_MPDU%d',u,1))];
                        count = count+1;
                    end
                    staID = [staID; repmat(STAID,numMPDU,1)];
                end
            else % NonHT
                if strcmp(x.MAC.MPDU.DecodeStatus,"Success")
                    cfgMAC = x.MAC.MPDU.Config;
                    mpduStatus = x.MAC.MPDU.DecodeStatus;
                    macFrameType = string(cfgMAC.FrameType);
                    address1 = string(x.MAC.MPDU.Config.Address1);
                    if any(strcmp(macFrameType,{'CTS','ACK'}))
                        address2 = "NA";
                    else
                        address2 = string(x.MAC.MPDU.Config.Address2);
                    end
                else
                    mpduStatus = x.MAC.MPDU.DecodeStatus;
                    macFrameType = "Unknown";
                    address1 = "Unknown";
                    address2 = "Unknown";
                end
            end

            fprintf(' \n');
            if strcmp(x.Format,'Non-HT')
                fprintf([pad(' ',22),'<strong>Recovered MPDU Summary of Packet %d</strong>\n\n'],pktNum);
                macSummary = table(address1,address2,mpduStatus,macFrameType);
                macSummary.Properties.VariableNames{1} = 'Address1';
                macSummary.Properties.VariableNames{2} = 'Address2';
                macSummary.Properties.VariableNames{3} = 'MPDU Decode Status';
                macSummary.Properties.VariableNames{4} = 'MAC Frame Type';
                disp(macSummary)
            elseif strcmp(x.Format,'HE-MU')
                fprintf([pad(' ',36),'<strong>Recovered MPDU Summary of Packet %d</strong>\n\n'],pktNum);
                macSummary = table(ampduIndex,staID,address1,address2,mpduDecodeStatus,macFrameType);
                macSummary.Properties.VariableNames{1} = 'AMPDU/MPDU Number';
                macSummary.Properties.VariableNames{2} = 'STAID';
                macSummary.Properties.VariableNames{3} = 'Address1';
                macSummary.Properties.VariableNames{4} = 'Address2';
                macSummary.Properties.VariableNames{5} = 'AMPDU/MPDU Decode Status';
                macSummary.Properties.VariableNames{6} = 'MAC Frame Type';
                disp(macSummary)
            else
                fprintf([pad(' ',33),'<strong>Recovered MPDU Summary of Packet %d</strong>\n\n'],pktNum);
                macSummary = table(ampduIndex,address1,address2,mpduDecodeStatus,macFrameType);
                macSummary.Properties.VariableNames{1} = 'AMPDU/MPDU Number';
                macSummary.Properties.VariableNames{2} = 'Address1';
                macSummary.Properties.VariableNames{3} = 'Address2';
                macSummary.Properties.VariableNames{4} = 'AMPDU/MPDU Decode Status';
                macSummary.Properties.VariableNames{5} = 'MAC Frame Type';
                disp(macSummary)
            end

            if nargout
                T = macSummary;
            end
        end

        function plotWaveform(obj,pktNum)
        % plotWaveform Plot time domain samples and signal spectrum of the
        % selected packet
        %
        %   plotWaveform(ANALYZER,PKTNUM) plots the time domain samples
        %   of the detected packet and the spectrum of the selected packet,
        %   PKTNUM.

            if isempty(obj.Results)
                return;
            end
            checkWaveformProcessed(obj);
            x = obj.Results{pktNum};
            validatePktNum(obj.Results,pktNum);

            persistent spectrumEstimator
            lstfLength = 80*obj.wavsr/1e6; % Single symbol length
            hAreas = gobjects(1,5);
            figure;
            plot(20*log10(abs(obj.rawWaveform))+30);
            hold on;

            numPktSamples = x.NumRxSamples;
            pktStart = x.PacketOffset;
            pktEnd = pktStart+numPktSamples;
            switch x.Format
                case 'Non-HT'
                    hAreas(1) = area([pktStart pktEnd],[max(ylim) max(ylim)],min(ylim),'FaceAlpha',0.1,'FaceColor','g');
                case 'VHT'
                    hAreas(2) = area([pktStart pktEnd],[max(ylim) max(ylim)],min(ylim),'FaceAlpha',0.1,'FaceColor','y');
                case 'HT-MF'
                    hAreas(3) = area([pktStart pktEnd],[max(ylim) max(ylim)],min(ylim),'FaceAlpha',0.1,'FaceColor','b');
                case {'HE-SU','HE-MU','HE-EXT-SU'}
                    hAreas(4) = area([pktStart pktEnd],[max(ylim) max(ylim)],min(ylim),'FaceAlpha',0.1,'FaceColor','r');
                otherwise
                    hAreas(5) = area([pktStart pktEnd],[max(ylim) max(ylim)],min(ylim),'FaceAlpha',0.1,'FaceColor','k');
            end

            xlabel('Samples');
            ylabel('Power (dBm)');
            sampleOffset = max((-lstfLength + pktStart),1); % First index to plot
            sampleSpan = pktEnd + 2*lstfLength; % Number samples to plot
            % Plot as much of the packet (and L-STF worth of samples either
            % side of the packet) as we can
            plotIdx = sampleOffset:min(sampleSpan,numel(obj.rawWaveform));

            xlim([plotIdx(1) plotIdx(end)]);
            title(sprintf('Detection summary (packet %d)',pktNum));

            useIdx = arrayfun(@(x) ~isa(x,'matlab.graphics.GraphicsPlaceholder'),hAreas);
            formatVec = ["Non-HT","VHT","HT-MF","HE",x.Format];
            lgd = legend(hAreas(useIdx),formatVec(useIdx),'location','best');
            title(lgd,'Packet Format');

            % Plot spectral mask
            if isempty(spectrumEstimator)
                spectrumEstimator = dsp.SpectrumEstimator('SampleRate',obj.wavsr,...
                                    'PowerUnits','dBm',...
                                    'FrequencyRange','centered',...
                                    'SpectralAverages',10);
            end

            % Plot the spectral mask after averaging over all antennas
            endIdx = round(numPktSamples); % Upsampled end of packet
            pktOffset = round(pktStart); % Start of packet in txWaveform
            idx = pktOffset+(1:(endIdx)); % Get index of packet within upsampled waveforms
            idx(idx<1) = [];
            idx(idx>length(obj.rawWaveform)) = [];
            if isempty(idx) || isnan(numPktSamples)
                return
            end
            psdAvg = spectrumEstimator(mean(obj.rawWaveform(idx,:),2));
            freqAxis = spectrumEstimator.getFrequencyVector;
            figure;
            plot(freqAxis,psdAvg);
            title(sprintf('Power spectrum (packet %d)',pktNum));
            xlabel('Frequency (MHz)');
            ylabel('Power (dBm)');
            grid on;
            release(spectrumEstimator);
        end

        function T = fieldSummary(obj,pktNum)
        %fieldSummary Display field summary of the selected packet
        %
        %   fieldSummary(ANALYZER,PKTNUM) displays the field summary of the
        %   selected packet, PKTNUM.
        %
        %   The table contains the following columns:
        %
        %   Field Name            Field names for an HE-MU packet as
        %                         one of L-STF, L-LTF, L-SIG, RL-SIG,
        %                         HE-SIG-A, HE-SIG-B, HE-STF, HE-LTF and
        %                         HE-Data. The HE-SIG-B field is hidden for
        %                         HE-SU and HE-EXT-SU packet formats. For
        %                         Non-HT packet format only L-STF, L-LTF,
        %                         L-SIG and Data fields are displayed.
        %
        %   Modulation Type       The modulation type of all fields for the
        %                         selected packet.
        %
        %   MCS                   Modulation and coding scheme. This column
        %                         is only displayed for Non-HT packet
        %                         format.
        %
        %   Num Symbols           The number of OFDM symbols within each
        %                         packet field.
        %
        %   Parity Check/CRC      Parity/CRC information.
        %
        %   Power                 Power of each field in dBm.
        %
        %   RMS EVM               RMS EVM in dBs of the signaling and the
        %                         data field.
        %
        %   Max EVM               Max EVM in dBs of the signaling and the
        %                         data field.
        %
        %   T = fieldSummary(ANALYZER,PKTNUM) returns the field
        %   summary of the selected packet in a table.

            if isempty(obj.Results)
                T = [];
                return;
            end
            checkWaveformProcessed(obj);
            validatePktNum(obj.Results,pktNum);
            x = obj.Results{pktNum};

            if x.LSIG.FailCheck || any(strcmp(x.Status,{'Unsupported format','Incomplete packet','Non-HT duplicate not supported'}))
                T = [];
                return;
            end

            fprintf(' \n');
            fprintf([pad(' ',43), '<strong>Field Summary of Packet %d (%s)</strong>\n\n'],pktNum,x.Format);
            if strcmp(x.Format,'Non-HT')
                fieldName =  ['L-STF'; 'L-LTF'; 'L-SIG'; 'Data '];
                mod = getNonHTMod(x.PHYConfig.MCS);
                modType = ['BPSK '; 'BPSK '; 'BPSK '; mod];

                % Power
                power = n2str(powerdBm([x.Preamble.LSTFPower; x.Preamble.LLTFPower; ...
                        x.LSIG.Power; x.NonHTData.Power]));
                lsigParity = [repmat('    ',2,1); 'Pass'; '    '];
                mcs = [repmat(' ',3,1); num2str(x.PHYConfig.MCS,1)];

                rmsEVM = [repmat('       ',2,1); n2str([x.LSIG.EVMRMS; x.NonHTData.EVMRMS])];
                maxEVM = [repmat('       ',2,1); n2str([x.LSIG.EVMMax; x.NonHTData.EVMMax])];

                cfg = x.PHYConfig;
                s = cfg.validateConfig;
                numDataSymbols = s.NumDataSymbols;
                numSymbols = num2str([2; 2; 1; numDataSymbols]);

                pktFieldSummary = table(fieldName,modType,mcs,numSymbols,lsigParity,power,rmsEVM,maxEVM);
                pktFieldSummary.Properties.VariableNames{1} = 'Field Name';
                pktFieldSummary.Properties.VariableNames{2} = 'Modulation Type';
                pktFieldSummary.Properties.VariableNames{3} = 'MCS';
                pktFieldSummary.Properties.VariableNames{4} = 'Num Symbols';
                pktFieldSummary.Properties.VariableNames{5} = 'Parity Check';
                pktFieldSummary.Properties.VariableNames{6} = 'Power (dBm)';
                pktFieldSummary.Properties.VariableNames{7} = 'RMS EVM (dB)';
                pktFieldSummary.Properties.VariableNames{8} = 'Max EVM (dB)';
                disp(pktFieldSummary);
                fprintf('\n');
            else
                % CRC HE-SIG-A
                if x.HESIGA.FailCRC
                    return; % Do not process any further
                end
                fieldName =  ['L-STF   '; 'L-LTF   '; 'L-SIG   '; 'RL-SIG  '; 'HE-SIG-A'; 'HE-SIG-B'; 'HE-STF  '; 'HE-LTF  '; 'Data    '];
                modType = ['BPSK'; 'BPSK'; 'BPSK'; 'BPSK'; 'BPSK'; 'QPSK'; 'BPSK'; 'BPSK'; '    '];

                % Get number of data symbols from the recover object
                cfgRx = x.PHYConfig(1);
                s = validateConfig(cfgRx,'DataLocationLength'); % Same for all users
                if strcmp(x.Format,'HE-MU')
                    if x.HESIGB.User.Processed % If user field is successfully processed only then display the relavent information
                        infoSIGB = cfgRx.getSIGBLength;
                        numLTFSym = cfgRx.NumHELTFSymbols;
                        numSymbols = num2str([2; 2; 1; 1; 2; infoSIGB.NumSIGBSymbols; 1; numLTFSym; s.NumDataSymbols]);
                    else
                        numSymbols = [num2str([2; 2; 1; 1; 2]); ' '; ' '; ' '; ' '];
                    end

                    % CRC HE-SIG-A
                    if x.HESIGA.FailCRC
                        return; % Do not process any further
                    end

                    power = [n2str(powerdBm([x.Preamble.LSTFPower; x.Preamble.LLTFPower; ...
                             x.LSIG.Power; x.RLSIG.Power; ...
                             x.HESIGA.Power])); repmat('       ',4,1)];

                    crcHESIG = [repmat('    ',2,1); 'Pass'; 'Pass'; 'Pass'; 'Fail'; repmat('    ',3,1)];

                    % EVM
                    rmsEVM = [repmat('       ',2,1); n2str([x.LSIG.EVMRMS; x.RLSIG.EVMRMS; ...
                        x.HESIGA.EVMRMS]); repmat('       ',4,1)];

                    maxEVM = [repmat('       ',2,1); n2str([x.LSIG.EVMMax; x.RLSIG.EVMMax; ...
                        x.HESIGA.EVMMax]); repmat('       ',4,1)];

                    if x.HESIGB.User.Processed
                        % Show power and EVMs if the HE-SIG-B user field has been processed
                        power(6,:) = n2str(powerdBm(x.HESIGB.Power));
                        rmsEVM(6,:) = n2str(x.HESIGB.EVMRMS);
                        maxEVM(6,:) = n2str(x.HESIGB.EVMMax);
                    end

                    if cfgRx.SIGBCompression
                        if (x.HESIGB.User.Processed && strcmp(x.HESIGB.User.Status,'Success'))
                            crcHESIG(6,:) = 'Pass';
                        end
                    else
                        if (x.HESIGB.Common.Processed && strcmp(x.HESIGB.Common.Status,'Success')) && ...
                                (x.HESIGB.User.Processed && strcmp(x.HESIGB.User.Status,'Success'))
                            crcHESIG(6,:) = 'Pass';
                        end
                    end

                    if x.HEPreamble.Processed
                        power(7:8,:) = n2str(powerdBm([x.HEPreamble.HESTFPower; x.HEPreamble.HELTFPower]));
                    end

                    if x.HEData.Processed
                        power(9,:) = n2str(powerdBm(x.HEData.Power));
                    end
                else % HE-SU, HE-EXT-SU
                    fieldName = fieldName([1:5,7:9]',:);
                    modType = modType([1:5,7:9]',:);
                    numLTFSym = cfgRx.NumHELTFSymbols;
                    if strcmp(x.Format,'HE-SU')
                        numSymbols = num2str([2; 2; 1; 1; 2; 1; numLTFSym; s.NumDataSymbols]);
                    else % 'HE-EXT-SU'
                        numSymbols = num2str([2 ; 2; 1; 1; 4; 1; numLTFSym; s.NumDataSymbols]);
                    end

                    % CRC HE-SIG-A
                    crcHESIG = [repmat('    ',2,1); 'Pass'; 'Pass'; 'Pass'; repmat('    ',3,1)];
                    power =  [n2str(powerdBm([x.Preamble.LSTFPower; x.Preamble.LLTFPower; ...
                             x.LSIG.Power; x.RLSIG.Power; x.HESIGA.Power])); repmat('       ',3,1)];
                         
                    if x.HEData.Processed
                        power(8,:) = n2str(powerdBm(x.HEData.Power));
                    else
                        power(8,:) = '       '; % NDP case
                    end
                    
                    if x.HEPreamble.Processed % For HighDoppler case
                        power(6:7,:) = n2str(powerdBm([x.HEPreamble.HESTFPower; x.HEPreamble.HELTFPower]));
                    end
                    
                    % EVM
                    rmsEVM = [repmat('       ',2,1); n2str([x.LSIG.EVMRMS; x.RLSIG.EVMRMS; ...
                             x.HESIGA.EVMRMS]); repmat('       ',3,1)];
                    maxEVM = [repmat('       ',2,1); n2str([x.LSIG.EVMMax; x.RLSIG.EVMMax; ...
                             x.HESIGA.EVMMax]); repmat('       ',3,1)];
                end

                pktFieldSummary = table(fieldName,modType,numSymbols,crcHESIG,power,rmsEVM,maxEVM);
                pktFieldSummary.Properties.VariableNames{1} = 'Field Name';
                pktFieldSummary.Properties.VariableNames{2} = 'Modulation Type';
                pktFieldSummary.Properties.VariableNames{3} = 'Num Symbols';
                pktFieldSummary.Properties.VariableNames{4} = 'Parity Check/CRC';
                pktFieldSummary.Properties.VariableNames{5} = 'Power (dBm)';
                pktFieldSummary.Properties.VariableNames{6} = 'RMS EVM (dB)';
                pktFieldSummary.Properties.VariableNames{7} = 'Max EVM (dB)';
                disp(pktFieldSummary);
                fprintf('\n');
            end

            if nargout
                T = pktFieldSummary;
            end
        end

        function T = signalingSummary(obj,pktNum)
        %signalingSummary Display signaling field summary of the selected
        %packet
        %
        %   signalingSummary(ANALYZER,PKTNUM) displays the signaling field
        %   summary of the selected packet.
        %
        %   The table contains the following columns:
        %
        %   Property   List the L-SIG (Figure 17-5 of IEEE Std
        %              802.11-2016) and HE-SIG-A field name (Table 27-18/19
        %              of IEEE P802.11ax/D4.1, April 2019).
        %
        %   Value      List the decoded, L-SIG and HE-SIG-A field values
        %              of the selected packet.
        %
        %   T = signalingSummary(ANALYZER,PKTNUM) returns the signaling
        %   field summary of the selected packet in a table.

            if isempty(obj.Results)
                T = [];
                return;
            end
            checkWaveformProcessed(obj);
            validatePktNum(obj.Results,pktNum);
            x = obj.Results{pktNum};

            % Nothing to display if L-SIG parity fails
            if strcmp(x.Status,'L-SIG check fail')
                T = [];
                return;
            end

            fprintf(' \n');
            if any(strcmp(x.Format,{'HE-MU','HE-SU','HE-EXT-SU'}))
                cfgRx = x.PHYConfig(1); % Common for all users
                if strcmp(x.Status,"HE-SIG-A FailCRC")
                    displayLSIGContents(x)
                    return;
                end

                title = ["Property","Value","Property","Value","Property","Value"];
                uplinkIndication = 'DL';
                if cfgRx.UplinkIndication
                    uplinkIndication = 'UL';
                end

                stbc = 'False';
                if cfgRx.STBC
                    stbc = 'True'; 
                end

                doppler = 'False';
                midamblePre = '';
                if cfgRx.HighDoppler
                    doppler = 'True';
                    midamblePre = num2str(cfgRx.MidamblePeriodicity);
                end

                if cfgRx.LDPCExtraSymbol
                    ldpcExtraSymbol = 'True';
                else
                    ldpcExtraSymbol = 'False';
                end

                PEDisambiguity = 'False';
                if cfgRx.PEDisambiguity
                    PEDisambiguity = 'True';
                end

                lsigInfo = x.LSIG.Info;
                rate = lsigRate(lsigInfo.MCS);
                fprintf([pad(' ',31),'<strong>Signaling Field Summary of Packet %d (%s)</strong>\n\n'],pktNum,x.Format);
                if strcmp(x.Format,'HE-MU')
                    sigbDCM = 'False';
                    if cfgRx.SIGBDCM
                        sigbDCM = 'True'; 
                    end
                    sigBCompression = 'False';
                    varField = "Num HE-SIG-B Symbols";
                    combinedSIGBUser = string(cfgRx.NumSIGBSymbolsSignaled);
                    if cfgRx.SIGBCompression
                        sigBCompression = 'True';
                        varField = "Num MU-MIMO Users";
                        combinedSIGBUser = sum(cfgRx.NumUsersPerContentChannel);
                    end

                    column = ["L-SIG Length", "L-SIG Rate", "UL/DL Indication", "SIGB MCS", "SIGB DCM", "BSS Color", "Spatial Reuse", ... % 1:7
                              "Bandwidth", "PreamblePuncturing", varField, "SIGB Compression", "Guard Interval", "HE-LTF Type", "Doppler", ...% 8:14
                              "TXOP", "Num HE-LTF Symbols", "Midamble Perodicity", "LDPC Extra Symbol", "STBC", "Pre-FEC Padding Factor", "PE Disambiguity"]; % 15:21

                    value = [string(lsigInfo.Length), string(rate), string(uplinkIndication), string(cfgRx.SIGBMCS), string(sigbDCM), string(cfgRx.BSSColor), string(cfgRx.SpatialReuse), ...
                             string(cfgRx.ChannelBandwidth), string(cfgRx.PreamblePuncturing), combinedSIGBUser, string(sigBCompression) ...
                             string(cfgRx.GuardInterval), string(cfgRx.HELTFType), string(doppler), string(cfgRx.TXOPDuration), string(cfgRx.NumHELTFSymbols), ...
                             string(cfgRx.MidamblePeriodicity), string(ldpcExtraSymbol), string(stbc), string(cfgRx.PreFECPaddingFactor), string(PEDisambiguity)];

                    numVal = 1:numel(column);
                    delIndex = [];
                    if any(strcmp(cfgRx.ChannelBandwidth,{'CBW20','CBW40'}))
                        delIndex = 9;
                    end

                    if strcmp(doppler,'False') % Hide MidamblePeriodicity
                        delIndex = [delIndex 17];
                    end
                    N = 7;
                else % HE-SU, HE-EXT-SU
                    dcm = 'False';
                    if cfgRx.DCM
                       dcm = 'True';
                    end

                    upper106ToneRU = 'False';
                    if cfgRx.RUSize==106 && cfgRx.RUIndex==2
                       upper106ToneRU = 'True'; 
                    end

                    beamforming = 'False';
                    if cfgRx.Beamforming
                      beamforming = 'True';
                    end

                    preHESpatialMapping = 'False';
                    if cfgRx.PreHESpatialMapping
                        preHESpatialMapping = 'True';
                    end

                    column = ["L-SIG Length", "L-SIG Rate", "Format", "Upper 106Tone RU", "Beam Change", "UL/DL Indication", "MCS", ...% 1:7
                             "DCM", "BSS Color", "Spatial Reuse", "Bandwidth", "Guard Interval", "HE-LTF Type", "Num Space-Time Streams", ...% 8:15
                             "Num HE-LTF Symbols", "Midamble Perodicity", "TXOP", "Channel Coding", "LDPC Extra Symbol", "STBC", "Beamformed", ...% 16:22
                             "Pre-FEC Padding Factor", "PE Disambiguity", "Doppler"]; % 23:27

                    value = [string(lsigInfo.Length), string(rate), string(cfgRx.PacketFormat), string(upper106ToneRU), string(preHESpatialMapping), string(uplinkIndication), string(cfgRx.MCS), ... %1:7
                             string(dcm), string(cfgRx.BSSColor), string(cfgRx.SpatialReuse), string(cfgRx.ChannelBandwidth), string(cfgRx.GuardInterval), string(cfgRx.HELTFType), string(cfgRx.NumSpaceTimeStreams), ... 8:15
                             string(cfgRx.NumHELTFSymbols), string(midamblePre), string(cfgRx.TXOPDuration), string(cfgRx.ChannelCoding), string(ldpcExtraSymbol), string(stbc), string(beamforming), ...% 16:22
                             string(cfgRx.PreFECPaddingFactor), string(PEDisambiguity), string(doppler)]; % 23:27

                    numVal = 1:numel(column);
                    delIndex = [];
                    if strcmp(doppler,'False') % Hide MidamblePeriodicity
                        delIndex = 16;
                    end

                    if ~strcmp(cfgRx.PacketFormat,'HE-EXT-SU')
                        delIndex = [4 delIndex];
                    end
                    N = 8;
                end

                visibleIndex = setxor(numVal,delIndex);
                column = [column(visibleIndex), repmat("",1,numel(delIndex))];
                value = [value(visibleIndex), repmat("",1,numel(delIndex))];
                coIdx = reshape(numVal,N,3);

                % LOOP over column number
                titleList = reshape(title,2,3);
                for c=1:3
                    col(:,c) = pad([titleList(1,c) column(coIdx(:,c))])'; %#ok<*AGROW>
                    col(2:end,c) = strjust(col(2:end,c),'left');
                    val(:,c) = pad([titleList(2,c) value(coIdx(:,c))])';
                    val(2:end,c) = strjust(val(2:end,c),'left');
                end

                titlePad = strjust([col(1,1) val(1,1) col(1,2) val(1,2) col(1,3) val(1,3)],'center');
                sep = arrayfun(@(x)string(repmat('_',1,strlength(x))),titlePad);
                fprintf('    <strong>%s    %s    %s    %s   %s     %s</strong> \n',titlePad);
                fprintf('    %s    %s    %s    %s    %s    %s\n\n',sep);
                fprintf('    %s    %s    %s    %s    %s    %s\n',[col(2:end,1) val(2:end,1) col(2:end,2) val(2:end,2) col(2:end,3) val(2:end,3)]');
                if nargout
                    T = table(col(2:end)',val(2:end)');
                    T.Properties.VariableNames{1} = 'Property';
                    T.Properties.VariableNames{2} = 'Value';
                end
            else
                T = displayLSIGContents(x);
            end
        end

        function T = ruSummary(obj,pktNum)
        %ruSummary Display resource unit (RU) summary of the selected packet  
        %   ruSummary(ANALYZER,PKTNUM) displays the RU summary of the
        %   selected packet, PKTNUM. The RU information is only displayed
        %   for an HE packet.
        %
        %   The table contains the following columns:
        %
        %   RU Number                Resource unit number.
        %
        %   RU Size                  Resource unit size.
        %
        %   Subcarrier Index (Start) The starting subcarrier index of an
        %                            RU.
        %
        %   Subcarrier Index (End)   The last subcarrier index of an RU.
        %
        %   Num Users                Number of users in an RU. This column
        %                            is only displayed for an HE MU packet
        %                            format.
        %
        %   Num STS                  Number of space-time streams in an RU.
        %                            This column is only displayed for an
        %                            HE MU packet format.
        %
        %   Power                    RU power in dBm.
        %
        %   T = ruSummary(ANALYZER,PKTNUM) returns the RU summary of
        %   the selected packet in a table.

            if isempty(obj.Results)
                T = [];
                return;
            end
            checkWaveformProcessed(obj);
            validatePktNum(obj.Results,pktNum);
            x = obj.Results{pktNum};

            if ~strcmp(x.Status,"Success") || ~any(strcmp(x.Format,{'HE-SU','HE-MU','HE-EXT-SU'})) || ~x.HEData.Processed
                T = [];
                return;
            end

            fprintf(' \n');
            if strcmp(x.Format,'HE-MU')
                fprintf([pad(' ',36),'<strong>Resource Unit (RU) Information of Packet %d (%s)</strong>\n\n'],pktNum,x.Format);
                cfgRx = x.PHYConfig(1); % RU information is common for all users
                cbw = wlan.internal.cbwStr2Num(cfgRx.ChannelBandwidth);
                numRUs = numel(x.HEPreamble.RU);
                ruName = strings(numRUs,1);
                ruSize = zeros(numRUs,1);
                ruStartInd = zeros(numRUs,1);
                ruEndInd = zeros(numRUs,1);
                numUserPerRU = zeros(numRUs,1);
                numSTSPerRU = zeros(numRUs,1);
                ruPower = zeros(numRUs,1);
                for ru=1:numRUs
                    ruName(ru) = string(['RU' num2str(ru)]);
                    ruSize(ru) = x.HEPreamble.RU(ru).RUSize;
                    [dataInd,pilotInd] = wlan.internal.heSubcarrierIndices(cbw,ruSize(ru),x.HEPreamble.RU(ru).RUIndex);
                    ruIndex = sort([dataInd;pilotInd]);
                    ruStartInd(ru) = min(ruIndex);
                    ruEndInd(ru) = max(ruIndex);
                    numUserPerRU(ru) = numel(x.HEPreamble.RU(ru).UserNumbers);
                    userNumber = x.HEPreamble.RU(ru).UserNumbers;
                    numSTSPerRU(ru) = x.PHYConfig(userNumber).RUTotalSpaceTimeStreams;
                    ruPower(ru) = powerdBm(x.HEData.RU(ru).Power);
                end

                ruTable = table(ruName,ruSize,ruStartInd,ruEndInd,numUserPerRU,numSTSPerRU,ruPower);
                ruTable.Properties.VariableNames{1} = 'RU Number';
                ruTable.Properties.VariableNames{2} = 'RU Size';
                ruTable.Properties.VariableNames{3} = 'Subcarrier Index (Start)';
                ruTable.Properties.VariableNames{4} = 'Subcarrier Index (End)';
                ruTable.Properties.VariableNames{5} = 'Num Users';
                ruTable.Properties.VariableNames{6} = 'Num STS';
                ruTable.Properties.VariableNames{7} = 'Power (dBm)';
                disp(ruTable);
            elseif strcmp(x.Format,'HE-SU') || strcmp(x.Format,'HE-EXT-SU')
                fprintf([pad(' ',18),'<strong>Resource Unit (RU) Information of Packet %d (%s)</strong>\n\n'],pktNum,x.Format);
                recConfig = x.PHYConfig;
                cbw = wlan.internal.cbwStr2Num(recConfig.ChannelBandwidth);
                [dataInd,pilotInd] = wlan.internal.heSubcarrierIndices(cbw,recConfig.RUSize,1);
                ruIndex = sort([dataInd;pilotInd]);
                ruName = string(['RU' num2str(1)]);
                ruPower = powerdBm(x.HEData.RU.Power);

                ruTable = table(ruName,recConfig.RUSize,min(ruIndex),max(ruIndex),ruPower);
                ruTable.Properties.VariableNames{1} = 'RU Number';
                ruTable.Properties.VariableNames{2} = 'RU Size';
                ruTable.Properties.VariableNames{3} = 'Subcarrier Index (Start)';
                ruTable.Properties.VariableNames{4} = 'Subcarrier Index (End)';
                ruTable.Properties.VariableNames{5} = 'Power (dBm)';
                disp(ruTable);
                fprintf('\n');
            end

            if nargout
                T = ruTable;
            end
        end

        function T = userSummary(obj,pktNum)
        %userSummary Display the user summary
        %
        %   userSummary(ANALYZER,PKTNUM) displays the user properties of
        %   the selected packet, PKTNUM.
        %
        %   The table contains the following columns:
        %
        %   STAID                   Station identification. This column is
        %                           only visible for an HE MU packet.
        %
        %   RU Number               Resource unit number.  
        %
        %   MCS                     Modulation and coding scheme.
        %
        %   Modulation Type         Modulation type. This column is only
        %                           visible for an HE MU packet.
        %
        %   DCM                     Dual carrier modulation.
        %
        %   Channel Coding          Channel coding information.
        %
        %   Num STS                 Number of space-time streams per user.
        %
        %   Transmit BeamForming    Beamforming steering matrix.
        %
        %   T = userSummary(ANALYZER,PKTNUM) returns the user summary
        %   of the selected packet in a table.

            if isempty(obj.Results)
                T = [];
                return;
            end
            checkWaveformProcessed(obj);
            validatePktNum(obj.Results,pktNum);
            x = obj.Results{pktNum};
            if ~strcmp(x.Status,"Success") || ~any(strcmp(x.Format,{'HE-SU','HE-MU','HE-EXT-SU'})) || ~x.HEData.Processed
                T = [];
                return;
            end

            fprintf(' \n');
            if strcmp(x.Format,'HE-MU')
                fprintf([pad(' ',40),'<strong>User Information of Packet %d (%s)</strong>\n\n'],pktNum,x.Format);
                cfgUsers = x.PHYConfig;
                numUsers = numel(cfgUsers);
                stdID = zeros(numUsers,1);
                nsts = zeros(numUsers,1);
                mcs = zeros(numUsers,1);
                dcm = zeros(numUsers,1);
                chCoding = strings(numUsers,1);
                BF = zeros(numUsers,1);
                modType = strings(numUsers,1);
                numRUs = numel(x.HEPreamble.RU);
                ruName = [];
                offset = 0;
                for ru=1:numRUs
                    numUserPerRU = numel(x.HEPreamble.RU(ru).UserNumbers);
                    for u=1:numUserPerRU
                        cfgRx = cfgUsers(x.HEPreamble.RU(ru).UserNumbers(u));
                        stdID(u+offset) = cfgRx.STAID;
                        nsts(u+offset) = cfgRx.NumSpaceTimeStreams;
                        mcs(u+offset) = cfgRx.MCS;
                        dcm(u+offset) = cfgRx.DCM;
                        chCoding(u+offset) = string(cfgRx.ChannelCoding);
                        BF(u+offset) = cfgRx.Beamforming;
                        modType(u+offset) = string(getHEModulationType(cfgRx.MCS));
                    end
                    offset = offset+numUserPerRU;
                    ruName = [ruName; repmat(string(['RU' num2str(ru)]),u,1)];
                end

                userTable = table(stdID,ruName,mcs,modType,dcm,chCoding,nsts,BF);
                userTable.Properties.VariableNames{1} = 'STAID';
                userTable.Properties.VariableNames{2} = 'RU Number';
                userTable.Properties.VariableNames{3} = 'MCS';
                userTable.Properties.VariableNames{4} = 'Modulation Type';
                userTable.Properties.VariableNames{5} = 'DCM';
                userTable.Properties.VariableNames{6} = 'Channel Coding';
                userTable.Properties.VariableNames{7} = 'Num STS';
                userTable.Properties.VariableNames{8} = 'Transmit BeamForming';
                disp(userTable);
            elseif strcmp(x.Format,'HE-SU') || strcmp(x.Format,'HE-EXT-SU')
                fprintf([pad(' ',28),'<strong>User Information of Packet %d (%s)</strong>\n\n'],pktNum,x.Format);
                cfgRx = x.PHYConfig;
                ruName(1) = string(['RU' num2str(1)]);
                nsts = cfgRx.NumSpaceTimeStreams;
                mcs = cfgRx.MCS;
                modType = string(getHEModulationType(cfgRx.MCS));
                dcm = cfgRx.DCM;
                chCoding(1) = string(cfgRx.ChannelCoding);
                BF = cfgRx.Beamforming;
                userTable = table(ruName,nsts,mcs,modType,dcm,chCoding,BF);
                userTable.Properties.VariableNames{1} = 'RU Number';
                userTable.Properties.VariableNames{2} = 'Num STS';
                userTable.Properties.VariableNames{3} = 'MCS';
                userTable.Properties.VariableNames{4} = 'Modulation Type';
                userTable.Properties.VariableNames{5} = 'DCM';
                userTable.Properties.VariableNames{6} = 'Channel Coding';
                userTable.Properties.VariableNames{7} = 'Transmit BeamForming';
                disp(userTable);
            end

            if nargout
                T = userTable;
            end
        end

        function T = userEVM(obj,pktNum)
        %userEVM Display EVM per spatial stream
        %
        %   userEVM(ANALYZER,PKTNUM) displays the EVM per spatial stream
        %   for the selected packet, PKTNUM.
        %
        %   The table contains the following columns:
        %
        %   STAID                  Station identification. This column is
        %                          only visible for an HE MU packet.
        %
        %   Spatial Stream Index   Staring space-time stream index within
        %                          an RU. This column is only displayed for
        %                          an HE MU packet format.
        %
        %   RMS EVM                RMS EVM in dBs of the signaling and the
        %                          data field.
        %
        %   Max EVM                Max EVM in dBs of the signaling and the
        %                          data field.
        %
        %   T = userEVM(ANALYZER,PKTNUM) returns the EVM of all
        %   spatial streams of the selected packet in a table.

            if isempty(obj.Results)
                 T = [];
                return;
            end
            checkWaveformProcessed(obj);
            validatePktNum(obj.Results,pktNum);
            x = obj.Results{pktNum};

            if ~strcmp(x.Status,"Success") || ~any(strcmp(x.Format,{'HE-SU','HE-MU','HE-EXT-SU'})) || ~x.HEData.Processed
                 T = [];
                return;
            end

            fprintf(' \n');
            if strcmp(x.Format,"HE-MU")
                fprintf([pad(' ',10),'<strong>User EVM per Spatial Stream of Packet %d (%s)</strong>\n\n'],pktNum,x.Format);
                cfgPkt = x;
                cfgUsers = cfgPkt.PHYConfig;
                stdID = [];
                evmRMS = [];
                evmMax =[];
                numSTS = [];
                for u=1:numel(cfgUsers)
                    cfgRx = cfgUsers(u);
                    if cfgRx.STBC
                        nsts = cfgRx.NumSpaceTimeStreams/2;
                    else
                        nsts = cfgRx.NumSpaceTimeStreams;
                    end
                    stdID = [stdID; repmat(cfgRx.STAID,nsts,1)]; %#ok<*AGROW>
                    evmRMS = [evmRMS; cfgPkt.HEData.User(u).EVMRMS];
                    evmMax = [evmMax; cfgPkt.HEData.User(u).EVMMax];
                    numSTS =  [numSTS; (1:nsts).'];
                end

                evmTable = table(stdID,numSTS,evmRMS,evmMax);
                evmTable.Properties.VariableNames{1} = 'STAID';
                evmTable.Properties.VariableNames{2} = 'Spatial Stream Index';
                evmTable.Properties.VariableNames{3} = 'RMS EVM (dB)';
                evmTable.Properties.VariableNames{4} = 'Max EVM (dB)';
                disp(evmTable)
            elseif strcmp(x.Format,'HE-SU') || strcmp(x.Format,'HE-EXT-SU')
                fprintf([pad('',5),'<strong>User EVM per Spatial Stream of Packet %d (%s)</strong>\n\n'],pktNum,x.Format);
                cfgRx = x.PHYConfig(1);
                cfgPkt =  x;
                if cfgRx.STBC
                    numSTS = 1:cfgRx.NumSpaceTimeStreams/2;
                else
                    numSTS = (1:cfgRx.NumSpaceTimeStreams).';
                end
                userEVM = cfgPkt.HEData.User.EVMRMS;
                userRMS = cfgPkt.HEData.User.EVMMax;
                evmTable = table(numSTS,userEVM,userRMS);
                evmTable.Properties.VariableNames{1} = 'Spatial Stream Index';
                evmTable.Properties.VariableNames{2} = 'RMS EVM (dB)';
                evmTable.Properties.VariableNames{3} = 'Max EVM (dB)';
                disp(evmTable)
            end

            if nargout
                T = evmTable;
            end
        end

        function plotConstellation(obj,pktNum)
        %plotConstellation Plot equalized data field symbols for each user
        %of a selected packet.

            if isempty(obj.Results)
                return;
            end

            if ~obj.isProcessed || isempty(obj.Results)
                error('No waveform to anaylze') 
            end

            validatePktNum(obj.Results,pktNum);
            x = obj.Results{pktNum};
            if ~strcmp(x.Status,"Success")
                return;
            end

            figure;
            fprintf(' \n');
            if strcmp(x.Format,'Non-HT')
                numUsers = 1;
                eqSym = x.NonHTData.EQDataSym;
                mcs = x.PHYConfig.MCS;
                h = plot(eqSym(:),'.'); hold on;
                legendStr = arrayfun(@(x)sprintf('MCS %d',x),mcs,'UniformOutput',false);
            else
               if ~x.HEData.Processed
                    return; % For NDP
                end
                numUsers = numel(x.PHYConfig);
                staID = zeros(1,numUsers);
                h = gobjects(numUsers,1);
                for u=1:numUsers
                    cfgUser = x.PHYConfig(u);
                    eqSymUser = x.HEData.User(u).EQDataSym;
                    [Nsd,Nsym,Nss] = size(eqSymUser);
                    eqDataSymPerSS = reshape(eqSymUser,Nsd*Nsym,Nss);
                    h(u) = plot(eqDataSymPerSS(:),'.'); hold on;
                    staID(u) = cfgUser.STAID;
                end
                legendStr = arrayfun(@(x)sprintf('STAID %d',x),staID,'UniformOutput',false);
            end

            % Plot reference constellation for all users
            for u=1:numUsers
                cfgUser = x.PHYConfig(u);
                referenceConstellation = wlanReferenceSymbols(cfgUser);
                referenceConstellation = complex(referenceConstellation);
                href = plot(referenceConstellation,'+','Color','k'); hold on;
            end

            legend show
            xlabel('In-phase Amplitude');
            ylabel('Quadrature Amplitude');
            title(sprintf('Equalized data symbols (packet %d)',pktNum));
            legend([h; href],[legendStr {'Ref'}] ,'Location','NorthEastOutside');

            axis square;
            m = max(abs(xlim(gca)));
            xlim([-m m]); ylim([-m m]);
            grid on;
        end

        function plotEVM(obj,pktNum)
        %plotEVM Plot EVM per subcarrier and data field symbols of a
        %selected packet. The plotted EVM is additionally averaged over
        %spatial streams.
        
            if isempty(obj.Results)
                return
            end
            checkWaveformProcessed(obj);
            validatePktNum(obj.Results,pktNum);

            x = obj.Results{pktNum};
            fprintf(' \n');
            figure;
            if ~strcmp(x.Status,"Success")
                return;
            end

            if strcmp(x.Format,'Non-HT')
                cfgUser = x.PHYConfig;
                eqSym = x.NonHTData.EQDataSym;
                [evmRMS,~,carrierIndex] = getEVMPerSubcarrier(eqSym,cfgUser,'Non-HT');
                plot(carrierIndex,evmRMS,'.-'); hold on;
            else
                if ~x.HEData.Processed
                    return; % For NDP
                end
                numUsers = numel(x.PHYConfig);
                staID = zeros(1,numUsers);
                for u=1:numUsers
                    cfgUser = x.PHYConfig(u);
                    eqSymUser = x.HEData.User(u).EQDataSym;
                    [evmRMS,~,carrierIndex] = getEVMPerSubcarrier(eqSymUser,cfgUser,'HE');
                    plot(carrierIndex,evmRMS,'.-'); hold on;
                    staID(u) = cfgUser.STAID;
                end
                legendStr = arrayfun(@(x)sprintf('STAID %d',x),staID,'UniformOutput',false);
            end

            xlabel('Subcarrier number');
            ylabel('EVM (dB)');
            xlim([carrierIndex(1) carrierIndex(end)])
            title(sprintf('Average EVM (RMS) per data subcarrier (packet %d)',pktNum));
            if strcmp(x.Format,'HE-MU')
                legend(legendStr,'Location','NorthEastOutside');
            end
            grid on;

            fprintf(' \n');
            figure;
            % Plot EVM per symbol
            if strcmp(x.Format,'Non-HT')
                cfgUser = x.PHYConfig;
                eqSym = x.NonHTData.EQDataSym;
                [evmRMS,~] = getEVMPerSymbol(eqSym,cfgUser);
                plot(1:numel(evmRMS),evmRMS,'.-'); hold on;
            else
                for u=1:numUsers
                    cfgUser = x.PHYConfig(u);
                    eqSymUser = x.HEData.User(u).EQDataSym;
                    [evmRMS,~] = getEVMPerSymbol(eqSymUser,cfgUser);
                    plot(1:numel(evmRMS),evmRMS,'.-'); hold on;
                end
            end
            
            xlabel('Symbol number');
            ylabel('EVM (dB)');
            title(sprintf('Average EVM (RMS) per symbol (packet %d)',pktNum));
            if strcmp(x.Format,'HE-MU')
                legend(legendStr,'Location','NorthEastOutside');
            end
            grid on;
        end

        function process(obj,rxWaveform,chanbw,varargin)
        %process Detect, decode and analyze WLAN packets within a waveform  
        %   process(ANALYZER,WAVEFORM,CHANBW) performs analysis of packets
        %   within a waveform.
        %
        %   WAVEFORM is a Ns-by-Nr complex array containing the waveform to
        %   process. Ns is the number of samples and Nr is the number of
        %   receive antennas.
        %
        %   CHANBW is the channel bandwidth of packets within WAVEFORM.
        %   It must be 'CBW20', 'CBW40', 'CBW80', or 'CBW160'.
        %
        %   process(ANALYZER,WAVEFORM,CHANBW,SR) additionally allows the
        %   sample rate of WAVEFORM to be specified in Hertz. If not
        %   provided the nominal sample rate of CHANBW is assumed. If SR is
        %   greater than the nominal sample rate, the waveform is
        %   resampled.

           obj.Results = process@WaveformAnalysisEngine(obj,rxWaveform,chanbw,varargin{:});
        end

        function results = getResults(obj)
        %getResults Returns analysis results
        %   RESULTS is a cell array of structures, RESULTS, containing the
        %   analysis result for each detected packet. The structure
        %   contents depends on the detected packet format and contents.

            checkWaveformProcessed(obj);
            results = obj.Results;
       end
   end
end

function [evmRMS,evmMAX,carrierIndex] = getEVMPerSubcarrier(eqDataSym,cfgRx,field)
% getEVMPerSubcarrier Get EVM per subcarrier

EVM = comm.EVM;
EVM.AveragingDimensions = [2 3];
EVM.MaximumEVMOutputPort = true;
EVM.ReferenceSignalSource = 'Estimated from reference constellation';
if cfgRx.MCS==0
    EVM.ReferenceConstellation = complex(wlanReferenceSymbols(cfgRx));
else
    EVM.ReferenceConstellation = wlanReferenceSymbols(cfgRx);
end
[rmsEVM,maxEVM] = EVM(eqDataSym);

switch field
    case 'HE'
        ofdmInfo = wlanHEOFDMInfo('HE-Data',cfgRx.ChannelBandwidth,cfgRx.GuardInterval,[cfgRx.RUSize cfgRx.RUIndex]);
    otherwise
        ofdmInfo = wlanNonHTOFDMInfo('NonHT-Data');
end
dataInd = ofdmInfo.ActiveFFTIndices(ofdmInfo.DataIndices);
Nfft = ofdmInfo.FFTLength;

evmRMS = nan(Nfft,1);
evmMAX = nan(Nfft,1);
evmRMS(dataInd,1) = 20*log10(rmsEVM/100);
evmMAX(dataInd,1) = 20*log10(maxEVM/100);
carrierIndex = -Nfft/2:Nfft/2-1;

end

function [rmsEVMSym,maxEVMSym] = getEVMPerSymbol(eqSymUser,cfgRx)

EVM = comm.EVM;
EVM.AveragingDimensions = [1 3];
EVM.MaximumEVMOutputPort = true;
EVM.ReferenceSignalSource = 'Estimated from reference constellation';

if cfgRx.MCS==0
    EVM.ReferenceConstellation = complex(wlanReferenceSymbols(cfgRx));
else
    EVM.ReferenceConstellation = wlanReferenceSymbols(cfgRx);
end
[rmsEVM,maxEVM] = EVM(eqSymUser); % Follow same averaging as above i.e. use EVM to average = [1 3];

rmsEVMSym = 20*log10(rmsEVM/100);
maxEVMSym = 20*log10(maxEVM/100);

end

function out = getHEModulationType(mcs)
    switch mcs
        case 0
            out = "BPSK";
        case 1
            out = "QPSK";
        case 2
            out = "QPSK";
        case 3
            out = "16QAM";
        case 4
            out = "16QAM";
        case 5
            out = "64QAM";
        case 6
            out = "64QAM";
        case 7
            out = "64QAM";
        case 8
            out = "256QAM";
        case 9
            out = "256QAM";
        case 10
            out = "1024QAM";   
        otherwise
            out = "1024QAM";
   end
end

function T = displayLSIGContents(processPkt)
    title = ["Property","Value"];  
    column = ["L-SIG Length","L-SIG Rate"];
    lsigInfo = processPkt.LSIG.Info;

    column = pad([title(1) column])';
    value = [string(lsigInfo.Length),string(lsigRate(lsigInfo.MCS))];
    value = pad([title(2) value])';
    titlePad = strjust([column(1) value(1)],'center');
    column = strjust(column(2:end),'left');
    value = strjust(value(2:end),'left');
    sep = arrayfun(@(x)string(repmat('_',1,strlength(x))),titlePad);
    fprintf('       <strong>L-SIG Summary</strong>\n\n');
    fprintf('   <strong>%s    %s</strong>\n',titlePad);
    fprintf('   %s    %s\n\n',sep);
    fprintf('    %s    %s\n',[column value]');
    T = table(column(2:end)',value(2:end)');
    T.Properties.VariableNames{1} = 'Property';
    T.Properties.VariableNames{2} = 'Value';
end

function out = lsigRate(mcs)
    switch mcs
        case 0
           rate = [1 1 0 1];
        case 1
           rate = [1 1 1 1];
        case 2
           rate = [0 1 0 1];
        case 3
           rate = [0 1 1 1];
        case 4
            rate = [1 0 0 1];
        case 5
            rate = [1 0 1 1];
        case 6
            rate = [0 0 0 1];
        otherwise
            rate = [0 0 1 1];  
    end
    out = ['0x' dec2hex(bi2de(rate))];
end

function out = n2str(in)
    inLen = numel(in);
    for i=1:inLen
        if sign(in(i))==-1
            out(i,1:7) = pad(num2str(in(i),'%10.2f'),7); %#ok<*AGROW>
        else
            out(i,1:7) = pad([' ' num2str(in(i),'%10.2f')],7);
        end
    end
end

function out = getNonHTMod(mcs)
    if mcs==0
        out = 'BPSK ';
    elseif mcs==1
        out = 'BPSK ';
    elseif mcs==2
        out = 'QPSK ';
    elseif mcs==3
        out = 'QPSK ';
    elseif mcs==4
        out =  '16QAM';
    elseif mcs==5
        out =  '16QAM';
    elseif mcs==6
        out =  '64QAM';
    else %mcs==7
        out =  '64QAM';
    end
end

function out = powerdBm(in)
    out = round(10*log10(in)+30,2);
end

function checkWaveformProcessed(obj)
    if ~obj.isProcessed
        error('No waveform processed.');
    end
end

function validatePktNum(in,pktNum)
%ValidatePktNum Validate packet number
    if pktNum>numel(in)
        error('Invalid packet selection. The selected packet should be between 1 and %d',numel(in));
    end
end