Микро-чат на ATcl

Микро-чат на ATcl

2 февраля 2023, 15:32
Maxim Kuznetsov
3
112

А давайте сейчас быстренько набрасаем чатик. Задействуем pub/sub механизмы: примерно так работают всякие копировщики, сигнальные сервисы и мониторинги.

Постановка задачи

делаем микро-чат:

  • это будет индикатор
  • с единственным (опциональным) параметром «NAME» - имя с общем чате
  • в поле коментариев отображается история чата (кто что говорил)
  • интерфейс пользователя делаем на нативном MQL: поле для ввода текста и кнопка «send»
  • при нажатии на кнопку send отправляем введённый текст всем другим пользователям

Подоснова

для сетевого взаимодействия возьмём протокол MQTT https://mqtt.org/ , и его клиентскую часть https://chiselapp.com/user/schelte/repository/mqtt/wiki?name=Tcl+MQTT+client (там есть ещё и брокер, но пока не про него). Этот пакет уже включен в ATcl

В двух словах про MQTT - клиенты могут публиковать произвольные данные в темы организованные иерархическим образом. И могут подписываться на получение данных из этой иерархии. Получается быстрая эффективная связь один-ко-многим. Это действительно очень быстро, это используется в IoT например для передачи показаний датчиков.

в качестве сервера (пока, для тестов) используем тестовый сервер test.mosquitto.org см. https://www.mosquitto.org/

будем использовать корневой топик atcl/8891/talk - это произвольное имя, просто чтобы не запутался случайно с другими темами тестового сервера. Все публикации будут с таким «корнем»

Реализация

Всю сетевую часть делаем на tcl и предоставляем на уровень MQL несколько функций:

  • start - начать работу
  • send имя текст - передать текст от заданного имени. (в терминах mqtt - записать «текст» в тему «имя»)
  • stop - завершить работу
  • receive - получить накопившиеся сообщения. MQL будет просто периодично её дёргать и смотреть что получено

процедура receive будет возвращать список сообщений, первым полем будет код:

  • 0 - соединение потеряно
  • 1 - соединение установлено
  • 2 - получено новое сообщение
  • 3 - получено сообщение ранее сохранённое на сервере

На Tcl 60 строчек всё это делают :

package require mqtt
 
set SERVER "test.mosquitto.org" ;# сервер для тестов
set MSGS {}             ;# сообщения для MQL
set MQTT {}             ;# дескриптор mqtt
set root "atcl/8891/talk" ;# топик
set connected 0 ;# соединён/нет
 
proc receive {} {
        set ret $::MSGS
        set ::MSGS {}
        set ret
}
proc start {} {
        set ::MQTT [ mqtt new ]
        $::MQTT connect "" $::SERVER
        $::MQTT subscribe "\$SYS/local/connection" [ list OnConnection ]
        $::MQTT subscribe "$::root/#" [ list OnMessage ]
}
proc stop {} {
        if { $::MQTT != {} } {
                catch { $::MQTT destroy }
                catch {rename $::MQTT {} }
                set ::MQTT {}
                set connected ""
        }
}
proc send { who args } {
        if { !$::connected } {
                return
        }
        # payloadformat указывает что передаём текст в utf-8
        $::MQTT publish -properties {PayloadFormatIndicator 1} $::root/$who [ join $args " " ] 0 1
}
proc OnConnection { topic message retain args } {
        set state [ dict get $message state ]
        if { $state == "connected" } {
                set ::connected 1
                ToMQL 1
        } elseif { $state == "disconnected" } {
                set ::connected 0
                ToMQL 0
        }
}
proc OnMessage { topic message retain args } {
        set path [ split $topic "/" ]
        set who [ lindex $path end ]
        if { $retain } {
                ToMQL 3 $who $message
        } else {
                ToMQL 2 $who $message
        }
}
proc ToMQL { code args } {
        lappend ::MSGS [ list $code {*}$args ]
}

На MQL втрое больше :

#property indicator_chart_window
 
#resource "talk.tcl" as string talk_tcl;
input string _NAME="";
string NAME;
#include <ATcl/ATcl.mqh>
ATcl *tcl=NULL;
string TextInput=".talk.Input";
string ButtonSend=".talk.Send";
 
