На главную Наши проекты:
Журнал   ·   Discuz!ML   ·   Wiki   ·   DRKB   ·   Помощь проекту
ПРАВИЛА FAQ Помощь Участники Календарь Избранное RSS
msm.ru
! Правила раздела Visual C++ / MFC / WTL (далее Раздела)
1) На Раздел распространяются все Правила Форума.
2) Перед тем, как создать новый топик, убедитесь, что Вы читали Правила создания тем в Разделе.
3) Вопросы, не связанные с программированием (настройки MS Visual Studio, книги, библиотеки и т.д.),
обсуждаются в разделе C/C++: Прочее
4) Вопросы разработки .NET (Windows Form, C++/CLI и т.п.) приложений на Visual C++/C# обсуждаются в разделе .NET.
5) Нарушение Правил может повлечь наказание со стороны модераторов.

Полезные ссылки:
user posted image FAQ Раздела user posted image Обновления для FAQ Раздела user posted image Поиск по Разделу user posted image MSDN Library Online
Модераторы: ElcnU
Страницы: (2) [1] 2  все  ( Перейти к последнему сообщению )  
> COM-порт и таймауты
    Доброго дня всем!
    Спешу к вам за помощью.

    Вопрос касается организации приема/передачи данных из COM-порта в использованием таймаутов.

    Перед выполнением нижеследующей функции я устанавливаю следующие параметры порта:
    ExpandedWrap disabled
      // установка параметров порта
      GetCommState(hPort, &dcbPort);
          dcbPort.BaudRate = CBR_9600;
          dcbPort.fBinary  = TRUE;
          dcbPort.fNull    = FALSE;
          dcbPort.Parity   = NOPARITY;
          dcbPort.ByteSize = 8;
          dcbPort.StopBits = ONESTOPBIT;
      SetCommState(hPort, &dcbPort);
      // таймауты не использую


    Вначале приведу работающий кусок кода из моей программы без использования таймаутов:

    ExpandedWrap disabled
      void OnDOWNLOAD()
      {
          int count = 0;
       
          unsigned char dataInPort[6];    // буфер для записи
          unsigned char dataFromPort[256];// буфер для чтения
       
          for ( int i=0 ; i<6 ; i++ )
              dataInPort[i] = 0;
          for ( int j=0 ; j<256 ; j++ )
              dataFromPort[j] = 0;
       
          HANDLE      hPort   = {0};  // дескриптор порта
          DCB     dcbPort = {0};  // свойства порта
          COMMTIMEOUTS    tm  = {0};  // таймауты
              
          OVERLAPPED  over    = {0};  // параметры асинхронной передачи
          COMSTAT     state   = {0};  // параметры приема/передачи
       
          CFile magEvents;        // файл, куда записываются данные
       
          DWORD dwWrite   = 0;
          DWORD dwRead    = 0;
          DWORD readed    = 0;
          DWORD dwWait    = 0;
          
          DWORD errorMask = 0;
          DWORD eventMask = 0;
       
          for ( ; ; )
          {
              dataInPort[0] = 0x01;
              dataInPort[1] = 0x18;
              dataInPort[2] = 0x00;
              dataInPort[3] = 0x00;
              dataInPort[4] = 0x81;
              dataInPort[5] = 0xDF;
       
              HANDLE hPort; // дескриптор порта
              DCB  dcbPort; // дескриптор структуры свойств порта
              
              OVERLAPPED over = {0};
              COMSTAT state   = {0};
              
              CFile magEvents;
              
              DWORD errorMask = 0;
       
              hPort = CreateFile("COM1", GENERIC_WRITE | GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
              if ( hPort==INVALID_HANDLE_VALUE )
              {
                  AfxMessageBox("Порт не настроен!\nДля настройки воспользуйтесь меню:\n\"Настройки\\COM-порт\".");
                  CloseHandle(hPort);
                  break;
              }
              else
              {
                  PurgeComm(hPort, PURGE_TXABORT|PURGE_RXABORT|PURGE_TXCLEAR|PURGE_RXCLEAR);
                  GetCommState(hPort, &dcbPort);
                  
                  // запись в порт
                  dwWrite = 0;
                  WriteFile(hPort, &dataInPort, 6, &dwWrite, &over);
                  
                  // чтение из порта
                  SetCommMask(hPort, EV_RXCHAR);
                  over.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
                  eventMask = 0;
          
                  int wce = WaitCommEvent(hPort, &eventMask, &over);
          
                  if ( (!wce) && (GetLastError()==ERROR_IO_PENDING) )
                      WaitForSingleObject(hPort, 500/*см. ниже "вопрос 1"*/);
          
                  if ( eventMask & EV_RXCHAR )
                  {
                      dwRead  = 0;
                      readed  = 0;
          
                      ClearCommError(hPort, &errorMask, &state);
                      ReadFile(hPort, &dataFromPort, state.cbInQue, &dwRead, &over);
                  
                      GetOverlappedResult(hPort, &over, &readed, FALSE);
          
                      ResetEvent(over.hEvent);
                      
                      unsigned short registerCount = 0;
                      unsigned short     byteCount = 0;
                      
                      registerCount = (dataFromPort[4] <<8 | dataFromPort[5]);
                          byteCount = (dataFromPort[2] <<8 | dataFromPort[3]);
                      
                      if ( registerCount==0 && count==0 )
                      {
                          AfxMessageBox("Данных в журнале нет");
                          CloseHandle(hPort);
                          break;
                      }
       
                      if ( registerCount == 0 )
                      {
                          AfxMessageBox("Журнал сохранен");
                          CloseHandle(hPort);
                          break;
                      }
       
                      count += 1;
       
                      ///////////////////////// сортировка данных и сохранение их в файл
                      //не обращайте внимания на этот кусок
                      unsigned char* data;
          
                      data = new unsigned char[255];
          
                      for ( int count=0 ; count<=( (int)byteCount-2 ) ; count++ )
                          data[count] = dataFromPort[count+6];
          
                      magEvents.Open("events.mag", CFile::modeCreate | CFile::modeNoTruncate | CFile::modeWrite, NULL);
                      magEvents.Seek(0, CFile::end);
                      magEvents.Write(data, ((int)byteCount-2) );
                      magEvents.Close();
                  
                      delete data;
                      //////////////////////////////////////////////////////////////////
                  }
                  
                  PurgeComm(hPort, PURGE_TXABORT|PURGE_RXABORT|PURGE_TXCLEAR|PURGE_RXCLEAR);
                  CloseHandle(hPort);
              }
          }
      }




    Вопрос 1:
    У меня данных максимум может идти до 256 байт,
    так вот за эти 500 мс я успеваю (в случае прихода всех 256 байт) полностью их успеть прочитать при скорости 9600.
    Вопрос 2: (вытекает из вопроса 1)
    использование вышеприведенной реализации функции с использованием 500мс (см. вопрос 1) не совсем то, что мне надо (хотя все
    работает корректно!!). Появилась на необходимость использования таймаутов и с ними я никак :( ваще никак не могу раобраться
    . Помогите в етом плиз.

    Там де я заполняю структуру DCB там же я устанавливаю значения тайимаутов, например такие:

    ExpandedWrap disabled
      // установка таймаутов (для скорости 9600)
      GetCommTimeouts(hPort, &tm);
          tm.ReadIntervalTimeout      = 4;
          tm.ReadTotalTimeoutConstant = 100;
          tm.ReadTotalTimeoutMultiplier   = 0;
          tm.WriteTotalTimeoutConstant    = 0;
          tm.WriteTotalTimeoutMultiplier  = 4;
      SetCommTimeouts(hPort, &tm);


    вот я установил....и что дальше я не могу понять, как мне переделать мою функцию чтобы при превышении вот этих таймаутов
    заканчивалась процедура записи/чтения.

    Нужно мне все это для того, чтобы:
    1. если девайс не отвечает, программа не висла. Типа ждем заданный интервал времени и если байт не появляется, выдаем нечто
    типа: m_static.SetWindowText("Девайс не отвечает"); и повторять это сообщение надо до тех пор пока девайс не станет отвечать.
    В случае, если девайс отвечает и идет обмен данными, выводить типа: m_static.SetWindowText("Девайс в работе");
    2. ну и для того, чтобы удовлетворить моего руководителя. :) . Хотя пункт 1 - это для самоудовлетворения :)
      неужели никто ничего не подскажет ? :)
      понятно что лень даже до конца читать пост :)

      ps: не использовать таймауты нельзя :( надо один раз просто с этим разобраться !
        Хм, в чем проблема-то? Если функция SetPortTimeouts() отработала успешно, а за положенное таймаутами время из порта не был получен байт/пакет (см.в MSDN), то функция ReadFile() вернет FALSE. Вот и проверяй результат - коли ЛОЖЬ, то девайс тебе не ответил, коли ПРАВДА, то обмен идет успешно.
        Кстати, зачем ты в каждой итерации открываешь и закрываешь порт? Имхо достаточно сделать это один раз перед началом обмена и закрыть после окончания обмена.
          Вопрос 1)
          Да уж...корректного я тут не увидел. Уж прости :whistle:

          При вызове WaitCommEvent ты теряешь статус завершённости асинх. операции записи.
          А запись вообще завершилась? что-то я этого не вижу...
          А если она отвалила с ошибкой записи последнего байта, то чего-же ты будешь ждать на WaitCommEvent?
          На той стороне понимают незавершённые запросы? Да, в твоём случае ты легко можешь пролететь
          по всему твоему циклу, и повторить запись. Но это разве корректно???
          Функция WaitCommEvent в оверлаппед-структуре требует наличия manual-reset эвента, а ты какой создал?
          При ожидании на WaitForSingleObject(hPort, 500) ты ждёшь завершения WaitCommEvent? Ты в этом
          точно уверен??? А может PFILE_OBJECT::hEvent всё ещё настроен после вызова WriteFile?
          Шансы зайти в обработку условия (eventMask & EV_RXCHAR) согласен огромные, но c учётом ранее написанного -
          они разве 100%?
          Такое смелое указывание state.cbInQue при возможном превышении sizeof( dataFromPort) c хоть и малой,
          но возможной долей вероятности схлопотать ошибку доступа на запись - это тож корректно?
          Указывание употреблённой, и не прочищенной оверлаппед-структуры (с непонятным статусом hEvent) в ReadFile - это корректно?
          GetOverlappedResult с указанным флагом FALSE (не дожидатся завершения асинх. операции) тут для чего? Ты разве
          определяешь, что операция ещё ERROR_IO_INCOMPLETE? А уж последующий вызов ResetEvent на возможно не установленный,
          и находящийся в прогрессе выполнения ReadFile - это вообще шедевр...

          Помойму ты достаточно плохо понимаешь асинхронные вызовы. Почиталб что-нить на эту тему... :yes:

          Вопрос 2)
          Насчет таймаутов - внимательно прочти следующее:
          Цитата

          If an application sets ReadIntervalTimeout and ReadTotalTimeoutMultiplier to MAXDWORD and sets ReadTotalTimeoutConstant to a value greater than zero and less than MAXDWORD, one of the following occurs when the ReadFile function is called:

          If there are any characters in the input buffer, ReadFile returns immediately with the characters in the buffer.
          If there are no characters in the input buffer, ReadFile waits until a character arrives and then returns immediately.
          If no character arrives within the time specified by ReadTotalTimeoutConstant, ReadFile times out.
            я рад за тебя, AlexSm, что ты такой умный....только вот так вот разговаривать не надо ни с кем!
            Чем вот так пальцы веером пускать, лучше бы помог разобраться как все это сделать действительно корректно и правильно!!


            Может у тебя опыта в этом вопросе и достаточно, то я за это берусь впервые... и я не видел ни одного человека который бы что нибудь с первого раза сделал бы правильно.
              AlexSm
              Цитата AlexSm @
              Функция WaitCommEvent в оверлаппед-структуре требует наличия manual-reset эвента, а ты какой создал?
              так он , вроде , такой и создал... :rolleyes:
              emp по-моему, не стоит смешивать таймауты и асинхронные операции .... надо использовать либо то, либо другое.
                Цитата emp @
                я рад за тебя, AlexSm

                Ты лучше сделай поиск с выражением: COM+GetCommState - и тебе всё станет ясно, я надеюсь; можно ещё в FAQ заглянуть!

                Также, >>>ещё<<< можно глянуть на сообщение под №8 ;D
                  2AlexSm: В прошлом своем посте я погорячился раньше времени, сорри за это.

                  товарищи я проанализировал все ваши советы и вот к чему пришел (насколько это правильно с вашей точки зрения и чего не хватает мне?). Хто возьмется проверить код и подсказать исчо чего нить ? %)

                  ExpandedWrap disabled
                    void OnDOWNLOAD()
                    {
                        int count = 0;
                     
                        unsigned char dataInPort[6];    // буфер для записи
                        unsigned char dataFromPort[256];// буфер для чтения
                     
                        for ( int i=0 ; i<6 ; i++ )
                            dataInPort[i] = 0;
                        for ( int j=0 ; j<6 ; j++ )
                            dataFromPort[j] = 0;
                     
                        HANDLE          hPort   = {0};  // дескриптор порта
                        DCB             dcbPort = {0};  // свойства порта
                        COMMTIMEOUTS    tm      = {0};  // таймауты
                            
                        OVERLAPPED  over    = {0};      // параметры асинхронной передачи
                        COMSTAT     state   = {0};      // параметры приема/передачи
                     
                        CFile magEvents;                // файл, куда записываются данные
                     
                        DWORD dwWrite   = 0;            // фактическое число записанных байт
                        DWORD dwRead    = 0;            // фактическое число прочитанных байт
                        DWORD dwWait    = 0;
                        DWORD readed    = 0;
                        
                        DWORD eventMask = 0;
                        DWORD errorMask = 0;    
                        DWORD timeout = 500;
                     
                        // открытие порта
                        hPort = CreateFile("COM1",
                                            GENERIC_WRITE | GENERIC_READ,
                                            0,
                                            NULL,
                                            OPEN_EXISTING,
                                            FILE_FLAG_OVERLAPPED,
                                            NULL);
                     
                        if ( hPort==INVALID_HANDLE_VALUE )
                            AfxMessageBox("Ошибка инициализации порта");
                        else
                        {
                            // очистка порта от старой информации
                            int pc = PurgeComm(hPort, PURGE_TXABORT|PURGE_RXABORT|PURGE_TXCLEAR|PURGE_RXCLEAR);
                            if ( pc==0 )
                                AfxMessageBox("Ошибка PurgeComm()");
                            else
                            {
                                int gcs = GetCommState(hPort, &dcbPort);
                                if ( gcs==0 )
                                    AfxMessageBox("Ошибка GetCommState()");
                                else
                                {
                                    dcbPort.BaudRate = CBR_9600;
                                    dcbPort.fBinary  = TRUE;      // двоичный режим обмена
                                    dcbPort.fNull    = FALSE;     // нулевые байты не отбрасываются при передаче
                                    dcbPort.Parity   = NOPARITY;  // бит четности отсутствует
                                    dcbPort.ByteSize = 8;         // число бит в байте
                                    dcbPort.StopBits = ONESTOPBIT;// один стоп-бит
                     
                                    int scs = SetCommState(hPort, &dcbPort);
                                    if ( scs==0 )
                                        AfxMessageBox("Ошибка SetCommState()");
                                    else
                                    {
                                        int gct = GetCommTimeouts(hPort, &tm);
                                        if ( gct==0 )
                                            AfxMessageBox("Ошибка GetCommTimeouts()");
                                        else
                                        {
                                            tm.ReadIntervalTimeout          = 4;
                                            tm.ReadTotalTimeoutConstant     = 100; // из диалога настроек порта
                                            tm.ReadTotalTimeoutMultiplier   = 0;
                                            tm.WriteTotalTimeoutConstant    = 0;
                                            tm.WriteTotalTimeoutMultiplier  = 4;
                     
                                            int sct = SetCommTimeouts(hPort, &tm);
                                            if ( sct==0 )
                                                AfxMessageBox("Ошибка SetCommTimeouts()");
                                            else
                                            {
                                                dataInPort[0] = 0x01;
                                                dataInPort[1] = 0x18;
                                                dataInPort[2] = 0x00;
                                                dataInPort[3] = 0x00;
                                                    unsigned short controlSum = 0;
                                                    controlSum = calcCRC((unsigned char *)dataInPort, 4);
                                                dataInPort[4] = (controlSum >> 8);
                                                dataInPort[5] = (unsigned char)controlSum;
                     
                                                // запись
                                                over.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
                                                if ( over.hEvent == NULL )
                                                    AfxMessageBox("Ошибка CreateEvent()");
                                                else
                                                {
                                                    BOOL scm = SetCommMask(hPort, EV_RXCHAR);
                                                    if ( scm==FALSE )
                                                        AfxMessageBox("Ошибка SetCommMask()");
                                                    else
                                                    {
                                                        dwWrite = 0;
                                                        dwWait  = 0;
                     
                                                        BOOL wf = WriteFile(hPort, &dataInPort, 6 ,&dwWrite, &over);
                                                        if ( wf==TRUE )
                                                            AfxMessageBox("Ошибка WriteFile()");
                                                        else
                                                        {
                                                            int wce = WaitCommEvent(hPort, &eventMask, &over);
                                                            if ( (!wce) && (GetLastError()==ERROR_IO_PENDING) )
                                                            {
                                                                DWORD wfso = WaitForSingleObject(hPort, timeout);
                                                                switch ( wfso )
                                                                {
                                                                case WAIT_ABANDONED:
                                                                    {
                                                                        // не знаю что это такое
                                                                        AfxMessageBox("WaitForSingleObject: abandon");
                                                                    } break;
                                                                case WAIT_OBJECT_0:
                                                                    {
                                                                        // не разобрался для чего это надо
                                                                        AfxMessageBox("че-та происходит");
                                                                    } break;
                                                                case WAIT_TIMEOUT:
                                                                    {
                                                                        // считываем данные, если они есть
                                                                        if ( eventMask & EV_RXCHAR )
                                                                        {
                                                                            dwRead  = 0;
                                                                            readed  = 0;
                     
                                                                            int cce = ClearCommError(hPort, &errorMask, &state);
                                                                            if ( cce==0 )
                                                                                AfxMessageBox("Ошибка ClearCommError()");
                                                                            else
                                                                            {
                                                                                BOOL rf = ReadFile(hPort, &dataFromPort, state.cbInQue, &dwRead, &over);
                                                                                if ( rf==FALSE )
                                                                                    AfxMessageBox("Ошибка ReadFile()");
                                                                                else
                                                                                {
                                                                                    if ( dwRead==0 )
                                                                                    {
                                                                                        // правильно?
                                                                                        AfxMessageBox("таймаут истек");
                                                                                    }
                                                                                    else
                                                                                    {
                                                                                        // типа тут можно начинать обработку принятых данных?
                                                                                        AfxMessageBox("можно начинать обработку данных...");
                                                                                    }
                                                                                }
                                                                            }
                                                                        }
                                                                        else
                                                                        {
                                                                            //где правда ?
                                                                            AfxMessageBox("device not response");
                                                                        }
                                                                    } break;
                                                                case  WAIT_FAILED:
                                                                    {
                                                                        AfxMessageBox("Ошибка WaitForSingleObject()");
                                                                    } break;
                                                                }
                                                            }
                                                            else
                                                            {
                                                                AfxMessageBox("Ошибка WaitCommEvent()");
                                                            }
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                     
                        CloseHandle(over.hEvent);
                        CloseHandle(hPort);
                    }


                  протестил вышенаписанное - все работает так как надо, но может есть замечания о некорректности?
                  и мне неясны з МСДН некоторые вещи (в коде означены комментариями)
                    olddiller: Да, c режимом эвента я переборщил :lol:

                    emp: Я тебя пытался подтолкнуть к мысле, что по предоставленному тобой коду, тут вполне мона обойтись без асинхронной возможности работы через оверлаппед интерфейс (тем более если ты не до конца понимаешь как оно работает). У тебя шаги выполняются последовательно, и никаких дополнительных действий (помимо работы с портом) ты не предпринимаешь (реакция прекращения работы потока/приложения/etc). Ну дык и зачем использовать оверлаппед? Сделай проще, синхронно:
                    1) Открыть порт без оверлаппед
                    2) Настроить таймауты с учетом цитаты из мсдн, что я приводил выше
                    3) Вызвать синхронный WriteFile
                    4) В случае успеха вызвать ReadFile
                    5) После ReadFile имеешь либо нормальный ответ, либо ошибку чтения. Причины уже отдельно определяешь...
                    6) Закрываешь порт

                    В случае, если считывать нада порциями, либо нада по самим данным определять конец посылки - в шаги 4-5 вставляешь обработку этого. Возможно по ClearCommError/WaitCommEvent/etc.
                      сделаю небольшие пояснения для чего мне это все надо:

                      1. во-первых, я хочу разобраться с асинхронным вводом-выводом
                      2. во-вторых, программа будет работать следующим образом:
                      до завершения работы программы будет крутиться в отдельном потоке вышеприведенный код и по расшифрованным принятым данным (они будут отображаться на далоге) пользователем будут приниматься те или иные действия, в число которых будет приостанавливаться этот постоянный цикл опроса девайса.
                      3. да и в-третьх, почму асинхронный? да потому, что каждый раз ожидая ответа от девайса я не знаю сколько данных придет (от 12 до 245 байт - в этих пределах)

                      еще раз прошу, лучше помогите разобраться с асинхронным вводом/выводом раз и навсегда :)
                      этому желанию и служит же данный форум, правда ?
                        1) Всёж таймауты надоб так:
                        ExpandedWrap disabled
                          tm.ReadTotalTimeoutConstant = 500; // Таймаут в мс на чтение по ReadFile
                          tm.ReadIntervalTimeout = MAXDWORD; // Между байтами может быть какой угодно интервал

                        Ты вроде 500 мс хотел...

                        2) После успешного WriteFile (без ухода в состояние IO_PENDING) у тебя будет диалог с ошибкой :D
                        3) Перепутал WAIT_OBJECT_0 и WAIT_TIMEOUT. WAIT_OBJECT_0 -> Произошло событие на указанном хэндле
                        4) &dataFromPort - амперсанд тут зачем?
                        5) То, что ReadFile с параметром оверлаппеда вернула FALSE - ещё не значит что произошла уж такая критичная ошибка. Нада проверить на ERROR_IO_PENDING

                        ЗЫ. После основного вызова асинх. операции проверяй что ты действительно попал в состояние IO_PENDING.
                        И далее можешь делать либо синхронно:
                        ExpandedWrap disabled
                          BOOL fOK = GetOverlappedResult(...., TRUE);
                          или
                          BOOL fOK = WaitForSingleObject(..., timeout) == WAIT_OBJECT_0;

                        либо асинхронно:
                        ExpandedWrap disabled
                          BOOL fOK;
                          while( !(fOK = GetOverlappedResult(..., FALSE)) && GetLastError() == ERROR_IO_INCOMPLETE)
                          {
                             ; // что-то делаешь ещё.
                          }
                          или
                          DWORD dwWaitCode;
                          while( (dwWaitCode = WaitForSingleObject( ..., 5 /* ждать по 5 мс */)) == WAIT_TIMEOUT)
                          {
                             ; // что-то делаешь ещё
                          }


                        Добавлено
                        ЗЫЗЫ. И воспользуйся поиском по совету SVK. Тем дофига было о работе с ком-портами.Может чего интересного подчерпнёшь...
                          я бы топик не создавал если бы тчо то нашел
                          да и кроме того я вижу что ты тоже не совсем понимаешь... не буду комментррировать что именно


                          топик не закрываю!
                          жду предложений еще
                          народ поднапрягите умы !! :)
                            Цитата emp @
                            я бы топик не создавал если бы тчо то нашел
                            да и кроме того я вижу что ты тоже не совсем понимаешь... не буду комментррировать что именно

                            Ооо...Вон оно как...Уж прости, что отнял у тебя драгоценное время. Больше тут писАть не буду. :huh:
                              мдя... для всех это такая тяжелая тема?

                              ну спасибки всем за помощь!

                              Добавлено
                              а может есть кто продвинутый, кто может подсказать как исправить мой код, а не советовать "поищи в инете" или "ты в этом ничего не понимаешь, даже не берись за это"... а я хочу понимать! и мне надо разобраться

                              жду советов...
                                почему нету монстров в етом вопросе ? :(
                                1 пользователей читают эту тему (1 гостей и 0 скрытых пользователей)
                                0 пользователей:


                                Рейтинг@Mail.ru
                                [ Script execution time: 0,0538 ]   [ 15 queries used ]   [ Generated: 5.04.26, 14:08 GMT ]