from datetime import datetime
from pathlib import Path

import numpy as np

import time
import os
import re
import sys
import json
import configparser
import asyncio
import argparse
import subprocess

shutdown_event = asyncio.Event()

# NOTES: source files for each metric
#
# - srsdu logs:   'ta', 'ta_cmd'
#                 'UE-Create'
#                 'UE-Context-Removed'
#
# - srscu logs:   'InitialULRRCMessageTransfer'
#                 'SDAP-DL', 'SDAP-UL'
#                 'RSRP'
#                 'RRC-setup', 'RRC-release'
#
# - open5gs logs: 'UE-conn'

# NOTES: regular expressions syntax
# - (a|b|c) = OR = a or b or c
# - [abc]   = Character class = Any character from a, b, c
# - [a-d]   = Character class = Any character from a, b, c, d
# - [a-d0-3]   = Character class = Any character from a, b, c, d, 0, 1, 2, 3
# - \d      = Any digit
# - \s      = Space character
# - {n}     = The preceding item is matched exactly n times.
# - ?       = 0 or 1 repetitions of the preceding regular expression
# - *       = 0 or more repetitions of the preceding regular expression
# - +       = 1 or more repetitions of the preceding regular expression

# 3GPP TS 38.213:
# - NTA = TA·16·64/2μ (TA=0..3846)
# - NTA_new = NTA_old + (TA - 31) · 16·64/2μ (TA=0..63)

# 3GPP TS 38.211:
# - Tc = 1/(Δfmax·Nf) = 508.63 ps, Δfmax=480·10³, Nf=4096
# - TTA = (NTA + NTA_offset) · Tc

# NOTES:
# - TC-RNTI are unique at the cell level (PHY/MAC). In a CU or DU there can be more than one
#   UE with the same TC-RNTI. UEs are identified at the CU and DU levels using F1AP identifiers:
#   · gNB-DU-UE-F1AP-ID (Assigned by DU) = DU-local identification of a UE within a DU
#   · gNB-CU-UE-F1AP-ID (Assigned by CU) = CU-local identification of a UE within a CU

# =============================================================================
# Functions
# =============================================================================

def do_log(par_str):

    dt_log = time.strftime("%d/%m/%Y %H:%M:%S", time.localtime())

    prefix = dt_log + " :: "

    print(prefix + par_str)

    log_file.write(prefix + par_str + '\n')
    log_file.flush()

def run_bash(par_command):
    result = subprocess.run(
        par_command,
        shell=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True
    )
    return result.stdout.strip()

# Provides a list with the positions of ISO 8061 date times substrings present in a given string
# - ISO 8601 datetime format: YYYY-MM-DDTHH:MM:SS.microseconds
# - Regex pattern for ISO 8601: d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}

def find_dt_pos(text):

    pattern = r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}'

    matches = re.finditer(pattern, text)
    pos = [(match.start(), match.end()) for match in matches]

    return pos

# fd = find direcion ('l', 'r')
# vt = value type (
def get_val(par, st, reg, ini=None, end=None, fd='l', vt='str'):

    if not ini and not end:
        s = st[ini:]
    elif not end:
        s = st[ini:]
    else:
        s = st[ini:end]

    if fd == 'r':
        pos_par = s.rfind(par)
    else:
        pos_par = s.find(par)

    if pos_par == -1:
        return -1

    pos_par += len(par)

    match_par = re.match(reg, s[pos_par:].strip())

    if match_par:
        val = match_par.string[match_par.start():match_par.end()]
        if vt == 'int':
            val = int(val)
        return val
    else:
        return -2

# count files naming scheme:
# - count_cu.txt
# - count_core.txt
# - count_abs_du_3_cells_301.txt     ← gnb_du_id: 3 pci: 301 = NUC + B210 (1 cell)
# - count_tbs_du_8_cells_801_802.txt ← gnb_du_id: 8 pci: 801 802 = Deneb-6 + N310 (2 cells)
# - count_tbs_du_5_cells_501.txt     ← gnb_du_id: 5 pci: 501 = Asus Gaming + B210 (1 cell)

async def start_log_parser():

    global log_metric
    global dic_m_ta, dic_m_rrc_conn, dic_m_ue_conn, dic_m_sdap_dl, dic_m_rsrp_serv, dic_m_rsrp_delta
    global dic_last_ta, dic_sdap_ue_data, dic_ue_ngap_id_pci
    global dt_iso

    rrc_conn_delta = {'RRC-setup': 1, 'RRC-release': -1}

    # Each metric defines a list of strings that must be present in a single line of the log file
    # in order to trigger the processing of that metric

    metric_keyw = {'UE-Create': [' ue=', '- UE creation:'],
                   'UE-Context-Removed': [' ue=', ' [DU-F1   ]', 'F1 UE context removed'],
                   'ta': [' ta=', 'RAR PDSCH'],
                   'ta_cmd': ['ta_cmd=', 'DL PDU:'],
                   
                   'InitialULRRCMessageTransfer': ['[I] ue=', '[CU-UEMNG] [I]', 'rnti'],
                   'RRC-setup': ['[D] ue=', '[RRC     ]', 'Rx SRB1 DCCH UL', 'rrcSetupComplete'],
                   'RRC-release': ['[D] ue=', '[RRC     ]', 'Tx SRB1 DCCH DL', 'rrcRelease'],
                   'SDAP-DL': ['pdu_len=', '[SDAP    ] [D]', 'DL: TX PDU'],
                   'SDAP-UL': ['sdu_len=', '[SDAP    ] [D]', 'UL: RX SDU'],
                   'RSRP': ['"rsrp":'],

                   'UE-conn': ['Number of gNB-UEs is now', 'amf']
                   }

    metric_nogo = {'UE-Create': [],
                   'UE-Context-Removed': [],
                    'ta': [],
                   'ta_cmd': [],
                   
                   'InitialULRRCMessageTransfer': [],
                   'RRC-setup': [],
                   'RRC-release': [],
                   'SDAP-DL': [],
                   'SDAP-UL': [],
                   'RSRP': [': true'],

                   'UE-conn': []
                   }

    metric_desc = {'UE-Create': 'DU · proc="[DU-MNG  ] UE Create": Procedure started',
                   'UE-Context-Removed': 'DU · F1 UE context removed',
                   'ta': 'DU: TA (RAR)',
                   'ta_cmd': 'DU · TA command (MAC CE)',
                   
                   'InitialULRRCMessageTransfer': 'CU · InitialULRRCMessageTransfer',
                   'RRC-setup': 'CU · RRC connection establishment',
                   'RRC-release': 'CU ·  RRC connection release',
                   'SDAP-DL': 'CU · SDAP Data DL',
                   'SDAP-UL': 'CU · SDAP Data UL',
                   'RSRP': 'CU · RSRP serving cell',

                   'UE-conn': '5GC · UE connections added or removed at the AMF'
                   }

    metric_context_size = {'UE-Create': 1,
                           'UE-Context-Removed': 1,
                           'ta': 1,
                           'ta_cmd': 1,
                           
                           'InitialULRRCMessageTransfer': 1,
                           'RRC-setup': 1,
                           'RRC-release': 1,
                           'SDAP-DL': 1,
                           'SDAP-UL': 1,
                           'RSRP': 1,

                           'UE-conn': 2
                           }

    # ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
    # Samples from srsdu log files
    # ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■

    # ———————————————————————————————————————————————
    # UE-Create (' ue=', '- UE creation:')
    # - ue=
    # - rnti=
    # - pci=
    # ———————————————————————————————————————————————

    # ———————————————————————————————————————————————
    # UE-Context-Removed (' ue=', ' [DU-F1   ]', 'F1 UE context removed')
    # - ue=
    # - c-rnti=
    # ———————————————————————————————————————————————

    # ---------
    # Excerpt 1:
    # ---------

    # 2025-12-02T16:32:45.276224 [PHY     ] [D] [   577.4] PRACH: rsi=64 rssi=+6.9dB detected_preambles=[{idx=43 ta=5.47us detection_metric=4.0}] t=511.8us
    # ···
    # 2025-12-02T16:32:45.286691 [DU-MNG  ] [I] ue=0 rnti=0x4601 proc="UE Create": Procedure started....
    # 2025-12-02T16:32:45.286846 [DU-F1   ] [I] ue=0 c-rnti=0x4601 du_ue=0: F1 UE context created successfully.
    # ···
    # 2025-12-02T16:32:45.287545 [MAC     ] [I] [   579.0] ue=0 crnti=0x4601 proc="MAC UE Creation": finished successfully
    # 2025-12-02T16:32:45.287549 [DU-MNG  ] [I] ue=0 rnti=0x4601 proc="UE Create": Procedure finished successfully.
    # ···
    # 2025-12-02T16:32:45.287554 [SCHED   ] [D] [   579.0] Processed slot events pci=501:
    # - UE creation: ue=0 rnti=0x4601 pci=501
    # 2025-12-02T16:32:45.287555 [RLC     ] [I] du=5 ue=0 SRB0 UL: RX SDU. sdu_len=6
    # 2025-12-02T16:32:45.287568 [DU-F1   ] [I] Tx PDU du=5 tid=1 ue=0 du_ue=0: InitialULRRCMessageTransfer

    # ---------
    # Excerpt 2:
    # ---------

    # 2025-12-02T16:35:35.836011 [SCHED   ] [D] [   225.9] Processed slot events pci=501:
    # - PRACH: slot=225.4 pci=501 preamble=43 ra-rnti=0x39 temp_crnti=0x4602 ta_cmd=10
    # ···
    # 2025-12-02T16:35:35.846101 [DU-MNG  ] [I] ue=1 rnti=0x4602 proc="UE Create": Procedure started....
    # 2025-12-02T16:35:35.846196 [DU-F1   ] [I] ue=1 c-rnti=0x4602 du_ue=1: F1 UE context created successfully.
    # ···
    # 2025-12-02T16:35:35.847018 [MAC     ] [I] [   227.0] ue=1 crnti=0x4602 proc="MAC UE Creation": finished successfully
    # 2025-12-02T16:35:35.847028 [DU-MNG  ] [I] ue=1 rnti=0x4602 proc="UE Create": Procedure finished successfully.
    # ···
    # 2025-12-02T16:35:35.847039 [SCHED   ] [D] [   227.0] Processed slot events pci=501:
    # - CSI: ue=0 rnti=0x4601: cqi=15 ri=1
    # - UE creation: ue=1 rnti=0x4602 pci=501
    # 2025-12-02T16:35:35.847047 [DU-F1   ] [I] Tx PDU du=5 tid=2 ue=1 du_ue=1: InitialULRRCMessageTransfer
    # ···
    # 2025-12-02T16:35:50.944699 [DU-F1   ] [I] Rx PDU du=5 ue=0 cu_ue=0 du_ue=0: UEContextReleaseCommand
    # ···
    # 2025-12-02T16:35:50.970271 [DU-F1   ] [I] ue=0 c-rnti=0x4601 du_ue=0 cu_ue=0: F1 UE context removed.
    # 2025-12-02T16:35:50.970274 [MAC     ] [D] [   715.3] ue=0 crnti=0x4601 proc="UE Delete Request": started...

    # ---------
    # Excerpt 3:
    # ---------

    # 2025-12-02T16:35:51.995896 [SCHED   ] [D] [   817.9] Processed slot events pci=501:
    # - PRACH: slot=817.4 pci=501 preamble=21 ra-rnti=0x39 temp_crnti=0x4603 ta_cmd=10
    # ···
    # 2025-12-02T16:35:52.006084 [DU-MNG  ] [I] ue=0 rnti=0x4603 proc="UE Create": Procedure started....
    # 2025-12-02T16:35:52.006205 [DU-F1   ] [I] ue=0 c-rnti=0x4603 du_ue=2: F1 UE context created successfully.
    # ···
    # 2025-12-02T16:35:52.006963 [MAC     ] [I] [   819.0] ue=0 crnti=0x4603 proc="MAC UE Creation": finished successfully
    # 2025-12-02T16:35:52.006965 [SCHED   ] [D] [   819.0] Processed slot events pci=501:
    # - UE creation: ue=0 rnti=0x4603 pci=501
    # - CSI: ue=1 rnti=0x4602: cqi=15 ri=1
    # 2025-12-02T16:35:52.006968 [DU-MNG  ] [I] ue=0 rnti=0x4603 proc="UE Create": Procedure finished successfully.
    # ···
    # 2025-12-02T16:35:52.006987 [DU-F1   ] [I] Tx PDU du=5 tid=3 ue=0 du_ue=2: InitialULRRCMessageTransfer

    # ---------
    # Excerpt 4:
    # ---------

    # 2025-12-02T16:36:43.917765 [SCHED   ] [D] [   890.1] Slot decisions pci=501 t=10us (0 PDSCHs, 0 PUSCHs, 0 attempted PDCCHs, 0 attempted UCIs):
    # - PUCCH: c-rnti=0x4602 format=1 prb=[4..5) symb=[0..14) cs=0 occ=0 uci: harq_bits=0 sr=1
    # 2025-12-02T16:36:43.918544 [DU-F1   ] [I] Rx PDU du=5 cu_ue=3: UEContextSetupRequest
    # ···
    # 2025-12-02T16:36:43.918552 [DU-MNG  ] [I] ue=2 proc="UE Create": Procedure started....
    # 2025-12-02T16:36:43.918641 [DU-F1   ] [I] ue=2 c-rnti=0x0 du_ue=3: F1 UE context created successfully.
    # ···
    # 2025-12-02T16:36:43.919726 [SCHED   ] [D] [   890.3] Processed slot events pci=501:
    # - UE creation: ue=2 rnti=0x4604 pci=501
    # 2025-12-02T16:36:43.919728 [SCHED   ] [D] [   890.3] Slot decisions pci=501 t=16us (0 PDSCHs, 0 PUSCHs, 0 attempted PDCCHs, 0 attempted UCIs):
    # - PUCCH: c-rnti=0x4604 format=1 prb=[4..5) symb=[0..14) cs=0 occ=0 uci: harq_bits=0 sr=1
    # 2025-12-02T16:36:43.919743 [MAC     ] [I] [   890.3] ue=2 crnti=0x4604 proc="MAC UE Creation": finished successfully
    # 2025-12-02T16:36:43.919747 [DU-MNG  ] [I] ue=2 proc="UE Create": Procedure finished successfully.

    # ---------
    # Excerpt 5:
    # ---------

    # 2025-12-02T16:36:44.132354 [DU-F1   ] [I] Rx PDU du=5 cu_ue=4: UEContextSetupRequest
    # ···
    # 2025-12-02T16:36:44.132363 [DU-MNG  ] [I] ue=3 proc="UE Create": Procedure started....
    # 2025-12-02T16:36:44.132457 [DU-F1   ] [I] ue=3 c-rnti=0x0 du_ue=4: F1 UE context created successfully.
    # ···
    # 2025-12-02T16:36:44.133731 [SCHED   ] [D] [   911.7] Processed slot events pci=501:
    # - UE creation: ue=3 rnti=0x4605 pci=501
    # 2025-12-02T16:36:44.133740 [MAC     ] [I] [   911.7] ue=3 crnti=0x4605 proc="MAC UE Creation": finished successfully
    # 2025-12-02T16:36:44.133744 [DU-MNG  ] [I] ue=3 proc="UE Create": Procedure finished successfully.
    # ···
    # 2025-12-02T16:36:50.367617 [DU-F1   ] [I] Rx PDU du=5 ue=3 cu_ue=4 du_ue=4: UEContextReleaseCommand
    # ···
    # 2025-12-02T16:36:50.486774 [DU-F1   ] [I] ue=3 c-rnti=0x4605 du_ue=4 cu_ue=4: F1 UE context removed.
    # 2025-12-02T16:36:50.486777 [MAC     ] [D] [   523.0] ue=3 crnti=0x4605 proc="UE Delete Request": started...

    # ---------
    # Excerpt 6:
    # ---------

    # 2025-12-02T16:37:08.795701 [SCHED   ] [D] [   305.9] Processed slot events pci=501:
    # - PRACH: slot=305.4 pci=501 preamble=43 ra-rnti=0x39 temp_crnti=0x4608 ta_cmd=10
    # - RLC Buffer State: ue=2 lcid=4 pending_bytes=0
    # ···
    # 2025-12-02T16:37:08.805741 [DU-MNG  ] [I] ue=3 rnti=0x4608 proc="UE Create": Procedure started....
    # 2025-12-02T16:37:08.805826 [DU-F1   ] [I] ue=3 c-rnti=0x4608 du_ue=5: F1 UE context created successfully.
    # ···
    # 2025-12-02T16:37:08.806692 [MAC     ] [I] [   307.0] ue=3 crnti=0x4608 proc="MAC UE Creation": finished successfully
    # 2025-12-02T16:37:08.806696 [DU-MNG  ] [I] ue=3 rnti=0x4608 proc="UE Create": Procedure finished successfully.
    # 2025-12-02T16:37:08.806698 [MAC     ] [D] [   307.0] UL subPDU rnti=0x4608 ue=3 lcid=0 CE: Forwarding UL SDU of 6 bytes
    # 2025-12-02T16:37:08.806701 [RLC     ] [I] du=5 ue=3 SRB0 UL: RX SDU. sdu_len=6
    # 2025-12-02T16:37:08.806712 [DU-F1   ] [I] Tx PDU du=5 tid=4 ue=3 du_ue=5: InitialULRRCMessageTransfer
    # ···
    # 2025-12-02T16:37:08.806716 [SCHED   ] [D] [   307.0] Processed slot events pci=501:
    # - UE creation: ue=3 rnti=0x4608 pci=501
    # ···
    # 2025-12-02T16:37:13.582213 [DU-F1   ] [I] Rx PDU du=5 ue=2 cu_ue=3 du_ue=3: UEContextReleaseCommand
    # ···
    # 2025-12-02T16:37:13.612926 [DU-F1   ] [I] ue=2 c-rnti=0x4604 du_ue=3 cu_ue=3: F1 UE context removed.
    # 2025-12-02T16:37:13.612928 [MAC     ] [D] [   787.6] ue=2 crnti=0x4604 proc="UE Delete Request": started...
    # ···
    # 2025-12-02T16:37:24.216117 [DU-F1   ] [I] Rx PDU du=5 ue=1 cu_ue=1 du_ue=1: UEContextReleaseCommand
    # ···
    # 2025-12-02T16:37:24.250886 [DU-F1   ] [I] ue=1 c-rnti=0x4602 du_ue=1 cu_ue=1: F1 UE context removed.
    # 2025-12-02T16:37:24.250889 [MAC     ] [D] [   827.4] ue=1 crnti=0x4602 proc="UE Delete Request": started...
    # ···
    # 2025-12-02T16:38:21.301388 [DU-F1   ] [I] Rx PDU du=5 ue=3 cu_ue=5 du_ue=5: UEContextReleaseCommand
    # ···
    # 2025-12-02T16:38:21.334657 [DU-F1   ] [I] ue=3 c-rnti=0x4608 du_ue=5 cu_ue=5: F1 UE context removed.
    # 2025-12-02T16:38:21.334661 [MAC     ] [D] [   391.8] ue=3 crnti=0x4608 proc="UE Delete Request": started...
    # ···
    # 2025-12-02T16:39:11.702836 [DU-F1   ] [I] Rx PDU du=5 ue=0 cu_ue=2 du_ue=2: UEContextReleaseCommand
    # ···
    # 2025-12-02T16:39:11.711524 [DU-F1   ] [I] ue=0 c-rnti=0x4603 du_ue=2 cu_ue=2: F1 UE context removed.
    # 2025-12-02T16:39:11.711527 [MAC     ] [D] [   309.5] ue=0 crnti=0x4603 proc="UE Delete Request": started...

    # ———————————————————————————————————————————————
    # ta (' ta=', 'RAR PDSCH')
    # - ta=
    # - tc-rnti=
    # - pci=
    # ———————————————————————————————————————————————

    # 2025-12-02T16:37:08.795704 [SCHED   ] [D] [   305.9] Slot decisions pci=501 t=23us (1 PDSCH, 0 PUSCHs, 0 attempted PDCCHs, 0 attempted UCIs):
    # - DL PDCCH: rnti=0x39 type=ra-rnti cs_id=0 ss_id=1 format=1_0 cce=0 al=4
    # - RAR PDSCH: ra-rnti=0x39 rb=[0..3) symb=[2..14) tbs=9 mcs=0 rv=0 grants (1): tc-rnti=0x4608: rapid=43 ta=10 time_res=0

    # ———————————————————————————————————————————————
    # ta_cmd ('ta_cmd=', 'DL PDU:')
    # - ta_cmd=
    # - tc-rnti=
    # - pci=
    # ———————————————————————————————————————————————

    # 2025-07-23T17:36:09.166176 [SCHED   ] [D] [   181.5] Slot decisions pci=103 t=63us (1 PDSCH, 0 PUSCHs, 0 attempted PDCCHs, 0 attempted UCIs):
    # - DL PDCCH: rnti=0x4601 type=c-rnti cs_id=1 ss_id=2 format=1_0 cce=0 al=2 dci: h_id=2 ndi=false rv=0 mcs=28 res_ind=0 tpc=1
    # - UE PDSCH: ue=0 c-rnti=0x4601 h_id=2 rb=[0..2) symb=[2..14) tbs=149 mcs=28 rv=0 nrtx=0 k1=4 dl_bo=0 olla=0 grants: lcid=61: size=1, lcid=1: size=5
    # 2025-07-23T17:36:09.166183 [FAPI    ] [D] [   181.5] Sector#0: Omnidirectional PDSCH precoding matrix, nof_layers=1
    # 2025-07-23T17:36:09.166186 [FAPI    ] [D] [   181.5] Sector#0: DL_TTI.request slot=181.5, is_last_message_in_slot=false
    #   - PDCCH bwp=0:106 symb=0:2 nof_dcis=1
    #   - PDSCH rnti=0x4601 bwp=0:106 symb=2:12 CW: tbs=149 mod=6 rv_idx=0
    # 2025-07-23T17:36:09.166202 [RLC     ] [D] du=3 ue=0 SRB1 DL: MAC opportunity. grant_len=5 tx_window_size=0
    # 2025-07-23T17:36:09.166203 [RLC     ] [I] du=3 ue=0 SRB1 DL: TX status PDU. pdu_len=3 grant_len=5
    # 2025-07-23T17:36:09.166204 [RLC     ] [D] du=3 ue=0 SRB1 DL: TX entity state. tx_next_ack=5 tx_next=5 poll_sn=4 pdu_without_poll=0 byte_without_poll=0 sn_under_segmentation=none
    # 2025-07-23T17:36:09.166207 [MAC     ] [I] [   181.5] DL PDU: ue=0 rnti=0x4601 size=149: TA_CMD: tag_id=0, ta_cmd=30, SDU: lcid=1 nof_sdus=1 total_size=5

    # ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
    # Samples from srscu log:
    # ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■

    # ———————————————————————————————————————————————
    # InitialULRRCMessageTransfer ('[I] ue=', '[CU-UEMNG] [I]', 'rnti')
    # - ue=
    # - pci=
    # - rnti=
    # ———————————————————————————————————————————————

    # 2025-12-02T16:32:50.033829 [CU-CP-F1] [I] Rx PDU du=5 tid=1 du_ue=0: InitialULRRCMessageTransfer
    # 2025-12-02T16:32:50.033896 [CU-UEMNG] [I] ue=0 du_index=0 plmn=21405 gnb_du_id=5 pci=501 rnti=0x4601 pcell_index=0: Created new CU-CP UE

    # ———————————————————————————————————————————————
    # RRC-setup ('[D] ue=', '[RRC     ]', 'Rx SRB1 DCCH UL', 'rrcSetupComplete')
    # - ue=
    # - c-rnti=
    # ———————————————————————————————————————————————

    # 2025-12-02T16:35:56.761755 [RRC     ] [D] ue=4 c-rnti=0x4603: Rx SRB1 DCCH UL rrcSetupComplete (79 B)
    # 2025-12-02T16:35:56.761757 [RRC     ] [D] ue=4 c-rnti=0x4603: Containerized rrcSetupComplete: [

    # ———————————————————————————————————————————————
    # RRC-release ('[D] ue=', '[RRC     ]', 'Tx SRB1 DCCH DL', 'rrcRelease')
    # - ue=
    # - c-rnti=
    # ———————————————————————————————————————————————

    # 2025-12-02T16:36:24.853724 [RRC     ] [D] ue=1 c-rnti=0x4601: Tx SRB1 DCCH DL rrcRelease (8 B)
    # 2025-12-02T16:36:24.853726 [RRC     ] [D] ue=1 c-rnti=0x4601: Containerized rrcRelease: [

    # ———————————————————————————————————————————————
    # SDAP-DL ('pdu_len=', '[SDAP    ] [D]', 'DL: TX PDU')
    # - ue=
    # - pdu_len=
    # ———————————————————————————————————————————————

    # 2025-12-02T16:35:42.725530 [SDAP    ] [D] ue=3 psi=2 QFI=1 DRB2 DL: TX PDU. QFI=1 pdu_len=126
    # ···
    # 2025-12-02T16:35:42.744907 [SDAP    ] [D] ue=3 psi=2 QFI=1 DRB2 UL: RX SDU. QFI=1 sdu_len=52

    # ———————————————————————————————————————————————
    # RSRP ('"rsrp":')
    # - c-rnti=
    # - "physCellId": (pci)
    # - "rsrp":
    # ———————————————————————————————————————————————

    # 2025-12-02T16:35:30.503344 [RRC     ] [D] ue=2 c-rnti=0x4602: Containerized measurementReport: [
    #   {
    #     "UL-DCCH-Message": {
    #       "message": {
    #         "c1": {
    #           "measurementReport": {
    #             "criticalExtensions": {
    #               "measurementReport": {
    #                 "measResults": {
    #                   "measId": 1,
    #                   "measResultServingMOList": [
    #                     {
    #                       "servCellId": 0,
    #                       "measResultServingCell": {
    #                         "physCellId": 301,
    #                         "measResult": {
    #                           "cellResults": {
    #                             "resultsSSB-Cell": {
    #                               "rsrp": 71,
    #                               "rsrq": 65,
    #                               "sinr": 82
    #                             }
    #                           },
    #                           "rsIndexResults": {
    #                             "resultsSSB-Indexes": [
    #                               {
    #                                 "ssb-Index": 0,
    #                                 "ssb-Results": {
    #                                   "rsrp": 71,
    #                                   "rsrq": 65,
    #                                   "sinr": 82
    #                                 }
    #                               }
    #                             ]
    #                           }
    #                         }
    #                       }
    #                     }
    #                   ],
    #                   "measResultNeighCells": {
    #                     "measResultListNR": [
    #                       {
    #                         "physCellId": 501,
    #                         "measResult": {
    #                           "cellResults": {
    #                             "resultsSSB-Cell": {
    #                               "rsrp": 61,
    #                               "rsrq": 62,
    #                               "sinr": 56
    #                             }
    #                           },
    #                           "rsIndexResults": {
    #                             "resultsSSB-Indexes": [
    #                               {
    #                                 "ssb-Index": 0,
    #                                 "ssb-Results": {
    #                                   "rsrp": 61,
    #                                   "rsrq": 62,
    #                                   "sinr": 56
    #                                 }
    #                               }
    #                             ]

    # ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
    # Samples from open5gs log:
    # ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■

    # ···············································
    # UE-conn ('Number of gNB-UEs is now', 'amf')
    # ···············································

    # 2025-07-23T19:36:44.421752+02:00 optiplex-7010 open5gs-amfd[1451801]: 07/23 19:36:44.421: [amf] INFO: InitialUEMessage (../src/amf/ngap-handler.c:437)
    # 2025-07-23T19:36:44.421752+02:00 optiplex-7010 open5gs-amfd[1451801]: 07/23 19:36:44.421: [amf] INFO: [Added] Number of gNB-UEs is now 3 (../src/amf/context.c:2785)
    # 2025-07-23T19:36:44.422292+02:00 optiplex-7010 open5gs-amfd[1451801]: 07/23 19:36:44.421: [amf] INFO:     RAN_UE_NGAP_ID[2] AMF_UE_NGAP_ID[74] TAC[7] CellID[0x19b3] (../src/amf/ngap-handler.c:598)

    # 2025-11-25T11:14:34.963886+00:00 deneb-4 open5gs-amfd[229389]: 11/25 12:14:34.963: [amf] INFO: UE Context Release [Action:2] (../src/amf/ngap-handler.c:1733)
    # 2025-11-25T11:14:34.963886+00:00 deneb-4 open5gs-amfd[229389]: 11/25 12:14:34.963: [amf] INFO:     RAN_UE_NGAP_ID[0] AMF_UE_NGAP_ID[6] (../src/amf/ngap-handler.c:1734)
    # 2025-11-25T11:14:34.963886+00:00 deneb-4 open5gs-amfd[229389]: 11/25 12:14:34.963: [amf] INFO:     SUCI[suci-0-214-05-0-0-0-0000000009] (../src/amf/ngap-handler.c:1738)
    # 2025-11-25T11:14:34.963886+00:00 deneb-4 open5gs-amfd[229389]: 11/25 12:14:34.963: [amf] INFO: [Removed] Number of gNB-UEs is now 3 (../src/amf/context.c:2796)

    # Metric collecting dictionaries

    # - dic_m_* dictionaries comprise one temporal window. Each dictionary contains
    #   a per-cell (per-PCI) list of dictionaries containing the collected metric values

    # - When a temporal windows ends, these dictionaries are used to count the collected
    #   metric values. Then, dictionaries are cleared to start collecting metrics for
    #   the next temporal window.

    # dic_m_ta: ◀ @ srsdu log files
    #    Cell             RNTI 1                                             RNTI2
    # { PCI1: {'RNTI': ------ : {'TA': [], 'NTA': [], 'TTA': []}, 'RNTI': ------ : {'TA': [], 'NTA': [], 'TTA': []} ...
    #   PCI2: {'RNTI': ------ : {'TA': [], 'NTA': [], 'TTA': []}, 'RNTI': ------ : {'TA': [], 'NTA': [], 'TTA': []} ...
    # ... }
    
    # NOTE: 'TA':  [] = List of TA values for that UE (RNTI) in that cell (PCI)
    #       'NTA': [] = List of NTA values for that UE (RNTI) in that cell (PCI)
    #       'TTA': [] = List of TTA values for that UE (RNTI) in that cell (PCI)

    # dic_m_rrc_conn: ◀ @ srsdu log files
    #    Cell             RNTI 1               Value             RNTI 2               Value    ...
    # { PCI1: [{'RNTI': ------ , 'RRC-conn': ----- }, {'RNTI': ------ , 'RRC-conn': ----- }, ... ],
    #   PCI2: [{'RNTI': ------ , 'RRC-conn': ----- }, {'RNTI': ------ , 'RRC-conn': ----- }, ... ],
    # ... }
    
    # NOTE: Value = 1 (RRC connection establishment) / -1 ((RRC connection release)

    # dic_m_ue_conn: ◀ @ open5gs log files
    #    Cell                  RAN_UE_NGAP_ID 1         Value                  RAN_UE_NGAP_ID 2         Value    ...
    # { 'PCI1': [{'RAN_UE_NGAP_ID': ------ , 'UE-conn': ----- }, {'RAN_UE_NGAP_ID': ------ , 'UE-conn': ----- }, ... ],
    #   'PCI2': [{'RAN_UE_NGAP_ID': ------ , 'UE-conn': ----- }, {'RAN_UE_NGAP_ID': ------ , 'UE-conn': ----- }, ... ],
    # ... }

    # NOTE: Value = 1 (UE added) / -1 (UE removed)

    # dic_m_sdap_dl: ◀ @ srscu log files
    #    Cell             RNTI 1              Value             RNTI 2              Value    ...
    # { 'PCI1': [{'RNTI': ------ , 'Data-DL': ----- }, {'RNTI': ------ , 'Data-DL': ----- }, ... ],
    #   'PCI2': [{'RNTI': ------ , 'Data-DL': ----- }, {'RNTI': ------ , 'Data-DL': ----- }, ... ],
    # ... }

    # dic_rrsp_serv: ◀ @ srscu log files
    #    Cell             RNTI 1           Value             RNTI 2           Value    ...
    # { 'PCI1': [{'RNTI': ------ , 'RSRP': ----- }, {'RNTI': ------ , 'RSRP': ----- }, ... ],
    #   'PCI2': [{'RNTI': ------ , 'RSRP': ----- }, {'RNTI': ------ , 'RSRP': ----- }, ... ],
    # ... }

    # dic_rrsp_delta: ◀ @ srscu log files
    #    Cell             RNTI 1                   PCI 1                    Value                 Value                     PCI 2                    Value                 Value    ...
    # { 'PCI1': [{'RNTI': ------ , 'PCI neighbor': ----- , 'RSRP neighbor': ----- , 'RSRP delta': ----- }, {'PCI neighbor': ----- , 'RSRP neighbor': ----- , 'RSRP delta': ----- }, ... ],
    #   'PCI2': [{'RNTI': ------ , 'PCI neighbor': ----- , 'RSRP neighbor': ----- , 'RSRP delta': ----- }, {'PCI neighbor': ----- , 'RSRP neighbor': ----- , 'RSRP delta': ----- }, ... ],
    # ... }

    l_win = []  # Lines window: list of lines read from log files

    # =============================================================================
    # Main
    # =============================================================================

    do_log("Metrics:")
    do_log("- Metric keys:")
    for k in metric_keyw:
        do_log("  · " + k + ": '" + "', '".join(metric_keyw[k]) + "'")
    do_log("- Metric no-gos:")
    for k in metric_nogo:
        do_log("  · " + k + ": '" + "', '".join(metric_nogo[k]) + "'")
    do_log("- Metric descriptors:")
    for k in metric_desc:
        do_log("  · " + k + ": '" + metric_desc[k] + "'")

    do_log("")

    # Wait to receive the 'enable' command from the controller to begin parsing the log file

    do_log("Waiting for 'enable' command from controller to begin parsing the log file '" + ini_input_file + "' ...")

    while True:
        if proc_enable:
            break
        await asyncio.sleep(0.1)

    do_log("... 'enable' command received from the controller")
    do_log('')

    # Start a new temporal window

    do_log('======================================================')
    do_log("Temporal window started ...")
    do_log('------------------------------------------------------')
    do_log('')

    # Wait for the presence of the log file and then open it

    do_log("Opening log file '" + ini_input_file + "' ...")

    while True:
        if os.path.exists(inp_file_name):
            f = open(inp_file_name)
            break
        await asyncio.sleep(0.1)

    do_log('... file opened')
    do_log("")

    # ------------------------
    # Process lines read from log file
    # ------------------------

    proc_state = st_waiting_metric

    metric_context_read = 0
    metric_name = ''
    metric_str = ''

    val_pci_serv = None
    val_rsrp_serv = None
    val_rnti = None

    params_str = ''

    list_pci_neig = []
    list_neig_rsrp = []
    list_delta_rsrp = []

    # Wait for the presence of new lines in the log file

    f.seek(0, os.SEEK_END)  # Go to end of file

    while not shutdown_event.is_set():

        line = f.readline()

        if not line:
            await asyncio.sleep(0.1)
            continue

        l_win.append(line)

        if proc_state == st_waiting_meas_res_neigh:
            if line.find("measResultNeighCells") != -1:
                # Set state to wait for neighbor RSRP values
                l_win = []  # Clear the lines window
                proc_state = st_waiting_neigh_rsrp_values

        if proc_state == st_waiting_neigh_rsrp_values:

            # This state is reached after reading the RSRP of a serving cell and reading
            # a 'measResultNeighCells' string:
            # - In this state it is expected one or more RSRP values for neighbor cells.
            # - When a date time is found in this state, reading of neighbor cells RSRP values
            #   can be considered finished.

            # Check if there is a RSRP value in the current line
            if line.find(metric_keyw['RSRP'][0]) != -1:

                # ------------------------
                # - Limit the lines window to 150 lines (lines sub-window)
                # - Join the sub-window into a single string
                # ------------------------

                l_sub_win = l_win[-150:]
                l_sub_win_str = ''.join(l_sub_win)

                # Read RSRP and PCI of neihgbor cell

                # Locate the last occurrence of:
                # - measResultServingCell ▶ physCellId, rsrp
                # - measResultNeighCells  ▶ physCellId, rsrp

                # Add the found value to the RSRP neighbor lists:
                # - list_pci_neig
                # - list_neig_rsrp
                # - list_delta_rsrp

                pos_meas_serv = l_sub_win_str.rfind('measResultServingCell')
                pos_index_res = l_sub_win_str.rfind('rsIndexResults')

                # rsIndexResults found and ignored as a RSRP metric
                if pos_index_res != -1:
                    l_win = []  # Clear the lines window
                    continue  # Continue with the next line of the log file

                # measResultServingCell found when waiting for neighbor cells RSRP values
                if pos_meas_serv != -1:
                    proc_state = st_waiting_metric
                    continue  # Continue with the next line of the log file

                # Get the neighbor cell PCI value present in the lines sub-window
                pos_pci_neig = l_sub_win_str.rfind('"physCellId":')

                # Neighbor cell PCI not present for metric RSR
                if pos_pci_neig == -1:
                    do_log("ERROR: Neighbor cell PCI not present for metric 'RSRP'")
                    l_win = []  # Clear the lines window
                    proc_state = st_waiting_metric
                    continue  # Continue with the next line of the log file

                s = l_sub_win_str[pos_pci_neig + len('"physCellId":'):].strip()
                match = re.match("\\d+", s)

                if match:
                    val_pci_neig = int(s[match.start():match.end()])
                    params_str += ' | Neighbor cell PCI: ' + str(val_pci_neig)
                else:
                    do_log("ERROR: Invalid PCI value of neighbor cell for metric '" + metric_desc['RSRP'] + "'")
                    l_win = []  # Clear the lines window
                    proc_state = st_waiting_metric
                    continue  # Continue with the next line of the log file

                pos_rsrp_neig = l_sub_win_str.rfind(metric_keyw['RSRP'][0])

                s = l_sub_win_str[pos_rsrp_neig + len(metric_keyw['RSRP'][0]):].strip()
                match = re.match("\\d+", s)
                val_rsrp_neig = s[match.start():match.end()]

                if val_pci_serv and val_pci_neig:
                    list_pci_neig.append(val_pci_neig)
                    list_neig_rsrp.append(int(val_rsrp_neig))
                    list_delta_rsrp.append(int(val_rsrp_serv) - int(val_rsrp_neig))

                    do_log("Metric: 'RSRP delta' | Value: " + str(int(val_rsrp_serv) - int(val_rsrp_neig)))
                    do_log("Neighbor cell PCI: " + str(val_pci_neig) + " | " +
                           "RSRP neighbor cell: " + str(val_rsrp_neig) + " | " +
                           "RSRP serving cell: " + str(val_rsrp_serv))
                else:
                    proc_state = st_waiting_metric

                l_win = []  # Clear the lines window

                continue  # Continue with the next line of the log file

            # When an ISO 8601 datetime is found in the current line finish the neighbor cells RSRP reading
            # loop and calculate the deltas RSRP betweeen the serving cell and the neighbor cells

            if find_dt_pos(line):

                # ------------------------
                # Add RSRP of the serving cell in the metric dictionary list
                # ------------------------

                dic_metric = {'RNTI': val_rnti, 'RSRP serving': int(val_rsrp_serv)}

                if val_pci_serv in dic_m_rsrp_serv:
                    dic_m_rsrp_serv[val_pci_serv].append(dic_metric)
                else:
                    dic_m_rsrp_serv[val_pci_serv] = [dic_metric]

                # ------------------------
                # Add delta RSRP of the neighbor cells in the metric dictionary list
                # ------------------------

                for i, neig_pci in enumerate(list_pci_neig):

                    dic_metric = {'RNTI': val_rnti, 'PCI neighbor': neig_pci,
                                  'RSRP serving': val_rsrp_serv, 'RSRP neighbor': list_neig_rsrp[i],
                                  'RSRP delta': list_delta_rsrp[i]}

                    if val_pci_serv in dic_m_rsrp_delta:
                        dic_m_rsrp_delta[val_pci_serv].append(dic_metric)
                    else:
                        dic_m_rsrp_delta[val_pci_serv] = [dic_metric]

                proc_state = st_waiting_metric  # Set state to metric reading

                # Clear the lines window except the last line containing the ISO 8601 datetime used
                # to finish the delta RSRP reading loop
                l_win = l_win[-1:]

                do_log("End of delta RSRP reading loop")

                continue  # Continue with the next line of the log file

        # With some metrics (e.g., UE connections added or removed at the AMF) it is necessary to read more lines after
        # the line having the metric (in order to collect all the needed information)

        if metric_context_read == 0:

            metric_name = ''
            metric_str = ''

            for m_n in metric_keyw:
                if all(s in line for s in metric_keyw[m_n]) and not any(s in line for s in metric_nogo[m_n]):
                    s = metric_keyw[m_n][0].replace('[', r'\[')  # Insert \ to escape [ in regular expressions
                    match = re.search(s + r"\s*[+-]?([0-9]+[.])?[0-9]+", line)
                    if match:
                        metric_name = m_n
                        metric_str = line[match.start():match.end()]  # Metric string (name + value)
                        metric_context_read += 1
                        break

        # ------------------------------------------
        # If one of the metrics is present in the line read then process that line
        # ------------------------------------------

        if metric_name:

            # If needed read more lines to collect all needed information
            if metric_context_read < metric_context_size[metric_name]:
                metric_context_read += 1
                continue

            metric_context_read = 0

            # ------------------------
            # - Limit the lines window to 150 lines (lines sub-window)
            # - Join the sub-window into a single string
            # ------------------------

            l_sub_win = l_win[-150:]
            l_sub_win_str = ''.join(l_sub_win)

            # ------------------------
            # Find ISO 8601 date-time occurrences in the lines sub-window
            # ------------------------

            pos_dt = find_dt_pos(l_sub_win_str)

            # Datetime not found
            if not pos_dt:
                continue  # Continue with the next line of the log file

            # Select the last date-time occurrence in the lines sub-window
            if metric_name == 'UE-conn':
                pos_dt = pos_dt[-2]
            else:
                pos_dt = pos_dt[-1]

            # Extract date-time (ISO 8601)
            dt_iso = l_sub_win_str[pos_dt[0]:pos_dt[1]]

            # Lines sub-window taken from the last date-time occurrence to the end of the lines sub-window
            l_sub_win_str_fdt = l_sub_win_str[pos_dt[0]:]

            # ------------------------
            # Read metric value (numeric value after the position of the metric)
            # ------------------------

            # Position of the metric string in the 'from last date time' lines sub-window
            pos_metric_fdt = l_sub_win_str_fdt.rfind(metric_str)

            if pos_metric_fdt == -1:
                do_log("ERROR: error reading metric " + metric_desc[metric_name])
                l_win = []  # Clear the lines window
                proc_state = st_waiting_metric
                continue  # Continue with the next line of the log file

            s = l_sub_win_str_fdt[pos_metric_fdt + len(metric_keyw[metric_name][0]):].strip()
            match = re.match("[+-]?([0-9]+[.])?[0-9]+", s)

            do_log('------------------------------------------------------')

            if match:
                val_metric = s[match.start():match.end()]
                do_log("Datetime: " + dt_iso)
                do_log("Metric: '" + metric_desc[metric_name] + "' | Value: " + val_metric)
            else:
                do_log("Datetime: " + dt_iso)
                do_log("ERROR: invalid value for metric '" + metric_desc[metric_name] + "'")
                l_win = []  # Clear the lines window
                proc_state = st_waiting_metric
                continue  # Continue with the next line of the log file

            # Position of the metric in the lines sub-window
            pos_metric = pos_dt[0] + pos_metric_fdt

            # ------------------------------------------
            # UE-Create (' ue=', '- UE creation:') @ srsdu log
            # - ue=
            # - rnti=
            # - pci=
            # ------------------------------------------

            # 2025-12-02T16:35:52.006965 [SCHED   ] [D] [   819.0] Processed slot events pci=501:
            # - UE creation: ue=0 rnti=0x4603 pci=501

            # This is to get the PCI associated with each created UE identified by the pair (RNTI, ue)
            # for usage in other DU metrics

            # Note that RNTIs are unique per-cell but not per-DU. So, to differentiate UEs within a DU
            # I will use the pairs (RNTI, ue) or (RNTI, PCI) as UE identifiers.

            # Although, according to 3GPP, PCIs are allowed to be repeated within a DU or CU, in my
            # design PCIs will be unique both at the DU and CU levels. Therefore, (PCI, RNTI) is a
            # valid UE identifier for the whole 5G network under test.

            if metric_name == 'UE-Create':

                val_ue = int(val_metric)

                pos_ue_c = l_sub_win_str[:pos_metric].rfind('UE creation:') + len('UE creation:')

                # ------------------------
                # Get the RNTI value present in the lines sub-window
                # ------------------------

                val_rnti = get_val('rnti=', l_sub_win_str, "0x[0-9a-fA-F]+", ini=pos_ue_c)

                if val_rnti in [-1, -2]:
                    if val_rnti == -1:
                        do_log("ERROR: RNTI not present for metric '" + metric_desc[metric_name] + "'" + '\n')
                    else:
                        do_log("ERROR: invalid RNTI value for metric '" + metric_desc[metric_name] + "'")

                    l_win = []  # Clear the lines window
                    proc_state = st_waiting_metric
                    continue  # Continue with the next line of the log file

                # ------------------------
                # Get the PCI value present in the lines sub-window
                # ------------------------

                val_pci = get_val('pci=', l_sub_win_str, "\\d+", pos_ue_c, vt='int')

                if val_pci in [-1, -2]:
                    if val_pci == -1:
                        do_log("ERROR: PCI of serving cell not present for metric '" + metric_desc[metric_name] + "'" + '\n')
                    else:
                        do_log("ERROR: invalid PCI value of serving cell for metric '" + metric_desc[metric_name] + "'")

                    l_win = []  # Clear the lines window
                    proc_state = st_waiting_metric
                    continue  # Continue with the next line of the log file

                # Save the PCI associated with the pair (RNTI, ue)
                dic_du_pci_rnti_ue[(val_rnti, val_ue)] = val_pci

                params_str =  'RNTI: ' + val_rnti + ' | ' + 'Serving cell PCI: ' + str(val_pci) + ' | ' + 'UE: ' + str(val_ue)
                do_log(params_str)

                l_win = []  # Clear the lines window
                proc_state = st_waiting_metric  # Set state to metric readings
                log_metric = True

            # ------------------------------------------
            # UE-Context-Removed (' ue=', ' [DU-F1   ]', 'F1 UE context removed') @ srsdu log
            # - ue=
            # - c-rnti=
            # ------------------------------------------

            # 2025-12-02T16:35:50.970271 [DU-F1   ] [I] ue=0 c-rnti=0x4601 du_ue=0 cu_ue=0: F1 UE context removed.

            # This is to remove:
            # - The PCI associated with the UE identified with the pair (RNTI, ue)
            # - The entry of the last TA value for the UE identified with the pair (PCI, RNTI)

            elif metric_name == 'UE-Context-Removed':

                val_ue = int(val_metric)

                # ------------------------
                # Get the RNTI value present in the lines sub-window
                # ------------------------

                val_rnti = get_val('c-rnti=', l_sub_win_str_fdt, "0x[0-9a-fA-F]+")

                if val_rnti in [-1, -2]:
                    if val_rnti == -1:
                        do_log("ERROR: C-RNTI not present for metric '" + metric_desc[metric_name] + "'" + '\n')
                    else:
                        do_log("ERROR: invalid C-RNTI value for metric '" + metric_desc[metric_name] + "'")

                    l_win = []  # Clear the lines window
                    proc_state = st_waiting_metric
                    continue  # Continue with the next line of the log file

                # ------------------------
                # Get the PCI for this pair (RNTI, ue) as saved in the 'UE-Create' metric
                # ------------------------

                if (val_rnti, val_ue) not in dic_du_pci_rnti_ue:
                    do_log("ERROR: PCI not previously saved for this (RNTI, ue) for metric '" + metric_desc[metric_name] + "'")
                    l_win = []  # Clear the lines window
                    proc_state = st_waiting_metric
                    continue  # Continue with the next line of the log file

                val_pci = dic_du_pci_rnti_ue[(val_rnti, val_ue)]

                # Remove the association between (RNTI, ue) and PCI
                del dic_du_pci_rnti_ue[(val_rnti, val_ue)]

                # Remove the last TA value for this UE so TA is no longer considered for this UE
                if (val_pci, val_rnti) in dic_last_ta:
                    del dic_last_ta[(val_pci, val_rnti)]
                    do_log("Last TA value deleted for PCI=" +  str(val_pci) + " RNTI=" + str(val_rnti))

                params_str =  'RNTI: ' + val_rnti + ' | ' + 'Serving cell PCI: ' + str(val_pci) + ' | ' + 'UE: ' + str(val_ue)
                do_log(params_str)

                l_win = []  # Clear the lines window
                proc_state = st_waiting_metric  # Set state to metric readings
                log_metric = True

            # ------------------------------------------
            # TA @ PRACH RAR (' ta=', 'RAR PDSCH') @ srsdu log
            # - ta=
            # - tc-rnti=
            # - pci=
            # ------------------------------------------

            # 2025-12-02T16:37:08.795704 [SCHED   ] [D] [   305.9] Slot decisions pci=501 t=23us (1 PDSCH, 0 PUSCHs, 0 attempted PDCCHs, 0 attempted UCIs):
            # - DL PDCCH: rnti=0x39 type=ra-rnti cs_id=0 ss_id=1 format=1_0 cce=0 al=4
            # - RAR PDSCH: ra-rnti=0x39 rb=[0..3) symb=[2..14) tbs=9 mcs=0 rv=0 grants (1): tc-rnti=0x4608: rapid=43 ta=10 time_res=0

            # ------------------------------------------
            # TA command @ MAC CE ('ta_cmd=', 'DL PDU:') @ srsdu log
            # - ta_cmd=
            # - rnti=
            # - pci=
            # - ue=
            # ------------------------------------------

            # 2025-07-23T17:36:09.166176 [SCHED   ] [D] [   181.5] Slot decisions pci=103 t=63us (1 PDSCH, 0 PUSCHs, 0 attempted PDCCHs, 0 attempted UCIs):
            # ···
            # 2025-07-23T17:36:09.166207 [MAC     ] [I] [   181.5] DL PDU: ue=0 rnti=0x4601 size=149: TA_CMD: tag_id=0, ta_cmd=30, SDU: lcid=1 nof_sdus=1 total_size=5           

            elif metric_name in ['ta', 'ta_cmd']:

                # ------------------------
                # Calculate NTA value for numerology μ=0 (Δfc = 15 kHz)
                # ------------------------

                ta_val = int(val_metric)

                # ------------------------
                # Get the serving cell PCI value present in the lines sub-window
                # ------------------------

                val_pci = get_val('pci=', l_sub_win_str, "\\d+", end=pos_metric, fd='r', vt='int')

                if val_pci in [-1, -2]:
                    if val_pci == -1:
                        do_log("ERROR: PCI of serving cell not present for metric '" + metric_desc[metric_name] + "'" + '\n')
                    else:
                        do_log("ERROR: invalid PCI value of serving cell for metric '" + metric_desc[metric_name] + "'")

                    l_win = []  # Clear the lines window
                    proc_state = st_waiting_metric
                    continue  # Continue with the next line of the log file

                # ·····················
                # TA @ PRACH RAR ('RAR PDSCH') = TA initialization
                # ·····················
                if metric_name == 'ta':

                    # ------------------------
                    # Get the TC-RNTI value present in the lines sub-window
                    # ------------------------

                    val_rnti = get_val('tc-rnti=', l_sub_win_str_fdt, "0x[0-9a-fA-F]+", end=pos_metric, fd='r')

                    if val_rnti in [-1, -2]:
                        if val_rnti == -1:
                            do_log("ERROR: TC-RNTI not present for metric '" + metric_desc[metric_name] + "'" + '\n')
                        else:
                            do_log("ERROR: invalid TC-RNTI value for metric '" + metric_desc[metric_name] + "'")

                        l_win = []  # Clear the lines window
                        proc_state = st_waiting_metric
                        continue  # Continue with the next line of the log file

                    params_str = 'RNTI: ' + val_rnti + ' | ' + 'Serving cell PCI: ' + str(val_pci)

                    # Calculate NTA value for numerology μ=0 (Δfc = 15 kHz)
                    ta_nta = ta_val * 16 * 64

                # ·····················
                # TA command @ MAC CE = TA update
                # ·····················
                else:  # metric_name == 'ta_cmd':

                    # ------------------------
                    # Get the RNTI value present in the lines sub-window
                    # ------------------------

                    val_rnti = get_val('rnti=', l_sub_win_str_fdt, "0x[0-9a-fA-F]+", end=pos_metric, fd='r')

                    if val_rnti in [-1, -2]:
                        if val_rnti == -1:
                            do_log("ERROR: RNTI not present for metric '" + metric_desc[metric_name] + "'" + '\n')
                        else:
                            do_log("ERROR: invalid RNTI value for metric '" + metric_desc[metric_name] + "'")

                        l_win = []  # Clear the lines window
                        proc_state = st_waiting_metric
                        continue  # Continue with the next line of the log file

                    # ------------------------
                    # Get the ue identifier present in the lines sub-window
                    # ------------------------

                    val_ue = get_val('ue=', l_sub_win_str, "\\d+", end=pos_metric, fd='r', vt='int')

                    if val_ue in [-1, -2]:
                        if val_ue == -1:
                            do_log("ERROR: ue not present for metric '" + metric_desc[metric_name] + "'" + '\n')
                        else:
                            do_log("ERROR: invalid ue value for metric '" + metric_desc[metric_name] + "'")

                        l_win = []  # Clear the lines window
                        proc_state = st_waiting_metric
                        continue  # Continue with the next line of the log file

                    if (val_pci, val_rnti) in dic_last_ta:

                        params_str = 'RNTI: ' + val_rnti + ' | ' + 'Serving cell PCI: ' + str(val_pci) + ' | ' + 'UE: ' + str(val_ue)

                        # Calculate NTA value for numerology μ=0 (Δfc = 15 kHz)
                        ta_nta = dic_last_ta[(val_pci, val_rnti)][1] + (ta_val - 31) * 16 * 64

                    else:

                        # ·····················
                        # TA update command without previous RAR TA initialization
                        # ·····················

                        params_str = 'RNTI: ' + val_rnti + ' | ' + 'Serving cell PCI: ' + str(val_pci) + ' | ' + 'UE: ' + str(val_ue)
                        do_log(params_str)

                        do_log("ERROR: TA command without previous RAR TA initialization")

                        l_win = []  # Clear the lines window
                        proc_state = st_waiting_metric
                        continue  # Continue with the next line of the log fil

                if val_pci not in dic_m_ta:
                    dic_m_ta[val_pci] = {}

                if val_rnti not in dic_m_ta[val_pci]:
                    dic_m_ta[val_pci][val_rnti] = {'TA': [], 'NTA': [], 'TTA': []}

                # - Add the initial NTA or updated NTA value to the TA list of the UE receiving TA value
                # - Repeat the last NTA value to the TA lists for the rest of UEs

                ta_tta = round(ta_nta * nr_tc, 8)

                # Update the last NTA for the UE identified by the pair (PCI, RNTI)
                dic_last_ta[(val_pci, val_rnti)] = [ta_val, ta_nta, ta_tta]

                # Iterate RNTI at each PCI
                for v in dic_m_ta[val_pci]:
                    if v == val_rnti:
                        dic_m_ta[val_pci][v]['TA'].append(ta_val)
                        dic_m_ta[val_pci][v]['NTA'].append(ta_nta)
                        dic_m_ta[val_pci][v]['TTA'].append(ta_tta)
                    elif (val_pci, v) in dic_last_ta:
                        dic_m_ta[val_pci][v]['TA'].append(dic_last_ta[(val_pci, v)][0])
                        dic_m_ta[val_pci][v]['NTA'].append(dic_last_ta[(val_pci, v)][1])
                        dic_m_ta[val_pci][v]['TTA'].append(dic_last_ta[(val_pci, v)][2])

                do_log(params_str)

                l_win = []  # Clear the lines window
                proc_state = st_waiting_metric  # Set state to metric reading
                log_metric = True

            # ------------------------------------------
            # InitialULRRCMessageTransfer ('[I] ue=', '[CU-UEMNG] [I]') @ srscu log
            # - ue=
            # - pci=
            # - rnti=
            # ------------------------------------------

            # 2025-12-02T16:32:50.033829 [CU-CP-F1] [I] Rx PDU du=5 tid=1 du_ue=0: InitialULRRCMessageTransfer
            # 2025-12-02T16:32:50.033896 [CU-UEMNG] [I] ue=0 du_index=0 plmn=21405 gnb_du_id=5 pci=501 rnti=0x4601 pcell_index=0: Created new CU-CP UE

            # This is to get the PCI associated with each UE identified by the pair (RNTI, ue) for usage in other CU metrics

            elif metric_name == 'InitialULRRCMessageTransfer':

                val_ue = int(val_metric)

                # ------------------------
                # Locate and identify the PCI
                # ------------------------

                val_pci = get_val('pci=', l_sub_win_str, "\\d+", ini=pos_metric, vt='int')

                if val_pci in [-1, -2]:
                    if val_pci == -1:
                        do_log("ERROR: PCI of serving cell not present for metric '" + metric_desc[metric_name] + "'" + '\n')
                    else:
                        do_log("ERROR: invalid serving cell PCI value for metric '" + metric_desc[metric_name] + "'")

                    l_win = []  # Clear the lines window
                    proc_state = st_waiting_metric
                    continue  # Continue with the next line of the log file

                # ------------------------
                # Locate and identify the RNTI
                # ------------------------

                val_rnti = get_val('rnti=', l_sub_win_str, "0x[0-9a-fA-F]+", ini=pos_metric)

                if val_rnti in [-1, -2]:
                    if val_rnti == -1:
                        do_log("ERROR: RNTI not present for metric '" + metric_desc[metric_name] + "'" + '\n')
                    else:
                        do_log("ERROR: invalid RNTI value for metric '" + metric_desc[metric_name] + "'")

                    l_win = []  # Clear the lines window
                    proc_state = st_waiting_metric
                    continue  # Continue with the next line of the log file

                # Save the PCI and RNTI of this UE (ue) for CU metrics
                dic_cu_pci_rnti_ue[val_ue] = {'PCI': val_pci, 'RNTI': val_rnti}

                params_str = 'RNTI: ' + val_rnti + ' | Serving cell PCI: ' + str(val_pci) + ' | ' + 'UE: ' + str(val_ue)
                do_log(params_str)

                l_win = []  # Clear the lines window
                proc_state = st_waiting_metric  # Set state to metric readings
                log_metric = True

            # ------------------------------------------
            # RRC-setup ('[D] ue=', '[RRC     ]', 'Rx SRB1 DCCH UL', 'rrcSetupComplete') @ srscu log
            # - ue=
            # - c-rnti=
            # ------------------------------------------

            # 2025-12-02T16:35:56.761755 [RRC     ] [D] ue=4 c-rnti=0x4603: Rx SRB1 DCCH UL rrcSetupComplete (79 B)
            # 2025-12-02T16:35:56.761757 [RRC     ] [D] ue=4 c-rnti=0x4603: Containerized rrcSetupComplete: [

            # ------------------------------------------
            # RRC-release ('[D] ue=', '[RRC     ]', 'Tx SRB1 DCCH DL', 'rrcRelease') @ srscu log
            # - ue=
            # - c-rnti=
            # ------------------------------------------

            # 2025-12-02T16:36:24.853724 [RRC     ] [D] ue=1 c-rnti=0x4601: Tx SRB1 DCCH DL rrcRelease (8 B)
            # 2025-12-02T16:36:24.853726 [RRC     ] [D] ue=1 c-rnti=0x4601: Containerized rrcRelease: [

            elif metric_name in ['RRC-setup', 'RRC-release']:

                val_ue = int(val_metric)

                # ------------------------
                # Get the C-RNTI value present in the lines sub-window
                # ------------------------

                val_rnti = get_val('c-rnti=', l_sub_win_str_fdt, "0x[0-9a-fA-F]+")

                if val_rnti in [-1, -2]:
                    if val_rnti == -1:
                        do_log("ERROR: C-RNTI not present for metric '" + metric_desc[metric_name] + "'" + '\n')
                    else:
                        do_log("ERROR: invalid C-RNTI value for metric '" + metric_desc[metric_name] + "'")

                    l_win = []  # Clear the lines window
                    proc_state = st_waiting_metric
                    continue  # Continue with the next line of the log file

                # ------------------------
                # Get the PCI for this ue from the 'InitialULRRCMessageTransfer' metric
                # ------------------------

                if val_ue in dic_cu_pci_rnti_ue:
                    val_pci = dic_cu_pci_rnti_ue[val_ue]['PCI']
                else:
                    do_log("ERROR: PCI not found for the pair (RNTI, ue) for metric '" + metric_desc[metric_name] + "'")
                    l_win = []  # Clear the lines window
                    proc_state = st_waiting_metric
                    continue  # Continue with the next line of the log file

                rrc_conn_val = rrc_conn_delta[metric_name]

                dic_metric = {'RNTI': val_rnti, 'RRC-conn': rrc_conn_val}

                if val_pci in dic_m_rrc_conn:
                    dic_m_rrc_conn[val_pci].append(dic_metric)
                else:
                    dic_m_rrc_conn[val_pci] = [dic_metric]

                params_str = 'RNTI: ' + val_rnti + ' | ' + 'Serving cell PCI: ' + str(val_pci) + ' | ' + 'UE: ' + str(val_ue)
                do_log(params_str)

                l_win = []  # Clear the lines window
                proc_state = st_waiting_metric  # Set state to metric reading
                log_metric = True

            # ------------------------------------------
            # SDAP-DL ('pdu_len=', '[SDAP    ] [D]', 'DL: TX PDU') @ srscu log
            # - ue=
            # - pdu_len=
            # ------------------------------------------

            # 2025-12-02T16:35:42.725530 [SDAP    ] [D] ue=3 psi=2 QFI=1 DRB2 DL: TX PDU. QFI=1 pdu_len=126
            # ···
            # 2025-12-02T16:35:42.744907 [SDAP    ] [D] ue=3 psi=2 QFI=1 DRB2 UL: RX SDU. QFI=1 sdu_len=52

            elif metric_name == 'SDAP-DL':

                data_dl_val = int(val_metric)  # SDAP PDU size (pdu_len=) (SDAP has no header)

                # ------------------------
                # Locate and identify the ue
                # ------------------------

                val_ue = get_val('ue=', l_sub_win_str, "\\d+", end=pos_metric, fd='r', vt='int')

                if val_ue in [-1, -2]:
                    if val_ue == -1:
                        do_log("ERROR: UE not found for metric '" + metric_desc[metric_name] + "'" + '\n')
                    else:
                        do_log("ERROR: invalid UE value for metric '" + metric_desc[metric_name] + "'")

                    l_win = []  # Clear the lines window
                    proc_state = st_waiting_metric
                    continue  # Continue with the next line of the log file

                # Get the PCI and RNTI for this UE from the 'InitialULRRCMessageTransfer' metric
                if val_ue in dic_cu_pci_rnti_ue:
                    val_rnti = dic_cu_pci_rnti_ue[val_ue]['RNTI']
                    val_pci = dic_cu_pci_rnti_ue[val_ue]['PCI']
                else:
                    do_log("ERROR: PCI and RNTI not found for this UE (ue=" + str(val_ue) + ") for metric '" + metric_desc[metric_name] + "'")
                    l_win = []  # Clear the lines window
                    proc_state = st_waiting_metric
                    continue  # Continue with the next line of the log file

                # Option 1: Add DL data volume aggregated at the RNTI level.
                #           dic_m_sdap_dl will have one entry per PCI and RNTI

                if op_agg_sdap_dl == 1:

                    # Aggregate DL data traffic at the RNTI level
                    if val_ue in dic_sdap_ue_data:
                        dic_sdap_ue_data[val_ue] += data_dl_val
                    else:
                        dic_sdap_ue_data[val_ue] = data_dl_val

                    # Add aggregated DL data volume to the metric dictionary for the current window

                    dic_metric = {'RNTI': val_rnti, 'Data-DL': dic_sdap_ue_data[val_ue]}

                    if val_pci in dic_m_sdap_dl:

                        # Update the DL data for the RNTI found
                        rnti_found = False
                        for i, rnti_data in enumerate(dic_m_sdap_dl[val_pci]):
                            # If this RNTI is present in the list then update the DL data
                            if rnti_data['RNTI'] == val_rnti:
                                dic_m_sdap_dl[val_pci][i] = dic_metric
                                rnti_found = True

                        # If this RNTI is not present in the list then add it
                        if not rnti_found:
                            dic_m_sdap_dl[val_pci].append(dic_metric)

                    else:

                        dic_m_sdap_dl[val_pci]= [dic_metric]

                    do_log("Aggregated value: " + str(dic_sdap_ue_data[val_ue]))

                # Option 2: Add each DL data volume measurement independently without aggregating
                #           dic_m_sdap_dl can have multiple measurements for the same RNTI

                # Add DL data volume to the metric dictionary for the current window

                if op_agg_sdap_dl == 0:

                    if val_pci in dic_m_sdap_dl:
                        dic_m_sdap_dl[val_pci].append({'RNTI': val_rnti, 'Data-DL': data_dl_val})
                    else:
                        dic_m_sdap_dl[val_pci] = [{'RNTI': val_rnti, 'Data-DL': data_dl_val}]

                params_str = 'RNTI: ' + val_rnti +  ' | Serving cell PCI: ' + str(val_pci)
                do_log(params_str)

                l_win = []  # Clear the lines window
                proc_state = st_waiting_metric  # Set state to metric readings
                log_metric = True

            # ------------------------------------------
            # RSRP ('"rsrp":') @ srscu log
            # · measResultServingCell ▶ physCellId, rsrp
            # · measResultNeighCells  ▶ physCellId, rsrp
            #
            # - c-rnti=
            # - "physCellId": (pci)
            # - "rsrp":
            # ------------------------------------------

            # 2025-12-02T16:35:30.503344 [RRC     ] [D] ue=2 c-rnti=0x4602: Containerized measurementReport: [
            # ···
            #                       "measResultServingCell": {
            #                         "physCellId": 301,
            # ···
            #                               "rsrp": 71,
            # ···
            #                       "measResultNeighCells": {
            # ···
            #                         "physCellId": 501,
            # ···
            #                               "rsrp": 61,

            elif metric_name == 'RSRP':

                # ------------------------
                # Get the C-RNTI value present in the lines sub-window
                # ------------------------

                val_rnti = get_val('c-rnti=', l_sub_win_str_fdt, "0x[0-9a-fA-F]+", end=pos_metric, fd='r')

                if val_rnti in [-1, -2]:
                    if val_rnti == -1:
                        do_log("ERROR: C-RNTI not present for metric '" + metric_desc[metric_name] + "'" + '\n')
                    else:
                        do_log("ERROR: invalid C-RNTI value for metric '" + metric_desc[metric_name] + "'")

                    l_win = []  # Clear the lines window
                    proc_state = st_waiting_metric
                    continue  # Continue with the next line of the log file

                params_str = 'RNTI: ' + val_rnti

                # ------------------------
                # Locate the last occurrence of:
                # - measResultServingCell ▶ physCellId, rsrp
                # - measResultNeighCells  ▶ physCellId, rsrp
                # ------------------------

                # Set:
                # - val_pci_serv: PCI of the serving cell for this report
                # - val_rsrp_serv: RSRP of the serving cell for this report

                # Then set state to 'st_reading_neigh_rsrp', clear the line list
                # and initialize the neighbor RSRP lists

                pos_meas_serv = l_sub_win_str_fdt.rfind('measResultServingCell')
                pos_meas_neig = l_sub_win_str_fdt.rfind('measResultNeighCells')
                pos_index_res = l_sub_win_str_fdt.rfind('rsIndexResults')

                if pos_meas_serv < pos_meas_neig:
                    do_log("ERROR: measResultNeighCells found but measResultServingCell expected for RSRP metric")
                    l_win = []  # Clear the lines window
                    proc_state = st_waiting_metric
                    continue  # Continue with the next line of the log file

                if pos_meas_serv < pos_index_res:
                    do_log("ERROR: rsIndexResults found but measResultServingCell expected for RSRP metric")
                    l_win = []  # Clear the lines window
                    proc_state = st_waiting_metric
                    continue  # Continue with the next line of the log file

                # RSRP metric without measResultServingCell: maybe an "a3-Offset" value
                if pos_meas_serv == -1:
                    l_win = []  # Clear the lines window
                    proc_state = st_waiting_metric
                    continue  # Continue with the next line of the log file

                # ------------------------
                # Get the serving cell PCI value present in the lines sub-window
                # ------------------------

                val_pci_serv = get_val('"physCellId":', l_sub_win_str_fdt, "\\d+", ini=pos_meas_serv, fd='r', vt='int')

                if val_pci_serv in [-1, -2]:
                    if val_pci_serv == -1:
                        do_log("ERROR: PCI of serving cell not present for metric '" + metric_desc[metric_name] + "'" + '\n')
                    else:
                        do_log("ERROR: invalid PCI value of serving cell for metric '" + metric_desc[metric_name] + "'")

                    l_win = []  # Clear the lines window
                    proc_state = st_waiting_metric
                    continue  # Continue with the next line of the log file

                # ------------------------
                # Get the RSRP value present in the lines sub-window
                # ------------------------

                pos_pci_serv = l_sub_win_str_fdt.rfind('"physCellId":', pos_meas_serv)

                pos_rsrp_serv = l_sub_win_str_fdt.find(metric_keyw[metric_name][0], pos_pci_serv)

                s = l_sub_win_str_fdt[pos_rsrp_serv + len(metric_keyw[metric_name][0]):].strip()
                match = re.match("\\d+", s)
                val_rsrp_serv = s[match.start():match.end()]

                do_log(params_str)

                l_win = []  # Clear the lines window
                proc_state = st_waiting_meas_res_neigh  # Set state to reading RSRP of neighbor cells
                log_metric = True

                # Initialize neighbor cells RSRP lists
                list_pci_neig = []
                list_neig_rsrp = []
                list_delta_rsrp = []

            # ------------------------------------------
            # UE-conn ('Number of gNB-UEs is now', 'amf') @ open5gs log
            # ------------------------------------------

            elif metric_name == 'UE-conn':

                s_conn = l_sub_win_str_fdt[:pos_metric_fdt]

                if s_conn.find('[Added]') != -1:

                    # Get the UE RAN_UE_NGAP_ID (3GPP TS 38.401 - 5G · NG-RAN · Architecture description)
                    s = l_sub_win_str[pos_metric:]
                    pos_ue_ngap_id = s.find('RAN_UE_NGAP_ID[')

                    if pos_ue_ngap_id == -1:
                        do_log("ERROR: RAN UE NGAP ID not present for metric '" + metric_desc[metric_name] + "'")
                        l_win = []  # Clear the lines window
                        proc_state = st_waiting_metric
                        continue  # Continue with the next line of the log file

                    s = s[pos_ue_ngap_id + len('RAN_UE_NGAP_ID['):].strip()
                    match_ue_ngap_id = re.match("[0-9a-fA-F]+", s)

                    val_ran_ue_ngap_id = s[match_ue_ngap_id.start():match_ue_ngap_id.end()]

                    cell_id_ue_ngap_id_str = 'RAN_UE_NGAP_ID: ' + str(val_ran_ue_ngap_id) + ' | '

                    # Get the serving cell Cell ID and metric values

                    s = l_sub_win_str[pos_metric:]
                    pos_cell_id = s.find('CellID[')

                    if pos_cell_id == -1:
                        do_log("ERROR: Cell ID of serving cell not present for metric '" + metric_desc[metric_name] + "'")
                        l_win = []  # Clear the lines window
                        proc_state = st_waiting_metric
                        continue  # Continue with the next line of the log file

                    s = s[pos_cell_id + len('CellID['):].strip()
                    match_cell_id = re.match("0x[0-9a-fA-F]+", s)

                    if match_cell_id:
                        val_cell_id = s[match_cell_id.start():match_cell_id.end()]

                        # Map cell ID to PCI
                        val_pci = pci_cell_id[val_cell_id]

                        # Annotate the PCI and cell ID of the added UE
                        dic_ue_ngap_id_pci[val_ran_ue_ngap_id] = [val_pci, val_cell_id]
                    else:
                        do_log("ERROR: invalid cell ID for metric '" + metric_desc[metric_name] + "'")
                        l_win = []  # Clear the lines window
                        proc_state = st_waiting_metric
                        continue  # Continue with the next line of the log file

                    conn_ue = 1

                elif s_conn.find('[Removed]') != -1:

                    # Get the UE RAN_UE_NGAP_ID (3GPP TS 38.401 - 5G · NG-RAN · Architecture description)
                    pos_ue_context_rel = l_sub_win_str.rfind('[amf] INFO: UE Context Release')
                    s = l_sub_win_str[pos_ue_context_rel:pos_metric]
                    pos_ue_ngap_id = s.find('RAN_UE_NGAP_ID[')

                    if pos_ue_ngap_id == -1:
                        do_log("ERROR: RAN UE NGAP ID not present for metric '" + metric_desc[metric_name] + "'")
                        l_win = []  # Clear the lines window
                        proc_state = st_waiting_metric
                        continue  # Continue with the next line of the log file

                    s = s[pos_ue_ngap_id + len('RAN_UE_NGAP_ID['):].strip()
                    match_ue_ngap_id = re.match("[0-9a-fA-F]+", s)

                    val_ran_ue_ngap_id = s[match_ue_ngap_id.start():match_ue_ngap_id.end()]

                    cell_id_ue_ngap_id_str = 'RAN_UE_NGAP_ID: ' + str(val_ran_ue_ngap_id) + ' | '

                    if val_ran_ue_ngap_id in dic_ue_ngap_id_pci:
                        # Get the PCI and cell ID of the removed UE from previous added UE connections annotations
                        val_pci, val_cell_id = dic_ue_ngap_id_pci[val_ran_ue_ngap_id]
                        del dic_ue_ngap_id_pci[val_ran_ue_ngap_id]  # Remove PCI for that UE
                    else:
                        do_log("ERROR: PCI and Cell ID of the removed UE are unknown (not added previously) for " + metric_desc[metric_name])
                        l_win = []  # Clear the lines window
                        proc_state = st_waiting_metric
                        continue  # Continue with the next line of the log file

                    conn_ue = -1

                else:
                    do_log("ERROR: 'Added' or 'Removed' tag not present for metric '" + metric_desc[metric_name] + "'")
                    l_win = []  # Clear the lines window
                    proc_state = st_waiting_metric
                    continue  # Continue with the next line of the log file

                cell_id_ue_ngap_id_str = cell_id_ue_ngap_id_str + 'Cell ID: ' + val_cell_id + ' | PCI: ' + val_pci

                dic_metric = {'RAN_UE_NGAP_ID':val_ran_ue_ngap_id, 'UE-conn': conn_ue}

                if val_pci in dic_m_ue_conn:
                    dic_m_ue_conn[val_pci].append(dic_metric)
                else:
                    dic_m_ue_conn[val_pci] = [dic_metric]

                # Print serving cell ID and UE RAN UE NGAP ID values for the current metric
                do_log(cell_id_ue_ngap_id_str)

                l_win = []  # Clear the lines window
                proc_state = st_waiting_metric  # Set state to metric reading
                log_metric = True

            else:
                do_log("ERROR: Metric not defined ")
                l_win = []  # Clear the lines window
                proc_state = st_waiting_metric
                continue

        # Keep the lines windows between 200 and 300 lines

        if len(l_win) > 300:
            l_win = l_win[-200:]

# Handle incoming TCP commands
async def handle_client(reader, writer):

    global proc_enable, log_metric
    global dic_m_ta, dic_m_rrc_conn, dic_m_ue_conn, dic_m_sdap_dl, dic_m_rsrp_serv, dic_m_rsrp_delta
    global dic_last_ta, dic_sdap_ue_data

    data = await reader.read(1024)
    command = data.decode().strip().lower()

    addr = writer.get_extra_info('peername')

    if log_metric:
        do_log("")

    do_log(f"Received '{command}' command from {addr}")

    if command == "enable":
        writer.write(b"Rx 'enable' command\n")

        proc_enable = True

    elif "save" in command:
        writer.write(b"Rx 'save' command\n")

        # ------------------------
        # If the command 'save' is received:
        # - Finish the temporal window
        # - Print the created list
        # - Create counting and dump them to the JSON file
        # - Empty lists
        # ------------------------

        dt_save = command[command.find("dt=") + 3:len(command)]

        do_log('')
        do_log("... Temporal window closed")
        do_log('------------------------------------------------------')
        do_log('')

        # Print lists

        do_log("Generated dictionaries:")
        do_log('')

        do_log("- TA:")
        for k in dic_m_ta:
            do_log('  PCI: ' + str(k))
            for s in dic_m_ta[k]:
                do_log('  · RNTI: ' + s + ' NTA = ' + str(dic_m_ta[k][s]['NTA']))
        do_log('')

        do_log("- RRC connection:")
        for k in dic_m_rrc_conn:
            do_log('PCI: ' + str(k))
            for s in dic_m_rrc_conn[k]:
                do_log(str(s))
        do_log('')

        do_log("- UE connections:")
        for k in dic_m_ue_conn:
            do_log('PCI: ' + str(k))
            for s in dic_m_ue_conn[k]:
                do_log(str(s))
        do_log('')

        do_log("- SDAP DL data:")
        for k in dic_m_sdap_dl:
            do_log('PCI: ' + str(k))
            for s in dic_m_sdap_dl[k]:
                do_log(str(s))
        do_log('')

        do_log('- RSRP serving cells:')
        for k in dic_m_rsrp_serv:
            do_log('PCI: ' + str(k))
            for s in dic_m_rsrp_serv[k]:
                do_log(str(s))
        do_log('')

        do_log('- Delta RSRP neighbor cells:')
        for k in dic_m_rsrp_delta:
            do_log('PCI: ' + str(k))
            for s in dic_m_rsrp_delta[k]:
                do_log(str(s))
        do_log('')

        # ------------------------
        # Create counting
        # ------------------------

        # dic_count =

        # - Dictionary with data collected for one temporal window comprising several cells and
        #   several metrics. Each dictionary has two timestamp keys 'Datetime' (PC date time) and
        #   'Timestamp' (date and time from the logs) and one key 'Data' containing the collected data.
        # - 'Data' key contains a dictionary with keys corresponding to the PCIs of the cells for
        #   which data has been collected.
        # - Each PCI key contains a list of dictionaries. Each dictionary contains:
        #   · Key 'Metric' = Metric name
        #   · Key 'Value'  = List of unique values for that metric
        #   · Key 'Count'  = Counting for each unique value

        #              Local               Logs
        # {'Datetime': -----, 'Timestamp': ----, 'Data':

        #                          Metric 1
        #     {'PCI1': [{'Metric': --------, 'Value': [], 'Count': []},
        #
        #                          Metric 2
        #               {'Metric': --------, 'Value': [], 'Count': []},
        #
        #               ···
        #                          Metric m
        #               {'Metric': --------, 'Value': [], 'Count': []}],
        #
        #                          Metric 1
        #      'PCI2': [{'Metric': --------, 'Value': [], 'Count': []}, ···
        #
        #     }
        #
        # }

        # For metric 'RSRP delta':

        #
        #               {'Metric': 'RSRP delta', 'Values': {PCI neighbor 1: {'Value': [], 'Count': []}},
        #                                                   PCI neighbor 2: {'Value': [], 'Count': []}},
        #                                                   ···
        #                                                   PCI neighbor n: {'Value': [], 'Count': []}}}

        # Count = Count of each unique metric value in the current temporal window

        # Current date-time (ISO 8601)
        dt_now = datetime.now().isoformat()

        dic_count = {'dt_PC': dt_now, 'dt_cont': dt_save, 'dt_srsran': dt_iso, 'Data': {}}

        # ------------------------
        # NTA counting
        # ------------------------

        # If the NTA dictionary is not empty
        if dic_m_ta:

            for pci in dic_m_ta:

                nta_list = [nta for rnti in dic_m_ta[pci] for nta in dic_m_ta[pci][rnti]['NTA']]

                # If there is at least one NTA numeric value
                if nta_list:

                    count = np.unique(nta_list, return_counts=True)
                    dic_metric = {'Metric': 'NTA', 'Value': count[0].tolist(), 'Count': count[1].tolist()}

                    if pci in dic_count['Data']:
                        dic_count['Data'][pci].append(dic_metric)
                    else:
                        dic_count['Data'][pci] = [dic_metric]
                        
        # ------------------------
        # RRC connection counting
        # ------------------------

        # If the RRC connection dictionary is not empty
        if dic_m_rrc_conn:

            for pci in dic_m_rrc_conn:

                rrc_conn_list = [v['RRC-conn'] for v in dic_m_rrc_conn[pci] if v['RRC-conn'] is not None]

                # If there is at least one NTA numeric value
                if rrc_conn_list:

                    count = np.unique(rrc_conn_list, return_counts=True)
                    dic_metric = {'Metric': 'RRC-conn', 'Value': count[0].tolist(), 'Count': count[1].tolist()}

                    if pci in dic_count['Data']:
                        dic_count['Data'][pci].append(dic_metric)
                    else:
                        dic_count['Data'][pci] = [dic_metric]

        # ------------------------
        # UE connection counting
        # ------------------------

        # If the UE connections dictionary is not empty
        if dic_m_ue_conn:

            for pci in dic_m_ue_conn:

                ues_list = [v['UE-conn'] for v in dic_m_ue_conn[pci] if v['UE-conn'] is not None]

                # If there is at least one NTA numeric value
                if ues_list:

                    count = np.unique(ues_list, return_counts=True)
                    dic_metric = {'Metric': 'UE-conn', 'Value': count[0].tolist(),
                                  'Count': count[1].tolist()}

                    if pci in dic_count['Data']:
                        dic_count['Data'][pci].append(dic_metric)
                    else:
                        dic_count['Data'][pci] = [dic_metric]

        # ------------------------
        # DL data counting
        # ------------------------

        # If the DL data dictionary is not empty
        if dic_m_sdap_dl:

            for pci in dic_m_sdap_dl:

                data_dl_list = [v['Data-DL'] for v in dic_m_sdap_dl[pci] if v['Data-DL'] is not None]

                # If there is at least one NTA numeric value
                if data_dl_list:

                    count = np.unique(data_dl_list, return_counts=True)
                    dic_metric = {'Metric': 'Data-DL', 'Value': count[0].tolist(), 'Count': count[1].tolist()}

                    if pci in dic_count['Data']:
                        dic_count['Data'][pci].append(dic_metric)
                    else:
                        dic_count['Data'][pci] = [dic_metric]

        # ------------------------
        # Serving cells RSRP counting
        # ------------------------

        # If the serving cells RSRP dictionary is not empty
        if dic_m_rsrp_serv:

            for pci in dic_m_rsrp_serv:

                rsrp_serv_list = [int(v['RSRP serving']) for v in dic_m_rsrp_serv[pci] if v is not None]

                # If there is at least one RSRP numeric value
                if rsrp_serv_list:

                    count = np.unique(rsrp_serv_list, return_counts=True)
                    dic_metric = {'Metric': 'RSRP serving', 'Value': count[0].tolist(),
                                  'Count': count[1].tolist()}

                    if pci in dic_count['Data']:
                        dic_count['Data'][pci].append(dic_metric)
                    else:
                        dic_count['Data'][pci] = [dic_metric]

        # ------------------------
        # Δ RSRP counting
        # ------------------------

        # If the delta RSRP dictionary is not empty
        if dic_m_rsrp_delta:

            # Create a dictionary with keys being serving cells. Each serving cell key contains
            # another dictionary with keys being neighbor cells of that serving cell. Each neighbor
            # cell key contains one list with two lists (one list of neighbor RSRP values and other list
            # of delta RSRP values for that neighbor cell)

            dic_metric_list = {}

            # Iterate all serving cells
            for pci_s in dic_m_rsrp_delta:

                # Initialize delta RSRP dictionary for this serving cell
                if pci_s not in dic_metric_list:
                    dic_metric_list[pci_s] = {}

                # Iterate all delta RSRP values in this serving cell
                for d in dic_m_rsrp_delta[pci_s]:
                    pci_n = d['PCI neighbor']

                    if pci_n not in dic_metric_list[pci_s]:
                        # Initialize delta RSRP dictionary for this neighbor cell
                        dic_metric_list[pci_s][pci_n] = [[], []]

                    # Add neighbor cell RSRP and delta RSRP for this neighbor cell
                    dic_metric_list[pci_s][pci_n][0].append(d['RSRP neighbor'])
                    dic_metric_list[pci_s][pci_n][1].append(d['RSRP delta'])

            # Create countings from just created neighbor RSRP and delta RSRP lists

            # Iterate all serving cells
            for pci_s in dic_metric_list:

                dic_metric_n = {}

                # Iterate all neighbor cells in this serving cell
                for pci_n in dic_metric_list[pci_s]:

                    # List of delta RSRP values for this neighbor cell
                    rsrp_delta_list = dic_metric_list[pci_s][pci_n][1]

                    # If there is at least one delta RSRP value
                    if rsrp_delta_list:
                        count = np.unique(rsrp_delta_list, return_counts=True)
                        dic_metric_n[pci_n] = {'Value': count[0].tolist(),
                                               'Count': count[1].tolist()}

                dic_metric = {'Metric': 'RSRP delta', 'Values': dic_metric_n}

                if pci_s in dic_count['Data']:
                    dic_count['Data'][pci_s].append(dic_metric)
                else:
                    dic_count['Data'][pci_s] = [dic_metric]

        do_log('Counting:')

        for pci in dic_count['Data']:
            do_log('PCI: ' + str(pci))
            for metric in dic_count['Data'][pci]:
                metric: dict[str, str | list]  # Type-hint to avoid warning in PyCharm

                do_log('- Metric: ' + metric['Metric'])
                if metric['Metric'] == 'RSRP delta':
                    metric: dict[str, dict]  # Type-hint to avoid warning in PyCharm

                    for c in metric['Values']:
                        do_log('  - RSRP delta:')
                        do_log('  · Value: ' + str(metric['Values'][c]['Value']))
                        do_log('  · Count: ' + str(metric['Values'][c]['Count']))
                else:
                    metric: dict[str, str | list]  # Type-hint to avoid warning in PyCharm

                    do_log('  · Value: ' + str(metric['Value']))
                    do_log('  · Count: ' + str(metric['Count']))

        do_log('')

        # Dump counting to JSON file

        do_log("Saving counting files (mount / local):")
        do_log("- " + (str(count_file_name_mnt) if count_path_mnt else "---"))
        do_log("- " + count_file_name_loc)
        do_log("")

        if count_path_mnt:

            # noinspection PyBroadException
            try:
                with open(count_file_name_mnt, 'a') as c_file:
                    json.dump(dic_count, c_file)
                    c_file.write("\n")
            except:
                do_log("ERROR: '" + count_path_mnt + "' mounted unit is down: it is not possible to write '" + ini_count_file + "'")
                do_log("")

        with open(count_file_name_loc, 'a') as c_file:
            json.dump(dic_count, c_file)
            c_file.write("\n")

        # Start a new temporal window

        do_log('======================================================')
        do_log("Temporal window started ...")
        do_log('------------------------------------------------------')
        do_log('')

        log_metric = False

        # Clear lists at the beginning of each temporal window

        # Metric dictionaries
        dic_m_ta = {}
        dic_m_rrc_conn = {}
        dic_m_ue_conn = {}
        dic_m_sdap_dl = {}
        dic_m_rsrp_serv = {}
        dic_m_rsrp_delta = {}

        # Auxiliary dictionaries
        dic_sdap_ue_data = {}

    elif command == "quit":
        sys.stdout.write('\x1b[2K\n')
        print("... script quit")
        shutdown_event.set()

    else:
        writer.write(b"Rx unknown command\n")

    await writer.drain()
    writer.close()

# Start TCP server that listens for commands
async def start_tcp_server(par_port = 5000):
    host = "0.0.0.0"

    server = await asyncio.start_server(handle_client, host, par_port)
    addrs = ", ".join(str(sock.getsockname()) for sock in server.sockets)
    do_log("Listening for TCP commands on " + addrs + " ...")

    async with server:
        await shutdown_event.wait()

# Run log parser and server concurrently
async def main(par_port):

    await asyncio.gather(
        start_tcp_server(par_port),
        start_log_parser()
    )

# =============================================================================
# Initialization
# =============================================================================

if __name__ == "__main__":

    # =============================================================================
    # Constants
    # =============================================================================

    version = '2.00'

    nr_dfmax = 480000
    nr_nf = 4096
    nr_tc = 1 / (nr_dfmax * nr_nf)

    # Correspondence between NCI and PCI for all cells
    pci_cell_id = {'0x19b1f': '301',
                   '0x19b33': '501',
                   '0x19b51': '801',
                   '0x19b52': '802'}

    str_bool = ['No', 'Yes']

    mnt_remote_folder = '//192.168.0.195/smbd_home/logs/Counts'
    mnt_test = "mount | grep 195"

    # =============================================================================
    # Initialization
    # =============================================================================

    proc_enable = False
    log_metric = False

    dt_iso = ''

    st_waiting_metric = 0
    st_waiting_meas_res_neigh = 1
    st_waiting_neigh_rsrp_values = 2

    parser = argparse.ArgumentParser(description='log-parser-main')
    parser.add_argument('-c', '--comp', help='Network component', required=True, choices=['cu', 'du', 'core'])
    parser.add_argument('-p', '--port', help='TCP listening port (Default=5000)', required=False, type=int, default=5000)

    args = vars(parser.parse_args())

    net_comp = args['comp']
    tcp_port = args['port']

    time_log = time.strftime("%Y%m%d_%H%M%S", time.localtime())
    log_file_name = 'log_' + net_comp + "_" + time_log + '.txt'
    log_file = open(log_file_name, 'w')

    # =============================================================================
    # Options
    # =============================================================================

    # Option to choose if the count file is also saved in a samba mounted folder:
    # /mnt/home_logs_counts_195 <- smb://192.168.0.195/smbd_home/logs/Logs = /home/ue/logs/Logs @ Asus Gaming
    op_count_net = 1  # 0 = Save count file locally / 1 = Save count file locally and in a samba network folder

    op_agg_sdap_dl = 1  # 0 = Do not aggregate / 1 = Aggregate

    # =============================================================================
    # Main
    # =============================================================================

    do_log("Log processor :: Version " + version)
    do_log("")

    if sys.platform == 'linux':
        do_log("OS: Ubuntu")
    elif sys.platform == 'win32':
        do_log("OS: Windows")
    else:
        do_log("OS: unknown")
        sys.exit()

    do_log("")

    log_path = './'

    do_log("Options:")
    do_log("- Network component: " + net_comp)
    do_log("- TCP listening port: " + str(tcp_port))
    do_log("- Aggregate SDAP data: " + str_bool[op_agg_sdap_dl])
    do_log("- Save count file to network: " + str_bool[op_count_net])
    do_log("")

    # Metric dictionaries
    dic_m_ta = {}  # Dictionary for timing advance metrics
    dic_m_rrc_conn = {}  # Dictionary for RRC connection establishment and release
    dic_m_ue_conn = {}  # Dictionary for UE added and removed connections at the AMf
    dic_m_sdap_dl = {}  # Dictionary for aggregated SDAP DL data
    dic_m_rsrp_serv = {}  # Dictionary for serving cells RSRP provided by reports
    dic_m_rsrp_delta = {}  # Dictionary for neighbor cells delta RSRP provided by reports

    # Auxiliary dictionaries
    dic_last_ta = {}  # Dictionary containing the last timing advance NTA for each pair [cell (PCI), UE (RNTI)]
    dic_du_pci_rnti_ue = {}  # Dictionary containing the PCI of each pair (RNTI, ue) for DU metrics
    dic_cu_pci_rnti_ue = {}  # Dictionary containing the RNTI and PCI of each ue for CU metrics
    dic_sdap_ue_data = {}  # Dictionary containing aggregated SDAP data volume at the RNTI level
    dic_ue_ngap_id_pci = {}  # Dictionary containing the PCI of each added UE in the AMF

    user = os.getlogin()

    if sys.platform == 'linux':
        ini_path = r'/home/' + user + '/config/srs-log/'
        inp_path = r'/home/' + user + '/logs/Logs/'
        count_path_loc = r'/home/' + user + '/logs/local-Counts/'
        if op_count_net:
            count_path_mnt = '/mnt/home_logs_counts_195'
        else:
            count_path_mnt = None
    else:
        ini_path = r'C:/Zona/Profesional/O-RAN/Org/Config srsRAN/Asus Gaming - UE - B210/home · ue · config/srs-log/'
        inp_path = r'./-Logs-/Logs/'
        count_path_loc = r'./-Logs-/local-Counts/'
        count_path_mnt = None

    ini_file_base = 'config-parser-main-' + net_comp + '.ini'

    ini_file = ini_path + ini_file_base

    do_log("INI file:")
    do_log("")
    do_log("· Ini file path: " + ini_path)
    do_log("· Ini file name: " + ini_file_base)
    do_log("")

    config = configparser.ConfigParser()
    config.read(ini_file)

    if config.has_option('config', 'tcp_port') and config['config']['tcp_port']:
        try:
            tcp_port = int(config['config']['tcp_port'])
            do_log("- TCP listening port: " + str(tcp_port))
        except ValueError:
            print("WARNING: non-integer value in 'tcp_port' parameter. Parameter ignored.")

    if config.has_option('config', 'comment'):
        ini_comm = config['config']['comment']  # Comment
        do_log("- Comment: " + ini_comm)
    else:
        ini_comm = '-'

    if config.has_option('config', 'du_id'):
        ini_du_id = config['config']['du_id']  # DU identifier
        do_log("- DU Id: " + ini_du_id)
    else:
        ini_du_id = '-'

    if config.has_option('config', 'gnb_id'):
        ini_gnb_id = config['config']['gnb_id']  # DU identifier
        do_log("- gNB Id: " + ini_gnb_id)
    else:
        ini_gnb_id = '-'
        
    if config.has_option('config', 'cells_ids'):
        ini_cells_ids = config['config']['cells_ids']  # DU identifier
        do_log("- Cells Ids: " + ini_cells_ids)
    else:
        ini_cells_ids = '-'        

    if config.has_option('config', 'ncis'):
        ini_ncis = config['config']['ncis']  # List of cells NCIs
        do_log("- Cells NCIs: " + ini_ncis)
    else:
        ini_ncis = '-'

    if config.has_option('config', 'pcis'):
        ini_pcis = config['config']['pcis']  # List of cells PCIs
        do_log("- Cells PCIs: " + ini_pcis)
    else:
        ini_pcis = '-'

    if config.has_option('config', 'input_file'):
        ini_input_file = config['config']['input_file']  # Input log file to be parsed
        do_log("- Input log file: " + ini_input_file)
    else:
        ini_input_file = '-'

    if config.has_option('config', 'count_file'):
        ini_count_file = config['config']['count_file']  # Output file to write counting results
        do_log("- Output count file: " + ini_count_file)
    else:
        ini_count_file = '-'

    # Check input log file name in config file
    if not ini_input_file or ini_input_file == '-':
        print("ERROR: invalid input log file name")
        sys.exit()

    # Check output count file name in config file
    if not ini_count_file or ini_count_file == '-':
        print("ERROR: invalid output count file name")
        sys.exit()

    do_log("")

    if count_path_mnt:

        do_log("Running bash command to test mounted folder: " + mnt_test)
        do_log("")

        bash_res = run_bash(mnt_test)

        do_log("Result: " + bash_res)
        do_log("")

        if count_path_mnt in bash_res and mnt_remote_folder in bash_res:
            do_log("Folder: " + count_path_mnt)
            do_log("mounted correctly on: " + mnt_remote_folder)
            do_log("")
        else:
            do_log("ERROR:")
            do_log("Folder: " + count_path_mnt)
            do_log("not mounted to: " + mnt_remote_folder)
            do_log("Plase run:")
            do_log("sudo systemctl daemon-reload")
            do_log("sudo mount -a")
            sys.exit()

    if count_path_mnt:

        count_file_name_mnt = Path(count_path_mnt, ini_count_file)

        try:
            os.remove(count_file_name_mnt)

            do_log("File: " + str(count_file_name_mnt))
            do_log("already exists: deleting it ...")
            do_log("")

        except FileNotFoundError:
            pass

        finally:

            do_log("Creating File:" + str(count_file_name_mnt))
            do_log("")

            with open(count_file_name_mnt, 'a') as h_file:
                h_file.write("log-parser main " + version + '\n')
                h_file.write("Current datetime: " + datetime.now().isoformat() + '\n')
                h_file.write("Input log file name: " + ini_input_file + '\n')
                h_file.write("Output log file name: " + ini_count_file + '\n')
                h_file.write("\n")
                h_file.flush()

    count_file_name_loc = count_path_loc + ini_count_file

    try:
        os.remove(count_file_name_loc)

        do_log("File:")
        do_log(count_file_name_loc)
        do_log("already exists: deleting it")
        do_log("")
    except FileNotFoundError:
        pass

    inp_file_name = inp_path + ini_input_file

    do_log("Files:")
    do_log("- Input log file path: " + inp_path)
    do_log("- Input log file: " + ini_input_file)
    do_log("- Output count file path (mount): " + (count_path_mnt if count_path_mnt else "---"))
    do_log("- Output count file path (local): " + count_path_loc)
    do_log("- Output count file: " + ini_count_file)
    do_log("- Output log path: " +  log_path)
    do_log("- Output log file: " + log_file_name)

    do_log("")

    try:
        asyncio.run(main(tcp_port))
    except KeyboardInterrupt:
        sys.stdout.write('\x1b[2K\n')
        print("... script gracefully stopped")
    finally:
        log_file.close()