string Prepare(string name) {
   string out="";
   int len=StringLen(name);
   for(int pos=0;pos<len;pos++) {
      ushort ch=StringGetCharacter(name,pos);
      if (ch=='/' || ch=='*' || ch=='+' || ch=='\\' || ch=='#') {
         continue;
      }
      out=out+ShortToString(ch);
   }
   StringTrimLeft(out);StringTrimRight(out);
   return out;
}
int OnInit()
{
   // разборка с аргументами
   // NAME - не пустая строка, без спец.символов
   NAME=Prepare(_NAME);
   if (NAME=="") {
      NAME=Prepare(AccountInfoString(ACCOUNT_NAME));
      if (NAME=="") {
         Alert("NAME wrong");
         return INIT_FAILED;
      }
   }
   // инициализация библиотеки, создание интерпретатора
   if (ATcl_OnInit()!=INIT_SUCCEEDED) {
      return INIT_FAILED;
   }
   tcl=new ATcl();
   if (tcl==NULL || tcl.OnInit()!=INIT_SUCCEEDED) {
      return INIT_FAILED;
   }
   // выполняем скрипт из ресурса
   if (tcl.Eval(talk_tcl)!=TCL_OK) {
      PrintFormat("script error: %s",tcl.StringResult());
      return INIT_FAILED;
   }
   // запускаем говорилку
   if (tcl.Call(tcl.Obj("start"))!=TCL_OK) {
      Alert("start failed "+tcl.StringResult());
      return INIT_FAILED;
   }
   EventSetMillisecondTimer(30);
   ObjectCreate(0,TextInput,OBJ_EDIT,0,0,0);
   ObjectSetInteger(0,TextInput,OBJPROP_XDISTANCE,300);
   ObjectSetInteger(0,TextInput,OBJPROP_YDISTANCE,2);
   ObjectSetInteger(0,TextInput,OBJPROP_XSIZE,400);
 
   ObjectCreate(0,ButtonSend,OBJ_EDIT,0,0,0);
   ObjectSetInteger(0,ButtonSend,OBJPROP_XDISTANCE,700);
   ObjectSetInteger(0,ButtonSend,OBJPROP_YDISTANCE,2);
   ObjectSetInteger(0,ButtonSend,OBJPROP_XSIZE,60);
   ObjectSetString(0,ButtonSend,OBJPROP_TEXT,"Send");
 
   return(INIT_SUCCEEDED);
}
void OnDeinit(const int reason)
{
   if (tcl!=NULL) {
      tcl.Call(tcl.Obj("stop"));
      tcl.OnDeinit(reason);
      delete tcl;
      tcl=NULL;
   }
   EventKillTimer();
   ObjectsDeleteAll(0,".talk.");
   ChartRedraw(0);
}
void OnTimer()
{
   tcl.Update();
   // получаем список сообщений
   if (tcl.Call(tcl.Obj("receive"))!=TCL_OK) {
      Alert("receive failed");
      ExpertRemove();
      return;
   }
   Tcl_Obj list=tcl.Result();
   if (list==0) return;
   // разбираем все сообщения
   tcl.Ref(list);
   int len=tcl.Count(list);
   for(int pos=0;pos<len;pos++) {
      Tcl_Obj msg=tcl.Index(list,pos);
      if (msg==0) continue;
      tcl.Ref(msg);
      long code=tcl.Long(msg,0);
      switch ((int)code) {
         case 0: // disconnect
            AppendToComment("** disconnect **"); 
         break;
         case 1: // connect
            AppendToComment("** connect **");    
         break;
         case 2: // message  
            { 
            string who=tcl.String(msg,1);
            string text=tcl.String(msg,2);
            AppendToComment(StringFormat("%s : %s",who,text)); 
            }
         break;
         case 3: // retained message  
            { 
            string who=tcl.String(msg,1);
            string text=tcl.String(msg,2);
            AppendToComment(StringFormat("( %s : %s )",who,text)); 
            }
         break;
      }
      tcl.Unref(msg);
   }
   tcl.Unref(list);
}
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
{
   tcl.Update();
   return(rates_total);
}
void OnChartEvent(const int id,const long& lparam,
                  const double& dparam,
                  const string& sparam)
{
   if (id==CHARTEVENT_OBJECT_ENDEDIT && sparam==TextInput) {
      OnInput();
      ChartRedraw(0);
   } else if (id==CHARTEVENT_OBJECT_CLICK && sparam==ButtonSend) {
      OnInput();
      ObjectSetInteger(0,ButtonSend,OBJPROP_STATE,0);
      ChartRedraw(0);
   }
}                  
void OnInput()
{
   string text=ObjectGetString(0,TextInput,OBJPROP_TEXT);
   StringTrimLeft(text);StringTrimRight(text);
   if (text=="") return;
   tcl.Call(tcl.Obj("send"),tcl.Obj(NAME),tcl.Obj(text));
   ObjectSetString(0,TextInput,OBJPROP_TEXT,"");
}
void AppendToComment(string text)
{
   string comment=ChartGetString(0,CHART_COMMENT);
   string lines[]; StringSplit(comment,'\n',lines);
   if (ArraySize(lines)>20) {
      ArrayRemove(lines,ArraySize(lines)-20);
      int total=ArraySize(lines);
      comment=lines[0];
      for(int pos=1;pos<total;pos++) {
         comment=comment+"\n"+lines[pos];
      }
   }
   comment=comment+"\n"+text;
   Comment(comment);
}

Результат

И можем в нём даже пообщаться

Вот так вот за часик с перекуром, сделали конкурента телеграм и основу сервиса сигналов и копирования сделок

Всем чмоки в этом чате :-)

Скачать библиотеку ATcl отсюда https://uploadfiles.in/6q9 или https://dfiles.eu/files/3sigiedi1 или https://www.filefactory.com/file/nmqp3op9f9q/atclsetup_1.01.exe

а также со страницы загрузки эскизного сайта http://atcl.unaux.com/download/

или следуя инструкциям, из репозитария проекта https://chiselapp.com/user/nektomk/repository/atcl-lib/home

Статьи на mql5.com про использование ATcl:


Файлы:
talk.zip  4 kb
Поделитесь с друзьями: