commit 660133c812931584d4c1968de8cc803ea18352e3 Author: Artem Prilutskiy <cyanide.burnout@gmail.com> Date: Wed Mar 23 20:08:20 2016 +0300 Initial import diff --git a/CallCapture/BrandMeisterBridge.cpp b/CallCapture/BrandMeisterBridge.cpp new file mode 100644 index 0000000..1f13fd6 --- /dev/null +++ b/CallCapture/BrandMeisterBridge.cpp @@ -0,0 +1,90 @@ +// Copyright 2015 by Artem Prilutskiy + +#include "BrandMeisterBridge.h" +#include <string.h> +#include <stdlib.h> +#include <syslog.h> +#include <stdio.h> + +#define ECHOLINK_DEFAULT_USER_NUMBER 1 +#define ECHOLINK_DEFAULT_CORD_NUMBER 10 +#define REGISTRY_CONFIGURATION_FILE "/opt/BrandMeister/Registry.cnf" + +BrandMeisterBridge::BrandMeisterBridge() : + proxy(ECHOLINK_DEFAULT_CORD_NUMBER), + store(REGISTRY_CONFIGURATION_FILE), + talker(NULL) +{ + +} + +BrandMeisterBridge::~BrandMeisterBridge() +{ + free(talker); +} + +// Interface methods for ModuleEchoLink + +const char* BrandMeisterBridge::getTalker() +{ + free(talker); + uint32_t number = proxy.getTalkerID(); + + char call[LONG_CALLSIGN_LENGTH]; + char text[SLOW_DATA_TEXT_LENGTH]; + if ((number != 0) && + (store.getCredentialsForID(number, call, text))) + { + asprintf(&talker, "%s %s", call, text); + return talker; + } + + asprintf(&talker, "DMR ID: %d", number); + return talker; +} + +void BrandMeisterBridge::setTalker(const char* call, const char* name) +{ + if (*call == '*') + { + // Do not process conference call-sign + return; + } + + const char* delimiter = strpbrk(call, " -\n"); + if (delimiter != NULL) + { + // Remove characters after call-sign + size_t length = delimiter - call; + char* buffer = (char*)alloca(length + 1); + strncpy(buffer, call, length); + call = buffer; + } + + uint32_t number = store.getPrivateIDForCall(call); + if (number == 0) + { + syslog(LOG_INFO, "DMR ID for call-sign %s not found", call); + number = ECHOLINK_DEFAULT_USER_NUMBER; + } + + syslog(LOG_DEBUG, "Set talker DMR ID to %d", number); + proxy.setTalkerID(number); +} + +void BrandMeisterBridge::handleChatMessage(const char* text) +{ + // CONF Russian Reflector, Open 24/7, Contacts: rv3dhc.link@qip.ru * Call CQ / Use pauses 2sec * [28/500] + // R3ABM-L *DSTAR.SU DMR Bridge* + // UB3AMO Moscow T I N A O + // ->UA0LQE-L USSURIISK + + const char* delimiter; + if ((strncmp(text, "CONF ", 5) == 0) && + ((delimiter = strstr(text, "\n->")) != NULL)) + { + const char* call = delimiter + 3; + setTalker(call, NULL); + } +} + diff --git a/CallCapture/BrandMeisterBridge.h b/CallCapture/BrandMeisterBridge.h new file mode 100644 index 0000000..554bbe8 --- /dev/null +++ b/CallCapture/BrandMeisterBridge.h @@ -0,0 +1,28 @@ +// Copyright 2015 by Artem Prilutskiy + +#ifndef BRANDMEISTERBRIDGE_H +#define BRANDMEISTERBRIDGE_H + +#include "PatchCordProxy.h" +#include "UserDataStore.h" + +class BrandMeisterBridge +{ + public: + + BrandMeisterBridge(); + ~BrandMeisterBridge(); + + const char* getTalker(); + void setTalker(const char* call, const char* name); + void handleChatMessage(const char* text); + + private: + + PatchCordProxy proxy; + UserDataStore store; + char* talker; + +}; + +#endif \ No newline at end of file diff --git a/CallCapture/CallCapture.cpp b/CallCapture/CallCapture.cpp new file mode 100644 index 0000000..ada3df8 --- /dev/null +++ b/CallCapture/CallCapture.cpp @@ -0,0 +1,113 @@ +#include <stdlib.h> +#include <getopt.h> +#include <syslog.h> +#include <stdio.h> +#include <pcre.h> + +#include "PatchCordProxy.h" +#include "UserDataStore.h" + +#define VECTORS_COUNT 8 +#define DEFAULT_USER_NUMBER 1 + +int main(int argc, const char* argv[]) +{ + printf("\n"); + printf("CallCapture for BrandMeister DMR Master Server\n"); + printf("Copyright 2015 Artem Prilutskiy (R3ABM, cyanide.burnout@gmail.com)\n"); + printf("\n"); + + // Start up + + struct option options[] = + { + { "expression", required_argument, NULL, 'e' }, + { "connection", required_argument, NULL, 'c' }, + { "identity", required_argument, NULL, 'i' }, + { "link", required_argument, NULL, 'l' }, + { NULL, 0, NULL, 0 } + }; + + pcre* expression = NULL; + const char* file = NULL; + uint32_t number = 10; + + int position = 0; + const char* error = NULL; + + int selection = 0; + while ((selection = getopt_long(argc, const_cast<char* const*>(argv), "e:c:l:", options, NULL)) != EOF) + switch (selection) + { + case 'e': + expression = pcre_compile(optarg, 0, &error, &position, NULL); + break; + + case 'c': + file = optarg; + break; + + case 'i': + openlog(optarg, 0, LOG_USER); + break; + + case 'l': + number = atoi(optarg); + break; + } + + if ((expression == NULL) && + (error != NULL)) + { + printf("Error compiling regular expression: %s (at %d)\n", error, position); + return EXIT_FAILURE; + } + + if ((expression == NULL) || + (file == NULL)) + { + printf( + "Usage: %s" + " --expression <regular expression>" + " --connection <path to configuration file>" + " --link <link number>" + " [--identity <identity>]" + "\n", + argv[0]); + return EXIT_FAILURE; + } + + // Main + + UserDataStore store(file); + PatchCordProxy proxy(number); + + char* line = NULL; + size_t length = 0; + ssize_t read; + + while ((read = getline(&line, &length, stdin)) != EOF) + { + syslog(LOG_DEBUG, "%s", line); + + int vectors[VECTORS_COUNT]; + int count = pcre_exec(expression, NULL, line, length, 0, 0, vectors, VECTORS_COUNT); + if (count > 0) + { + const char* call; + pcre_get_substring(line, vectors, count, 1, &call); + + uint32_t number = store.getPrivateIDForCall(call); + if (number == 0) + number = DEFAULT_USER_NUMBER; + proxy.setTalkerID(number); + syslog(LOG_INFO, "*** Found call-sign: %s (ID: %d)", call, number); + + pcre_free_substring(call); + } + } + + pcre_free(expression); + + return EXIT_SUCCESS; +}; diff --git a/CallCapture/Makefile b/CallCapture/Makefile new file mode 100644 index 0000000..a2e3cb8 --- /dev/null +++ b/CallCapture/Makefile @@ -0,0 +1,40 @@ +OBJECTS = \ + PatchCordProxy.o \ + UserDataStore.o \ + CallCapture.o + +LIBRARIES = \ + mysqlclient \ + pcre + +DEPENDENCIES = \ + dbus-1 + +CXXFLAGS := -fno-implicit-templates -D__STDC_CONSTANT_MACROS +FLAGS := -g -rdynamic -fno-omit-frame-pointer -O3 -MMD $(foreach directory, $(DIRECTORIES), -I$(directory)) $(shell pkg-config --cflags $(DEPENDENCIES)) -DBUILD=\"$(BUILD)\" -DVERSION=$(VERSION) +LIBS := -lstdc++ $(foreach library, $(LIBRARIES), -l$(library)) $(shell pkg-config --libs $(DEPENDENCIES)) + +# PREREQUISITES = + +CC = gcc +CXX = g++ +COMPILER_VERSION_FLAG := $(shell expr `${CXX} -dumpversion | cut -f1-2 -d.` \>= 4.9) + +CFLAGS += $(FLAGS) -std=gnu99 + +ifeq ($(COMPILER_VERSION_FLAG), 1) + CXXFLAGS += $(FLAGS) -std=gnu++11 +else + CXXFLAGS += $(FLAGS) -std=gnu++0x +endif + +all: build + +build: $(PREREQUISITES) $(OBJECTS) + $(CC) $(OBJECTS) $(FLAGS) $(LIBS) -o callcapture + +clean: + rm -f $(PREREQUISITES) $(OBJECTS) callcapture + rm -f *.d */*.d + +.PHONY: all build clean install diff --git a/CallCapture/PatchCordProxy.cpp b/CallCapture/PatchCordProxy.cpp new file mode 100644 index 0000000..bb3c453 --- /dev/null +++ b/CallCapture/PatchCordProxy.cpp @@ -0,0 +1,154 @@ +// Copyright 2015 by Artem Prilutskiy + +#include "PatchCordProxy.h" +#include <stdlib.h> +#include <string.h> + +#include <syslog.h> +#include <stdio.h> + +#define INTERFACE_NAME "me.burnaway.BrandMeister" +#define SERVICE_NAME INTERFACE_NAME +#define OBJECT_PATH "/me/burnaway/BrandMeister" + +// From AutoPatch.cpp +#define AUTOPATCH_LINK_NAME "AutoPatch" + +// From PatchCord.h +#define VALUE_CORD_OUTGOING_SOURCE_ID 1 +#define VALUE_CORD_INCOMING_SOURCE_ID 4 + +PatchCordProxy::PatchCordProxy(uint32_t number) : + number(number), + banner(NULL) +{ + connection = dbus_bus_get(DBUS_BUS_SYSTEM, NULL); +} + +PatchCordProxy::~PatchCordProxy() +{ + free(banner); + dbus_connection_unref(connection); +} + +void PatchCordProxy::setTalkerID(uint32_t value) +{ + getContextBanner(); + setVendorSpecificValue(VALUE_CORD_OUTGOING_SOURCE_ID, value); +} + +uint32_t PatchCordProxy::getTalkerID() +{ + getContextBanner(); + return getVendorSpecificValue(VALUE_CORD_INCOMING_SOURCE_ID); +} + +void PatchCordProxy::getContextBanner() +{ + DBusMessage* message = dbus_message_new_method_call( + SERVICE_NAME, OBJECT_PATH, + INTERFACE_NAME, "getContextList"); + + const char* name = AUTOPATCH_LINK_NAME; + + dbus_message_append_args(message, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_UINT32, &number, + DBUS_TYPE_INVALID); + + DBusPendingCall* pending; + if (dbus_connection_send_with_reply(connection, message, &pending, -1)) + { + dbus_connection_flush(connection); + dbus_message_unref(message); + dbus_pending_call_block(pending); + message = dbus_pending_call_steal_reply(pending); + char** array; + int count; + if ((dbus_message_get_args(message, NULL, + DBUS_TYPE_ARRAY, DBUS_TYPE_STRING, &array, &count, + DBUS_TYPE_INVALID)) && + (count > 0)) + { + free(banner); + banner = strdup(*array); + dbus_free_string_array(array); + } + dbus_pending_call_unref(pending); + } + dbus_message_unref(message); +} + +void PatchCordProxy::setVendorSpecificValue(uint32_t key, uint32_t value) +{ + DBusMessage* message = dbus_message_new_method_call( + SERVICE_NAME, OBJECT_PATH, + INTERFACE_NAME, "setVendorSpecificValue"); + + dbus_message_append_args(message, + DBUS_TYPE_STRING, &banner, + DBUS_TYPE_UINT32, &key, + DBUS_TYPE_UINT32, &value, + DBUS_TYPE_INVALID); + + DBusPendingCall* pending; + if (dbus_connection_send_with_reply(connection, message, &pending, -1)) + { + dbus_connection_flush(connection); + dbus_message_unref(message); + dbus_pending_call_block(pending); + message = dbus_pending_call_steal_reply(pending); + dbus_pending_call_unref(pending); + } + dbus_message_unref(message); + + // Each call of dbus_message_unref removes banner from the heap + banner = NULL; +} + +uint32_t PatchCordProxy::getVendorSpecificValue(uint32_t key) +{ + DBusMessage* message = dbus_message_new_method_call( + SERVICE_NAME, OBJECT_PATH, + INTERFACE_NAME, "getContextData"); + + dbus_message_append_args(message, + DBUS_TYPE_STRING, &banner, + DBUS_TYPE_INVALID); + + uint32_t value = 0; + + DBusPendingCall* pending; + if (dbus_connection_send_with_reply(connection, message, &pending, -1)) + { + dbus_connection_flush(connection); + dbus_message_unref(message); + dbus_pending_call_block(pending); + message = dbus_pending_call_steal_reply(pending); + const char* name; + const char* address; + dbus_uint32_t type; + dbus_uint32_t state; + dbus_uint32_t number; + dbus_uint32_t* values; + int count; + if (dbus_message_get_args(message, NULL, + DBUS_TYPE_STRING, &banner, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_UINT32, &type, + DBUS_TYPE_UINT32, &number, + DBUS_TYPE_STRING, &address, + DBUS_TYPE_UINT32, &state, + DBUS_TYPE_ARRAY, DBUS_TYPE_UINT32, &values, &count, + DBUS_TYPE_INVALID)) + value = values[key]; + dbus_pending_call_unref(pending); + } + + dbus_message_unref(message); + + // Each call of dbus_message_unref removes banner from the heap + banner = NULL; + + return value; +} diff --git a/CallCapture/PatchCordProxy.h b/CallCapture/PatchCordProxy.h new file mode 100644 index 0000000..75149f0 --- /dev/null +++ b/CallCapture/PatchCordProxy.h @@ -0,0 +1,31 @@ +// Copyright 2015 by Artem Prilutskiy + +#ifndef PATCHCORDPROXY_H +#define PATCHCORDPROXY_H + +#include <stdint.h> +#include <dbus/dbus.h> + +class PatchCordProxy +{ + public: + + PatchCordProxy(uint32_t number); + ~PatchCordProxy(); + + void setTalkerID(uint32_t value); + uint32_t getTalkerID(); + + private: + + DBusConnection* connection; + uint32_t number; + char* banner; + + void getContextBanner(); + void setVendorSpecificValue(uint32_t key, uint32_t value); + uint32_t getVendorSpecificValue(uint32_t key); + +}; + +#endif \ No newline at end of file diff --git a/CallCapture/UserDataStore.cpp b/CallCapture/UserDataStore.cpp new file mode 100644 index 0000000..9fbe681 --- /dev/null +++ b/CallCapture/UserDataStore.cpp @@ -0,0 +1,137 @@ +// Copyright 2015 by Artem Prilutskiy + +#include "UserDataStore.h" + +#include <stdlib.h> +#include <stdarg.h> +#include <string.h> +#include <syslog.h> + +#define GET_ID_FOR_CALL 0 +#define GET_CREDENTIALS_FOR_ID 1 + +UserDataStore::UserDataStore(const char* file) : + connection(NULL) +{ + const char* queries[] = + { + // GET_ID_FOR_CALL + "SELECT `ID`" + " FROM Users" + " WHERE `Call` = ?" + " ORDER BY `Priority`" + " LIMIT 1", + // GET_CREDENTIALS_FOR_ID + "SELECT `Call`, `Text`" + " FROM Users" + " WHERE `ID` = ?", + }; + + connection = mysql_init(NULL); + + mysql_options(connection, MYSQL_READ_DEFAULT_FILE, realpath(file, NULL)); + mysql_real_connect(connection, NULL, NULL, NULL, NULL, 0, NULL, CLIENT_REMEMBER_OPTIONS); + + my_bool active = true; + mysql_options(connection, MYSQL_OPT_RECONNECT, &active); + + for (size_t index = 0; index < PREPARED_STATEMENT_COUNT; index ++) + { + const char* query = queries[index]; + statements[index] = mysql_stmt_init(connection); + mysql_stmt_prepare(statements[index], query, strlen(query)); + } + + const char* error = mysql_error(connection); + if ((error != NULL) && (*error != '\0')) + syslog(LOG_CRIT, "MySQL error: %s\n", error); +} + +UserDataStore::~UserDataStore() +{ + for (size_t index = 0; index < PREPARED_STATEMENT_COUNT; index ++) + mysql_stmt_close(statements[index]); + + mysql_close(connection); +} + +// Public methods + +uint32_t UserDataStore::getPrivateIDForCall(const char* call) +{ + uint32_t number; + size_t length = strlen(call); + + return + execute(GET_ID_FOR_CALL, 1, 1, + MYSQL_TYPE_STRING, call, length, + MYSQL_TYPE_LONG, &number) * + number; +} + + +bool UserDataStore::getCredentialsForID(uint32_t number, char* call, char* text) +{ + return + execute(GET_CREDENTIALS_FOR_ID, 1, 2, + MYSQL_TYPE_LONG, &number, + MYSQL_TYPE_STRING, call, LONG_CALLSIGN_LENGTH + 1, + MYSQL_TYPE_STRING, text, SLOW_DATA_TEXT_LENGTH + 1); +} + +// Internals + +MYSQL_BIND* UserDataStore::bind(int count, va_list* arguments) +{ + MYSQL_BIND* bindings = NULL; + if (count > 0) + { + bindings = (MYSQL_BIND*)calloc(count, sizeof(MYSQL_BIND)); + for (int index = 0; index < count; index ++) + { + int type = va_arg(*arguments, int); + bindings[index].buffer_type = (enum_field_types)type; + bindings[index].buffer = va_arg(*arguments, void*); + if ((type == MYSQL_TYPE_STRING) || + (type == MYSQL_TYPE_DATETIME)) + { + bindings[index].buffer_length = va_arg(*arguments, int); + bindings[index].length = &bindings[index].buffer_length; + } + } + } + return bindings; +} + +bool UserDataStore::execute(int index, int count1, int count2, ...) +{ + bool result = true; + MYSQL_STMT* statement = statements[index]; + + va_list arguments; + va_start(arguments, count2); + MYSQL_BIND* parameters = bind(count1, &arguments); + MYSQL_BIND* columns = bind(count2, &arguments); + va_end(arguments); + + if ((parameters && mysql_stmt_bind_param(statement, parameters)) || + (columns && mysql_stmt_bind_result(statement, columns)) || + mysql_stmt_execute(statement) || + (columns && mysql_stmt_store_result(statement))) + { + const char* error = mysql_stmt_error(statement); + syslog(LOG_CRIT, "MySQL error while processing statement %d: %s\n", index, error); + result = false; + } + + if (result && columns) + { + result = !mysql_stmt_fetch(statement); + mysql_stmt_free_result(statement); + } + + free(columns); + free(parameters); + return result; +} + diff --git a/CallCapture/UserDataStore.h b/CallCapture/UserDataStore.h new file mode 100644 index 0000000..1f2ed01 --- /dev/null +++ b/CallCapture/UserDataStore.h @@ -0,0 +1,35 @@ +// Copyright 2015 by Artem Prilutskiy + +#ifndef USERDATASTORE_H +#define USERDATASTORE_H + +#include <stdint.h> +#include <stdarg.h> +#include <mysql/mysql.h> + +#define PREPARED_STATEMENT_COUNT 2 + +#define LONG_CALLSIGN_LENGTH 8 +#define SLOW_DATA_TEXT_LENGTH 20 + +class UserDataStore +{ + public: + + UserDataStore(const char* file); + ~UserDataStore(); + + uint32_t getPrivateIDForCall(const char* call); + bool getCredentialsForID(uint32_t number, char* call, char* text); + + protected: + + MYSQL* connection; + MYSQL_STMT* statements[PREPARED_STATEMENT_COUNT]; + + MYSQL_BIND* bind(int count, va_list* arguments); + bool execute(int index, int count1, int count2, ...); + +}; + +#endif diff --git a/SVXLink/Makefile b/SVXLink/Makefile new file mode 100644 index 0000000..8a8d8e7 --- /dev/null +++ b/SVXLink/Makefile @@ -0,0 +1,43 @@ +DIRECTORIES = \ + echolink + +OBJECTS = \ + echolink/UserDataStore.o \ + echolink/PatchCordProxy.o \ + echolink/BrandMeisterBridge.o \ + Test.o + +LIBRARIES = \ + mysqlclient + +DEPENDENCIES = \ + dbus-1 + +CXXFLAGS := -fno-implicit-templates -D__STDC_CONSTANT_MACROS +FLAGS := -g -rdynamic -fno-omit-frame-pointer -O3 -MMD $(foreach directory, $(DIRECTORIES), -I$(directory)) $(shell pkg-config --cflags $(DEPENDENCIES)) -DBUILD=\"$(BUILD)\" -DVERSION=$(VERSION) +LIBS := -lstdc++ $(foreach library, $(LIBRARIES), -l$(library)) $(shell pkg-config --libs $(DEPENDENCIES)) + +# PREREQUISITES = + +CC = gcc +CXX = g++ +COMPILER_VERSION_FLAG := $(shell expr `${CXX} -dumpversion | cut -f1-2 -d.` \>= 4.9) + +CFLAGS += $(FLAGS) -std=gnu99 + +ifeq ($(COMPILER_VERSION_FLAG), 1) + CXXFLAGS += $(FLAGS) -std=gnu++11 +else + CXXFLAGS += $(FLAGS) -std=gnu++0x +endif + +all: build + +build: $(PREREQUISITES) $(OBJECTS) + $(CC) $(OBJECTS) $(FLAGS) $(LIBS) -o unit-test + +clean: + rm -f $(PREREQUISITES) $(OBJECTS) unit-test + rm -f *.d */*.d + +.PHONY: all build clean install config binary system diff --git a/SVXLink/ModuleEchoLink.conf b/SVXLink/ModuleEchoLink.conf new file mode 100644 index 0000000..4d75aa1 --- /dev/null +++ b/SVXLink/ModuleEchoLink.conf @@ -0,0 +1,21 @@ +[ModuleEchoLink] +NAME=EchoLink +ID=2 +TIMEOUT=0 +#DROP_INCOMING=^()$ +#REJECT_INCOMING=^(.*-[LR])$ +#REJECT_INCOMING=^(.*)$ +#ACCEPT_INCOMING=^(.*)$ +#REJECT_OUTGOING=^()$ +#ACCEPT_OUTGOING=^(.*)$ +SERVERS=servers.echolink.org +CALLSIGN=MY0CALL-L +PASSWORD=password +SYSOPNAME=*DSTAR.SU DMR Bridge* +LOCATION=Moscow, Russia +MAX_QSOS=10 +MAX_CONNECTIONS=11 +LINK_IDLE_TIMEOUT=0 +DESCRIPTION="You have connected to a DSTAR.SU DMR Bridge\n" +AUTOCON_ECHOLINK_ID=196189 +AUTOCON_TIME=30 diff --git a/SVXLink/Test.cpp b/SVXLink/Test.cpp new file mode 100644 index 0000000..0681bba --- /dev/null +++ b/SVXLink/Test.cpp @@ -0,0 +1,13 @@ +#include <stdio.h> + +#include "BrandMeisterBridge.h" + +int main(int argc, const char* argv[]) +{ + BrandMeisterBridge bridge; + // Test 1 + printf("Talker: %s\n", bridge.getTalker()); + // Test 2 + bridge.setTalker("R3ABM", "Artem"); + return 0; +}; diff --git a/SVXLink/build.sh b/SVXLink/build.sh new file mode 100755 index 0000000..bb8ece9 --- /dev/null +++ b/SVXLink/build.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +PREFIX=/opt/SVXLink +sudo apt-get install libsigc++-2.0-dev libpopt-dev libgcrypt11-dev libasound2-dev libgsm1-dev + +sudo adduser svxlink --system --home $PREFIX --shell /bin/false --disabled-login --disabled-password +sudo addgroup svxlink +sudo adduser svxlink audio +sudo adduser svxlink master +sudo adduser svxlink svxlink + +pushd . +mkdir src/build +cd src/build +cmake -DCMAKE_INSTALL_PREFIX=$PREFIX -DUSE_QT=NO -DUSE_OSS=NO .. +make +sudo make install +popd + +sudo echo $PREFIX/lib > /etc/ld.so.conf.d/svxlink.conf +sudo ldconfig diff --git a/SVXLink/echolink/BrandMeisterBridge.cpp b/SVXLink/echolink/BrandMeisterBridge.cpp new file mode 100644 index 0000000..7e7931b --- /dev/null +++ b/SVXLink/echolink/BrandMeisterBridge.cpp @@ -0,0 +1,95 @@ +// Copyright 2015 by Artem Prilutskiy + +#include "BrandMeisterBridge.h" +#include <string.h> +#include <stdlib.h> +#include <syslog.h> +#include <stdio.h> + +#define ECHOLINK_DEFAULT_USER_NUMBER 1 +#define ECHOLINK_DEFAULT_CORD_NUMBER 10 +#define REGISTRY_CONFIGURATION_FILE "/opt/BrandMeister/Registry.cnf" + +BrandMeisterBridge::BrandMeisterBridge() : + proxy(ECHOLINK_DEFAULT_CORD_NUMBER), + store(REGISTRY_CONFIGURATION_FILE), + talker(NULL) +{ + +} + +BrandMeisterBridge::~BrandMeisterBridge() +{ + free(talker); +} + +// Interface methods for ModuleEchoLink + +const char* BrandMeisterBridge::getTalker() +{ + free(talker); + uint32_t number = proxy.getTalkerID(); + + char call[LONG_CALLSIGN_LENGTH]; + char text[SLOW_DATA_TEXT_LENGTH]; + if ((number != 0) && + (store.getCredentialsForID(number, call, text))) + { + asprintf(&talker, "%s %s", call, text); + return talker; + } + + asprintf(&talker, "DMR ID: %d", number); + return talker; +} + +void BrandMeisterBridge::setTalker(const char* call, const char* name) +{ + if (*call == '*') + { + // Do not process conference call-sign + return; + } + + const char* delimiter = strpbrk(call, " -\n"); + if (delimiter != NULL) + { + // Remove characters after call-sign + size_t length = delimiter - call; + char* buffer = (char*)alloca(length + 1); + strncpy(buffer, call, length); + call = buffer; + } + + uint32_t number = store.getPrivateIDForCall(call); + if (number == 0) + number = ECHOLINK_DEFAULT_USER_NUMBER; + + syslog(LOG_INFO, "Set talker ID to %d for call-sign %s", number, call); + proxy.setTalkerID(number); +} + +void BrandMeisterBridge::handleChatMessage(const char* text) +{ + // CONF Russian Reflector, Open 24/7, Contacts: rv3dhc.link@qip.ru * Call CQ / Use pauses 2sec * [28/500] + // R3ABM-L *DSTAR.SU DMR Bridge* + // UB3AMO Moscow T I N A O + // ->UA0LQE-L USSURIISK + + if (strncmp(text, "CONF ", 5) == 0) + { + const char* delimiter = strstr(text, "\n->"); + if (delimiter != NULL) + { + const char* call = delimiter + 3; + setTalker(call, NULL); + } + else + { + uint32_t number = ECHOLINK_DEFAULT_USER_NUMBER; + syslog(LOG_INFO, "Set talker ID to %d (call-sign was not fit into chat message)", number); + proxy.setTalkerID(number); + } + } +} + diff --git a/SVXLink/echolink/BrandMeisterBridge.h b/SVXLink/echolink/BrandMeisterBridge.h new file mode 100644 index 0000000..554bbe8 --- /dev/null +++ b/SVXLink/echolink/BrandMeisterBridge.h @@ -0,0 +1,28 @@ +// Copyright 2015 by Artem Prilutskiy + +#ifndef BRANDMEISTERBRIDGE_H +#define BRANDMEISTERBRIDGE_H + +#include "PatchCordProxy.h" +#include "UserDataStore.h" + +class BrandMeisterBridge +{ + public: + + BrandMeisterBridge(); + ~BrandMeisterBridge(); + + const char* getTalker(); + void setTalker(const char* call, const char* name); + void handleChatMessage(const char* text); + + private: + + PatchCordProxy proxy; + UserDataStore store; + char* talker; + +}; + +#endif \ No newline at end of file diff --git a/SVXLink/echolink/CMakeLists.txt b/SVXLink/echolink/CMakeLists.txt new file mode 100644 index 0000000..6f89c10 --- /dev/null +++ b/SVXLink/echolink/CMakeLists.txt @@ -0,0 +1,59 @@ +# The name of the module without the Module prefix +set(MODNAME EchoLink) + +# Module source code +set(MODSRC QsoImpl.cpp) + +# *** +pkg_check_modules(DBUS dbus-1) +include_directories(${DBUS_INCLUDE_DIRS}) +set(LIBS ${LIBS} ${DBUS_LIBRARIES} mysqlclient) +set(MODSRC ${MODSRC} PatchCordProxy.cpp UserDataStore.cpp BrandMeisterBridge.cpp) +# *** + +# Project libraries to link to +set(LIBS ${LIBS} echolib) + +# Find the TCL library +if(TCL_LIBRARY) + set(TCL_LIBRARY_CACHED TRUE) +endif(TCL_LIBRARY) +find_package(TCL QUIET) +if(TCL_FOUND) + if (NOT TCL_LIBRARY_CACHED) + message("-- Found TCL: ${TCL_LIBRARY}") + endif(NOT TCL_LIBRARY_CACHED) +else(TCL_FOUND) + message(FATAL_ERROR "-- Could NOT find the TCL scripting language") +endif(TCL_FOUND) +set(LIBS ${LIBS} ${TCL_LIBRARY}) +include_directories(${TCL_INCLUDE_PATH}) + +# Find the GSM codec library and include directory +find_package(GSM REQUIRED) +if(NOT GSM_FOUND) + message(FATAL_ERROR "libgsm not found") +endif(NOT GSM_FOUND) +include_directories(${GSM_INCLUDE_DIR}) +set(LIBS ${LIBS} ${GSM_LIBRARY}) + +string(TOUPPER MODULE_${MODNAME} VERNAME) + +# Add targets for version files +set(VERSION_DEPENDS) +add_version_target(${VERNAME} VERSION_DEPENDS) +add_version_target(SVXLINK VERSION_DEPENDS) + +# Build the plugin +add_library(Module${MODNAME} MODULE Module${MODNAME}.cpp ${MODSRC} + ${VERSION_DEPENDS} +) +set_target_properties(Module${MODNAME} PROPERTIES PREFIX "") +target_link_libraries(Module${MODNAME} ${LIBS}) + +# Install targets +install(TARGETS Module${MODNAME} DESTINATION ${SVX_MODULE_INSTALL_DIR}) +install(FILES ${MODNAME}.tcl DESTINATION ${SVX_SHARE_INSTALL_DIR}/events.d) +install_if_not_exists(Module${MODNAME}.conf + ${SVX_SYSCONF_INSTALL_DIR}/svxlink.d + ) diff --git a/SVXLink/echolink/ModuleEchoLink.cpp b/SVXLink/echolink/ModuleEchoLink.cpp new file mode 100644 index 0000000..2331e3b --- /dev/null +++ b/SVXLink/echolink/ModuleEchoLink.cpp @@ -0,0 +1,2084 @@ +/** +@file ModuleEchoLink.cpp +@brief A module that provides EchoLink connection possibility +@author Tobias Blomberg / SM0SVX +@date 2004-03-07 + +\verbatim +A module (plugin) for the multi purpose tranciever frontend system. +Copyright (C) 2004-2014 Tobias Blomberg / SM0SVX + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +\endverbatim +*/ + +/**************************************************************************** + * + * System Includes + * + ****************************************************************************/ + +#include <stdio.h> +#include <time.h> + +#include <algorithm> +#include <cassert> +#include <sstream> +#include <cstdlib> +#include <vector> + + +/**************************************************************************** + * + * Project Includes + * + ****************************************************************************/ + +#include <AsyncTimer.h> +#include <AsyncConfig.h> +#include <AsyncAudioSplitter.h> +#include <AsyncAudioValve.h> +#include <AsyncAudioSelector.h> +#include <EchoLinkDirectory.h> +#include <EchoLinkDispatcher.h> +#include <EchoLinkProxy.h> +#include <LocationInfo.h> +#include <common.h> + + +/**************************************************************************** + * + * Local Includes + * + ****************************************************************************/ + +#include "version/MODULE_ECHOLINK.h" +#include "ModuleEchoLink.h" +#include "QsoImpl.h" + + +/**************************************************************************** + * + * Namespaces to use + * + ****************************************************************************/ + +using namespace std; +using namespace sigc; +using namespace Async; +using namespace EchoLink; + + + +/**************************************************************************** + * + * Defines & typedefs + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Local class definitions + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Prototypes + * + ****************************************************************************/ + + + + +/**************************************************************************** + * + * Exported Global Variables + * + ****************************************************************************/ + + + + +/**************************************************************************** + * + * Local Global Variables + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Pure C-functions + * + ****************************************************************************/ + + +extern "C" { + Module *module_init(void *dl_handle, Logic *logic, const char *cfg_name) + { + return new ModuleEchoLink(dl_handle, logic, cfg_name); + } +} /* extern "C" */ + + + +/**************************************************************************** + * + * Public member functions + * + ****************************************************************************/ + + +ModuleEchoLink::ModuleEchoLink(void *dl_handle, Logic *logic, + const string& cfg_name) + : Module(dl_handle, logic, cfg_name), dir(0), dir_refresh_timer(0), + remote_activation(false), pending_connect_id(-1), last_message(""), + max_connections(1), max_qsos(1), talker(0), squelch_is_open(false), + state(STATE_NORMAL), cbc_timer(0), dbc_timer(0), drop_incoming_regex(0), + reject_incoming_regex(0), accept_incoming_regex(0), + reject_outgoing_regex(0), accept_outgoing_regex(0), splitter(0), + listen_only_valve(0), selector(0), num_con_max(0), num_con_ttl(5*60), + num_con_block_time(120*60), num_con_update_timer(0), reject_conf(false), + autocon_echolink_id(0), autocon_time(DEFAULT_AUTOCON_TIME), + autocon_timer(0), proxy(0) +{ + cout << "\tModule EchoLink v" MODULE_ECHOLINK_VERSION " starting...\n"; + +} /* ModuleEchoLink */ + + +ModuleEchoLink::~ModuleEchoLink(void) +{ + moduleCleanup(); +} /* ~ModuleEchoLink */ + + +bool ModuleEchoLink::initialize(void) +{ + if (!Module::initialize()) + { + return false; + } + + vector<string> servers; + if (!cfg().getValue(cfgName(), "SERVERS", servers)) + { + string server; + if (cfg().getValue(cfgName(), "SERVER", server)) + { + cerr << "*** WARNING: Config variable " << cfgName() + << "/SERVER is deprecated. Use SERVERS instead.\n"; + servers.push_back(server); + } + } + if (servers.empty()) + { + cerr << "*** ERROR: Config variable " << cfgName() << "/SERVERS not set " + "or empty\n"; + return false; + } + + if (!cfg().getValue(cfgName(), "CALLSIGN", mycall)) + { + cerr << "*** ERROR: Config variable " << cfgName() << "/CALLSIGN not set\n"; + return false; + } + if (mycall == "MYCALL-L") + { + cerr << "*** ERROR: Please set the EchoLink callsign (" << cfgName() + << "/CALLSIGN) to a real callsign\n"; + return false; + } + + string password; + if (!cfg().getValue(cfgName(), "PASSWORD", password)) + { + cerr << "*** ERROR: Config variable " << cfgName() << "/PASSWORD not set\n"; + return false; + } + if (password == "MyPass") + { + cerr << "*** ERROR: Please set the EchoLink password (" << cfgName() + << "/PASSWORD) to a real password\n"; + return false; + } + + if (!cfg().getValue(cfgName(), "LOCATION", location)) + { + cerr << "*** ERROR: Config variable " << cfgName() << "/LOCATION not set\n"; + return false; + } + + if (location.size() > Directory::MAX_DESCRIPTION_SIZE) + { + cerr << "*** WARNING: The value of " << cfgName() << "/LOCATION is too " + "long. Maximum length is " << Directory::MAX_DESCRIPTION_SIZE << + " characters.\n"; + location.resize(Directory::MAX_DESCRIPTION_SIZE); + } + + if (!cfg().getValue(cfgName(), "SYSOPNAME", sysop_name)) + { + cerr << "*** ERROR: Config variable " << cfgName() + << "/SYSOPNAME not set\n"; + return false; + } + + if (!cfg().getValue(cfgName(), "DESCRIPTION", description)) + { + cerr << "*** ERROR: Config variable " << cfgName() + << "/DESCRIPTION not set\n"; + return false; + } + + string value; + if (cfg().getValue(cfgName(), "MAX_CONNECTIONS", value)) + { + max_connections = atoi(value.c_str()); + } + + if (cfg().getValue(cfgName(), "MAX_QSOS", value)) + { + max_qsos = atoi(value.c_str()); + } + + if (max_qsos > max_connections) + { + cerr << "*** ERROR: The value of " << cfgName() << "/MAX_CONNECTIONS (" + << max_connections << ") must be greater or equal to the value of " + << cfgName() << "/MAX_QSOS (" << max_qsos << ").\n"; + return false; + } + + cfg().getValue(cfgName(), "ALLOW_IP", allow_ip); + + if (!cfg().getValue(cfgName(), "DROP_INCOMING", value)) + { + value = "^$"; + } + drop_incoming_regex = new regex_t; + int err = regcomp(drop_incoming_regex, value.c_str(), + REG_EXTENDED | REG_NOSUB | REG_ICASE); + if (err != 0) + { + size_t msg_size = regerror(err, drop_incoming_regex, 0, 0); + char msg[msg_size]; + size_t err_size = regerror(err, drop_incoming_regex, msg, msg_size); + assert(err_size == msg_size); + cerr << "*** ERROR: Syntax error in " << cfgName() << "/DROP_INCOMING: " + << msg << endl; + moduleCleanup(); + return false; + } + + // To reduce the number of senseless connects + if (cfg().getValue(cfgName(), "CHECK_NR_CONNECTS", value)) + { + vector<unsigned> params; + if (SvxLink::splitStr(params, value, ",") != 3) + { + cerr << "*** ERROR: Syntax error in " << cfgName() + << "/CHECK_NR_CONNECTS\n" + << "Example: CHECK_NR_CONNECTS=3,300,60 where\n" + << " 3 = max number of connects\n" + << " 300 = time in seconds that a connection is remembered\n" + << " 60 = time in minutes that the party is blocked\n"; + return false; + } + num_con_max = params[0]; + num_con_ttl = params[1]; + num_con_block_time = params[2] * 60; + } + + if (!cfg().getValue(cfgName(), "REJECT_INCOMING", value)) + { + value = "^$"; + } + reject_incoming_regex = new regex_t; + err = regcomp(reject_incoming_regex, value.c_str(), + REG_EXTENDED | REG_NOSUB | REG_ICASE); + if (err != 0) + { + size_t msg_size = regerror(err, reject_incoming_regex, 0, 0); + char msg[msg_size]; + size_t err_size = regerror(err, reject_incoming_regex, msg, msg_size); + assert(err_size == msg_size); + cerr << "*** ERROR: Syntax error in " << cfgName() << "/REJECT_INCOMING: " + << msg << endl; + moduleCleanup(); + return false; + } + + if (!cfg().getValue(cfgName(), "ACCEPT_INCOMING", value)) + { + value = "^.*$"; + } + accept_incoming_regex = new regex_t; + err = regcomp(accept_incoming_regex, value.c_str(), + REG_EXTENDED | REG_NOSUB | REG_ICASE); + if (err != 0) + { + size_t msg_size = regerror(err, accept_incoming_regex, 0, 0); + char msg[msg_size]; + size_t err_size = regerror(err, accept_incoming_regex, msg, msg_size); + assert(err_size == msg_size); + cerr << "*** ERROR: Syntax error in " << cfgName() << "/ACCEPT_INCOMING: " + << msg << endl; + moduleCleanup(); + return false; + } + + if (!cfg().getValue(cfgName(), "REJECT_OUTGOING", value)) + { + value = "^$"; + } + reject_outgoing_regex = new regex_t; + err = regcomp(reject_outgoing_regex, value.c_str(), + REG_EXTENDED | REG_NOSUB | REG_ICASE); + if (err != 0) + { + size_t msg_size = regerror(err, reject_outgoing_regex, 0, 0); + char msg[msg_size]; + size_t err_size = regerror(err, reject_outgoing_regex, msg, msg_size); + assert(err_size == msg_size); + cerr << "*** ERROR: Syntax error in " << cfgName() << "/REJECT_OUTGOING: " + << msg << endl; + moduleCleanup(); + return false; + } + + if (!cfg().getValue(cfgName(), "ACCEPT_OUTGOING", value)) + { + value = "^.*$"; + } + accept_outgoing_regex = new regex_t; + err = regcomp(accept_outgoing_regex, value.c_str(), + REG_EXTENDED | REG_NOSUB | REG_ICASE); + if (err != 0) + { + size_t msg_size = regerror(err, accept_outgoing_regex, 0, 0); + char msg[msg_size]; + size_t err_size = regerror(err, accept_outgoing_regex, msg, msg_size); + assert(err_size == msg_size); + cerr << "*** ERROR: Syntax error in " << cfgName() << "/ACCEPT_OUTGOING: " + << msg << endl; + moduleCleanup(); + return false; + } + + cfg().getValue(cfgName(), "REJECT_CONF", reject_conf); + cfg().getValue(cfgName(), "AUTOCON_ECHOLINK_ID", autocon_echolink_id); + int autocon_time_secs = autocon_time / 1000; + cfg().getValue(cfgName(), "AUTOCON_TIME", autocon_time_secs); + autocon_time = 1000 * max(autocon_time_secs, 5); // At least five seconds + + string proxy_server; + cfg().getValue(cfgName(), "PROXY_SERVER", proxy_server); + uint16_t proxy_port = 8100; + cfg().getValue(cfgName(), "PROXY_PORT", proxy_port); + string proxy_password; + cfg().getValue(cfgName(), "PROXY_PASSWORD", proxy_password); + + if (!proxy_server.empty()) + { + proxy = new Proxy(proxy_server, proxy_port, mycall, proxy_password); + proxy->connect(); + } + + IpAddress bind_addr; + if (cfg().getValue(cfgName(), "BIND_ADDR", bind_addr) && bind_addr.isEmpty()) + { + cerr << "*** ERROR: Invalid configuration value for " << cfgName() + << "/BIND_ADDR specified.\n"; + moduleCleanup(); + return false; + } + + // Initialize directory server communication + dir = new Directory(servers, mycall, password, location, bind_addr); + dir->statusChanged.connect(mem_fun(*this, &ModuleEchoLink::onStatusChanged)); + dir->stationListUpdated.connect( + mem_fun(*this, &ModuleEchoLink::onStationListUpdated)); + dir->error.connect(mem_fun(*this, &ModuleEchoLink::onError)); + dir->makeOnline(); + + // Start listening to the EchoLink UDP ports + Dispatcher::setBindAddr(bind_addr); + if (Dispatcher::instance() == 0) + { + cerr << "*** ERROR: Could not create EchoLink listener (Dispatcher) " + "object\n"; + moduleCleanup(); + return false; + } + Dispatcher::instance()->incomingConnection.connect( + mem_fun(*this, &ModuleEchoLink::onIncomingConnection)); + + // Create audio pipe chain for audio transmitted to the remote EchoLink + // stations: <from core> -> Valve -> Splitter (-> QsoImpl ...) + listen_only_valve = new AudioValve; + AudioSink::setHandler(listen_only_valve); + + splitter = new AudioSplitter; + listen_only_valve->registerSink(splitter); + + // Create audio pipe chain for audio received from the remove EchoLink + // stations: (QsoImpl -> ) Selector -> Fifo -> <to core> + selector = new AudioSelector; + AudioSource::setHandler(selector); + + // Periodic updates of the "watch num connects" list + if (num_con_max > 0) + { + num_con_update_timer = new Timer(6000000); // One hour + num_con_update_timer->expired.connect(sigc::hide( + mem_fun(*this, &ModuleEchoLink::numConUpdate))); + } + + if (autocon_echolink_id > 0) + { + // Initially set the timer to 15 seconds for quick activation on statup + autocon_timer = new Timer(15000, Timer::TYPE_PERIODIC); + autocon_timer->expired.connect( + mem_fun(*this, &ModuleEchoLink::checkAutoCon)); + } + + return true; + +} /* ModuleEchoLink::initialize */ + + + +/**************************************************************************** + * + * Protected member functions + * + ****************************************************************************/ + +void ModuleEchoLink::logicIdleStateChanged(bool is_idle) +{ + /* + printf("ModuleEchoLink::logicIdleStateChanged: is_idle=%s\n", + is_idle ? "TRUE" : "FALSE"); + */ + + if (qsos.size() > 0) + { + vector<QsoImpl*>::iterator it; + for (it=qsos.begin(); it!=qsos.end(); ++it) + { + (*it)->logicIdleStateChanged(is_idle); + } + } + + checkIdle(); + +} /* ModuleEchoLink::logicIdleStateChanged */ + + + +/**************************************************************************** + * + * Private member functions + * + ****************************************************************************/ + + +void ModuleEchoLink::moduleCleanup(void) +{ + //FIXME: Delete qso objects + + delete num_con_update_timer; + num_con_update_timer = 0; + + if (accept_incoming_regex != 0) + { + regfree(accept_incoming_regex); + delete accept_incoming_regex; + accept_incoming_regex = 0; + } + if (reject_incoming_regex != 0) + { + regfree(reject_incoming_regex); + delete reject_incoming_regex; + reject_incoming_regex = 0; + } + if (drop_incoming_regex != 0) + { + regfree(drop_incoming_regex); + delete drop_incoming_regex; + drop_incoming_regex = 0; + } + if (accept_outgoing_regex != 0) + { + regfree(accept_outgoing_regex); + delete accept_outgoing_regex; + accept_outgoing_regex = 0; + } + if (reject_outgoing_regex != 0) + { + regfree(reject_outgoing_regex); + delete reject_outgoing_regex; + reject_outgoing_regex = 0; + } + + delete dir_refresh_timer; + dir_refresh_timer = 0; + Dispatcher::deleteInstance(); + delete dir; + dir = 0; + delete proxy; + proxy = 0; + delete cbc_timer; + cbc_timer = 0; + delete dbc_timer; + dbc_timer = 0; + state = STATE_NORMAL; + delete autocon_timer; + autocon_timer = 0; + + AudioSink::clearHandler(); + delete splitter; + splitter = 0; + delete listen_only_valve; + listen_only_valve = 0; + + AudioSource::clearHandler(); + delete selector; + selector = 0; +} /* ModuleEchoLink::moduleCleanup */ + + +/* + *---------------------------------------------------------------------------- + * Method: activateInit + * Purpose: Called by the core system when this module is activated. + * Input: None + * Output: None + * Author: Tobias Blomberg / SM0SVX + * Created: 2004-03-07 + * Remarks: + * Bugs: + *---------------------------------------------------------------------------- + */ +void ModuleEchoLink::activateInit(void) +{ + updateEventVariables(); + state = STATE_NORMAL; + listen_only_valve->setOpen(true); +} /* activateInit */ + + +/* + *---------------------------------------------------------------------------- + * Method: deactivateCleanup + * Purpose: Called by the core system when this module is deactivated. + * Input: None + * Output: None + * Author: Tobias Blomberg / SM0SVX + * Created: 2004-03-07 + * Remarks: Do NOT call this function directly unless you really know what + * you are doing. Use Module::deactivate() instead. + * Bugs: + *---------------------------------------------------------------------------- + */ +void ModuleEchoLink::deactivateCleanup(void) +{ + vector<QsoImpl*> qsos_tmp(qsos); + vector<QsoImpl*>::iterator it; + for (it=qsos_tmp.begin(); it!=qsos_tmp.end(); ++it) + { + if ((*it)->currentState() != Qso::STATE_DISCONNECTED) + { + (*it)->disconnect(); + } + } + + outgoing_con_pending.clear(); + + remote_activation = false; + delete cbc_timer; + cbc_timer = 0; + delete dbc_timer; + dbc_timer = 0; + state = STATE_NORMAL; + listen_only_valve->setOpen(true); +} /* deactivateCleanup */ + + +/* + *---------------------------------------------------------------------------- + * Method: dtmfDigitReceived + * Purpose: Called by the core system when a DTMF digit has been + * received. + * Input: digit - The DTMF digit received (0-9, A-D, *, #) + * duration - The length in milliseconds of the received digit + * Output: Return true if the digit is handled or false if not + * Author: Tobias Blomberg / SM0SVX + * Created: 2004-03-07 + * Remarks: + * Bugs: + *---------------------------------------------------------------------------- + */ +#if 0 +bool ModuleEchoLink::dtmfDigitReceived(char digit, int duration) +{ + //cout << "DTMF digit received in module " << name() << ": " << digit << endl; + + return false; + +} /* dtmfDigitReceived */ +#endif + + +/* + *---------------------------------------------------------------------------- + * Method: dtmfCmdReceived + * Purpose: Called by the core system when a DTMF command has been + * received. A DTMF command consists of a string of digits ended + * with a number sign (#). The number sign is not included in the + * command string. + * Input: cmd - The received command. + * Output: None + * Author: Tobias Blomberg / SM0SVX + * Created: 2004-03-07 + * Remarks: + * Bugs: + *---------------------------------------------------------------------------- + */ +void ModuleEchoLink::dtmfCmdReceived(const string& cmd) +{ + cout << "DTMF command received in module " << name() << ": " << cmd << endl; + + remote_activation = false; + + if (state == STATE_CONNECT_BY_CALL) + { + handleConnectByCall(cmd); + return; + } + + if (state == STATE_DISCONNECT_BY_CALL) + { + handleDisconnectByCall(cmd); + return; + } + + if (cmd.size() == 0) // Disconnect node or deactivate module + { + if ((qsos.size() != 0) && + (qsos.back()->currentState() != Qso::STATE_DISCONNECTED)) + { + qsos.back()->disconnect(); + } + else if (outgoing_con_pending.empty()) + { + deactivateMe(); + } + } + /* + else if (cmd[0] == '*') // Connect by callsign + { + connectByCallsign(cmd); + } + */ + else if ((cmd.size() < 4) || (cmd[1] == '*')) // Dispatch to command handling + { + handleCommand(cmd); + } + else + { + connectByNodeId(atoi(cmd.c_str())); + } +} /* dtmfCmdReceived */ + + +void ModuleEchoLink::dtmfCmdReceivedWhenIdle(const std::string &cmd) +{ + if (cmd == "2") // Play own node id + { + stringstream ss; + ss << "play_node_id "; + const StationData *station = dir->findCall(dir->callsign()); + ss << (station ? station->id() : 0); + processEvent(ss.str()); + } + else + { + commandFailed(cmd); + } +} /* dtmfCmdReceivedWhenIdle */ + + +/* + *---------------------------------------------------------------------------- + * Method: squelchOpen + * Purpose: Called by the core system when activity is detected + * on the receiver. + * Input: is_open - true if the squelch is open or else false. + * Output: None + * Author: Tobias Blomberg / SM0SVX + * Created: 2004-03-07 + * Remarks: + * Bugs: + *---------------------------------------------------------------------------- + */ +void ModuleEchoLink::squelchOpen(bool is_open) +{ + //printf("RX squelch is %s...\n", is_open ? "open" : "closed"); + + squelch_is_open = is_open; + if (listen_only_valve->isOpen()) + { + broadcastTalkerStatus(); + } + + for (vector<QsoImpl*>::iterator it=qsos.begin(); it!=qsos.end(); ++it) + { + (*it)->squelchOpen(is_open); + } +} /* squelchOpen */ + + +/* + *---------------------------------------------------------------------------- + * Method: allMsgsWritten + * Purpose: Called by the core system when all audio messages queued + * for playing have been played. + * Input: None + * Output: None + * Author: Tobias Blomberg / SM0SVX + * Created: 2004-05-22 + * Remarks: + * Bugs: + *---------------------------------------------------------------------------- + */ +void ModuleEchoLink::allMsgsWritten(void) +{ + if (!outgoing_con_pending.empty()) + { + vector<QsoImpl*>::iterator it; + for (it=outgoing_con_pending.begin(); it!=outgoing_con_pending.end(); ++it) + { + (*it)->connect(); + } + //outgoing_con_pending->connect(); + updateDescription(); + broadcastTalkerStatus(); + outgoing_con_pending.clear(); + } +} /* allMsgsWritten */ + + +/* + *---------------------------------------------------------------------------- + * Method: onStatusChanged + * Purpose: Called by the EchoLink::Directory object when the status of + * the registration is changed in the directory server. + * Input: status - The new status + * Output: None + * Author: Tobias Blomberg / SM0SVX + * Created: 2004-03-07 + * Remarks: + * Bugs: + *---------------------------------------------------------------------------- + */ +void ModuleEchoLink::onStatusChanged(StationData::Status status) +{ + cout << "EchoLink directory status changed to " + << StationData::statusStr(status) << endl; + + // Get the directory list on first connection to the directory server + if ((status == StationData::STAT_ONLINE) || + (status == StationData::STAT_BUSY)) + { + if (dir_refresh_timer == 0) + { + getDirectoryList(); + } + } + else + { + delete dir_refresh_timer; + dir_refresh_timer = 0; + } + + // Update status at aprs.echolink.org + if (LocationInfo::has_instance()) + { + LocationInfo::instance()->updateDirectoryStatus(status); + } +} /* onStatusChanged */ + + +/* + *---------------------------------------------------------------------------- + * Method: onStationListUpdated + * Purpose: Called by the EchoLink::Directory object when the station list + * has been updated. + * Input: None + * Output: None + * Author: Tobias Blomberg / SM0SVX + * Created: 2004-03-07 + * Remarks: + * Bugs: + *---------------------------------------------------------------------------- + */ +void ModuleEchoLink::onStationListUpdated(void) +{ + if (pending_connect_id > 0) + { + const StationData *station = dir->findStation(pending_connect_id); + if (station != 0) + { + createOutgoingConnection(*station); + } + else + { + cout << "The EchoLink ID " << pending_connect_id + << " could not be found.\n"; + stringstream ss; + ss << "station_id_not_found " << pending_connect_id; + processEvent(ss.str()); + } + pending_connect_id = -1; + } + + if (dir->message() != last_message) + { + cout << "--- EchoLink directory server message: ---" << endl; + cout << dir->message() << endl; + last_message = dir->message(); + } +} /* onStationListUpdated */ + + +/* + *---------------------------------------------------------------------------- + * Method: onError + * Purpose: Called by the EchoLink::Directory object when a communication + * error occurs. + * Input: msg - The error message + * Output: None + * Author: Tobias Blomberg / SM0SVX + * Created: 2004-03-07 + * Remarks: + * Bugs: + *---------------------------------------------------------------------------- + */ +void ModuleEchoLink::onError(const string& msg) +{ + cerr << "*** ERROR: " << msg << endl; + + if (pending_connect_id > 0) + { + stringstream ss; + ss << "lookup_failed " << pending_connect_id; + processEvent(ss.str()); + } + +} /* onError */ + + + +/* + *---------------------------------------------------------------------------- + * Method: onIncomingConnection + * Purpose: Called by the EchoLink::Dispatcher object when a new remote + * connection is coming in. + * Input: callsign - The callsign of the remote station + * name - The name of the remote station + * priv - A private string for passing connection parameters + * Output: None + * Author: Tobias Blomberg / SM0SVX + * Created: 2004-03-07 + * Remarks: + * Bugs: + *---------------------------------------------------------------------------- + */ +void ModuleEchoLink::onIncomingConnection(const IpAddress& ip, + const string& callsign, + const string& name, + const string& priv) +{ + cout << "Incoming EchoLink connection from " << callsign + << " (" << name << ") at " << ip << "\n"; + + if (regexec(drop_incoming_regex, callsign.c_str(), 0, 0, 0) == 0) + { + cerr << "*** WARNING: Dropping incoming connection due to configuration.\n"; + return; + } + + if (qsos.size() >= max_connections) + { + cerr << "*** WARNING: Ignoring incoming connection (too many " + "connections)\n"; + return; + } + + const StationData *station; + StationData tmp_stn_data; + if (ip.isWithinSubet(allow_ip)) + { + tmp_stn_data.setIp(ip); + tmp_stn_data.setCallsign(callsign); + station = &tmp_stn_data; + } + else + { + // Check if the incoming callsign is valid + station = dir->findCall(callsign); + if (station == 0) + { + getDirectoryList(); + return; + } + } + + if (station->ip() != ip) + { + cerr << "*** WARNING: Ignoring incoming connection from " << callsign + << " since the IP address registered in the directory server " + << "(" << station->ip() << ") is not the same as the remote IP " + << "address (" << ip << ") of the incoming connection\n"; + getDirectoryList(); + return; + } + + // Create a new Qso object to accept the connection + QsoImpl *qso = new QsoImpl(*station, this); + if (!qso->initOk()) + { + delete qso; + cerr << "*** ERROR: Creation of Qso object failed\n"; + return; + } + qsos.push_back(qso); + updateEventVariables(); + qso->setRemoteCallsign(callsign); + qso->setRemoteName(name); + qso->setRemoteParams(priv); + qso->setListenOnly(!listen_only_valve->isOpen()); + qso->stateChange.connect(mem_fun(*this, &ModuleEchoLink::onStateChange)); + qso->chatMsgReceived.connect( + mem_fun(*this, &ModuleEchoLink::onChatMsgReceived)); + qso->isReceiving.connect(mem_fun(*this, &ModuleEchoLink::onIsReceiving)); + qso->audioReceivedRaw.connect( + mem_fun(*this, &ModuleEchoLink::audioFromRemoteRaw)); + qso->destroyMe.connect(mem_fun(*this, &ModuleEchoLink::destroyQsoObject)); + + splitter->addSink(qso); + selector->addSource(qso); + selector->enableAutoSelect(qso, 0); + + if (qsos.size() > max_qsos) + { + qso->reject(false); + return; + } + + // Check if it is a station that connects very often senselessly + if ((num_con_max > 0) && !numConCheck(callsign)) + { + qso->reject(false); + return; + } + + if ((regexec(reject_incoming_regex, callsign.c_str(), 0, 0, 0) == 0) || + (regexec(accept_incoming_regex, callsign.c_str(), 0, 0, 0) != 0) || + (reject_conf && (name.size() > 3) && + (name.rfind("CONF") == (name.size()-4)))) + { + qso->reject(true); + return; + } + + if (!isActive()) + { + remote_activation = true; + } + + if (!activateMe()) + { + qso->reject(false); + cerr << "*** WARNING: Could not accept incoming connection from " + << callsign + << " since the frontend was busy doing something else.\n"; + return; + } + + qso->accept(); + broadcastTalkerStatus(); + updateDescription(); + + if (LocationInfo::has_instance()) + { + list<string> call_list; + listQsoCallsigns(call_list); + + LocationInfo::instance()->updateQsoStatus(2, callsign, name, call_list); + } + + checkIdle(); + +} /* onIncomingConnection */ + + +/* + *---------------------------------------------------------------------------- + * Method: onStateChange + * Purpose: Called by the EchoLink::QsoImpl object when a state change has + * occured on the connection. + * Input: qso - The QSO object + * qso_state - The new QSO connection state + * Output: None + * Author: Tobias Blomberg / SM0SVX + * Created: 2006-03-12 + * Remarks: + * Bugs: + *---------------------------------------------------------------------------- + */ +void ModuleEchoLink::onStateChange(QsoImpl *qso, Qso::State qso_state) +{ + switch (qso_state) + { + case Qso::STATE_DISCONNECTED: + { + vector<QsoImpl*>::iterator it = find(qsos.begin(), qsos.end(), qso); + assert (it != qsos.end()); + qsos.erase(it); + it=qsos.begin(); + qsos.insert(it,qso); + updateEventVariables(); + + if (!qso->connectionRejected()) + { + last_disc_stn = qso->stationData(); + } + + if (remote_activation && + (qsos.back()->currentState() == Qso::STATE_DISCONNECTED)) + { + deactivateMe(); + } + + if (autocon_timer != 0) + { + autocon_timer->setTimeout(autocon_time); + } + + broadcastTalkerStatus(); + updateDescription(); + break; + } + + default: + updateEventVariables(); + break; + } +} /* ModuleEchoLink::onStateChange */ + + +/* + *---------------------------------------------------------------------------- + * Method: onChatMsgReceived + * Purpose: Called by the EchoLink::Qso object when a chat message has been + * received from the remote station. + * Input: qso - The QSO object + * msg - The received message + * Output: None + * Author: Tobias Blomberg / SM0SVX + * Created: 2004-05-04 + * Remarks: + * Bugs: + *---------------------------------------------------------------------------- + */ +void ModuleEchoLink::onChatMsgReceived(QsoImpl *qso, const string& msg) +{ + //cout << "--- EchoLink chat message received from " << qso->remoteCallsign() + // << " ---" << endl + // << msg << endl; + + vector<QsoImpl*>::iterator it; + for (it=qsos.begin(); it!=qsos.end(); ++it) + { + if (*it != qso) + { + (*it)->sendChatData(msg); + } + } + + // Escape TCL control characters + string escaped(msg); + replaceAll(escaped, "\\", "\\\\"); + replaceAll(escaped, "{", "\\{"); + replaceAll(escaped, "}", "\\}"); + stringstream ss; + // FIXME: This TCL specific code should not be here + ss << "chat_received [subst -nocommands -novariables {"; + ss << escaped; + ss << "}]"; + bridge.handleChatMessage(escaped.c_str()); + processEvent(ss.str()); +} /* onChatMsgReceived */ + + +/* + *---------------------------------------------------------------------------- + * Method: onIsReceiving + * Purpose: Called by the EchoLink::Qso object to indicate whether the + * remote station is transmitting or not. + * Input: qso - The QSO object + * is_receiving - true=remote station is transmitting + * false=remote station is not transmitting + * Output: None + * Author: Tobias Blomberg / SM0SVX + * Created: 2004-03-07 + * Remarks: + * Bugs: + *---------------------------------------------------------------------------- + */ +void ModuleEchoLink::onIsReceiving(bool is_receiving, QsoImpl *qso) +{ + //cerr << qso->remoteCallsign() << ": EchoLink receiving: " + // << (is_receiving ? "TRUE" : "FALSE") << endl; + + stringstream ss; + ss << "is_receiving " << (is_receiving ? "1" : "0"); + processEvent(ss.str()); + + if ((talker == 0) && is_receiving) + { + if (reject_conf) + { + string name = qso->remoteName(); + if ((name.size() > 3) && (name.rfind("CONF") == (name.size()-4))) + { + qso->sendChatData("Connects from a conference are not allowed"); + qso->disconnect(); + return; + } + } + talker = qso; + broadcastTalkerStatus(); + } + + if (talker == qso) + { + if (!is_receiving) + { + talker = findFirstTalker(); + if (talker != 0) + { + is_receiving = true; + } + broadcastTalkerStatus(); + } + } +} /* onIsReceiving */ + + +void ModuleEchoLink::destroyQsoObject(QsoImpl *qso) +{ + //cout << qso->remoteCallsign() << ": Destroying QSO object" << endl; + string callsign = qso->remoteCallsign(); + + splitter->removeSink(qso); + selector->removeSource(qso); + + vector<QsoImpl*>::iterator it = find(qsos.begin(), qsos.end(), qso); + assert (it != qsos.end()); + qsos.erase(it); + + updateEventVariables(); + delete qso; + + if (talker == qso) + { + talker = findFirstTalker(); + } + + it = find(outgoing_con_pending.begin(), outgoing_con_pending.end(), qso); + if (it != outgoing_con_pending.end()) + { + outgoing_con_pending.erase(it); + } + + qso = 0; + + //broadcastTalkerStatus(); + //updateDescription(); + + if (LocationInfo::has_instance()) + { + list<string> call_list; + listQsoCallsigns(call_list); + + LocationInfo::instance()->updateQsoStatus(0, callsign, "", call_list); + } + + checkIdle(); + +} /* ModuleEchoLink::destroyQsoObject */ + + +/* + *---------------------------------------------------------------------------- + * Method: getDirectoryList + * Purpose: Initiate the process of getting a directory list from the + * directory server. A timer is also setup to automatically + * refresh the directory listing. + * Input: timer - The timer instance (not used) + * Output: None + * Author: Tobias Blomberg / SM0SVX + * Created: 2004-03-07 + * Remarks: + * Bugs: + *---------------------------------------------------------------------------- + */ +void ModuleEchoLink::getDirectoryList(Timer *timer) +{ + delete dir_refresh_timer; + dir_refresh_timer = 0; + + if ((dir->status() == StationData::STAT_ONLINE) || + (dir->status() == StationData::STAT_BUSY)) + { + dir->getCalls(); + + dir_refresh_timer = new Timer(600000); + dir_refresh_timer->expired.connect( + mem_fun(*this, &ModuleEchoLink::getDirectoryList)); + } +} /* ModuleEchoLink::getDirectoryList */ + + +void ModuleEchoLink::createOutgoingConnection(const StationData &station) +{ + if (station.callsign() == mycall) + { + cerr << "Cannot connect to myself (" << mycall << "/" << station.id() + << ")...\n"; + processEvent("self_connect"); + return; + } + + if ((regexec(reject_outgoing_regex, station.callsign().c_str(), + 0, 0, 0) == 0) || + (regexec(accept_outgoing_regex, station.callsign().c_str(), + 0, 0, 0) != 0)) + { + cerr << "Rejecting outgoing connection to " << station.callsign() << " (" + << station.id() << ")\n"; + stringstream ss; + ss << "reject_outgoing_connection " << station.callsign(); + processEvent(ss.str()); + return; + } + + if (qsos.size() >= max_qsos) + { + cerr << "Couldn't connect to " << station.callsign() << " due to the " + << "number of active connections (" << qsos.size() << " > " + << max_qsos << ")" << endl; + processEvent("no_more_connections_allowed"); + return; + } + + cout << "Connecting to " << station.callsign() << " (" << station.id() + << ")\n"; + + QsoImpl *qso = 0; + + vector<QsoImpl*>::iterator it; + for (it=qsos.begin(); it!=qsos.end(); ++it) + { + if ((*it)->remoteCallsign() == station.callsign()) + { + if ((*it)->currentState() != Qso::STATE_DISCONNECTED) + { + cerr << "*** WARNING: Already connected to " << station.callsign() + << ". Ignoring connect request.\n"; + stringstream ss; + ss << "already_connected_to " << station.callsign(); + processEvent(ss.str()); + return; + } + qso = *it; + qsos.erase(it); + qsos.push_back(qso); + break; + } + } + + if (qso == 0) + { + qso = new QsoImpl(station, this); + if (!qso->initOk()) + { + delete qso; + cerr << "*** ERROR: Creation of Qso failed\n"; + processEvent("internal_error"); + return; + } + qsos.push_back(qso); + updateEventVariables(); + qso->setRemoteCallsign(station.callsign()); + qso->setListenOnly(!listen_only_valve->isOpen()); + qso->stateChange.connect(mem_fun(*this, &ModuleEchoLink::onStateChange)); + qso->chatMsgReceived.connect( + mem_fun(*this, &ModuleEchoLink::onChatMsgReceived)); + qso->isReceiving.connect(mem_fun(*this, &ModuleEchoLink::onIsReceiving)); + qso->audioReceivedRaw.connect( + mem_fun(*this, &ModuleEchoLink::audioFromRemoteRaw)); + qso->destroyMe.connect(mem_fun(*this, &ModuleEchoLink::destroyQsoObject)); + + splitter->addSink(qso); + selector->addSource(qso); + selector->enableAutoSelect(qso, 0); + } + + stringstream ss; + ss << "connecting_to " << qso->remoteCallsign(); + processEvent(ss.str()); + outgoing_con_pending.push_back(qso); + + if (LocationInfo::has_instance()) + { + stringstream info; + info << station.id(); + + list<string> call_list; + listQsoCallsigns(call_list); + + LocationInfo::instance()->updateQsoStatus(1, station.callsign(), + info.str(), call_list); + } + + checkIdle(); + +} /* ModuleEchoLink::createOutgoingConnection */ + + +void ModuleEchoLink::audioFromRemoteRaw(Qso::RawPacket *packet, + QsoImpl *qso) +{ + if (!listen_only_valve->isOpen()) + { + return; + } + + if ((qso == talker) && !squelch_is_open) + { + vector<QsoImpl*>::iterator it; + for (it=qsos.begin(); it!=qsos.end(); ++it) + { + if (*it != qso) + { + (*it)->sendAudioRaw(packet); + } + } + } +} /* ModuleEchoLink::audioFromRemoteRaw */ + + +QsoImpl *ModuleEchoLink::findFirstTalker(void) const +{ + vector<QsoImpl*>::const_iterator it; + for (it=qsos.begin(); it!=qsos.end(); ++it) + { + if ((*it)->receivingAudio()) + { + return *it; + } + } + + return 0; + +} /* ModuleEchoLink::findFirstTalker */ + + +void ModuleEchoLink::broadcastTalkerStatus(void) +{ + if (max_qsos < 2) + { + return; + } + + stringstream msg; + msg << "SvxLink " << SVXLINK_VERSION << " - " << mycall + << " (" << numConnectedStations() << ")\n\n"; + + if (squelch_is_open && listen_only_valve->isOpen()) + { + const char* sysop_name = bridge.getTalker(); + msg << "> " << mycall << " " << sysop_name << "\n\n"; + } + else + { + if (talker != 0) + { + msg << "> " << talker->remoteCallsign() << " " + << talker->remoteName() << "\n\n"; + bridge.setTalker(talker->remoteCallsign().c_str(), talker->remoteName().c_str()); + } + msg << mycall << " "; + if (!listen_only_valve->isOpen()) + { + msg << "[listen only] "; + } + msg << sysop_name << "\n"; + } + + vector<QsoImpl*>::const_iterator it; + for (it=qsos.begin(); it!=qsos.end(); ++it) + { + if ((*it)->currentState() == Qso::STATE_DISCONNECTED) + { + continue; + } + if ((*it != talker) || squelch_is_open) + { + msg << (*it)->remoteCallsign() << " " + << (*it)->remoteName() << "\n"; + } + } + + for (it=qsos.begin(); it!=qsos.end(); ++it) + { + (*it)->sendInfoData(msg.str()); + } + +} /* ModuleEchoLink::broadcastTalkerStatus */ + + +void ModuleEchoLink::updateDescription(void) +{ + if (max_qsos < 2) + { + return; + } + + string desc(location); + if (numConnectedStations() > 0) + { + stringstream sstr; + sstr << " (" << numConnectedStations() << ")"; + desc.resize(Directory::MAX_DESCRIPTION_SIZE - sstr.str().size(), ' '); + desc += sstr.str(); + } + + dir->setDescription(desc); + dir->refreshRegistration(); + +} /* ModuleEchoLink::updateDescription */ + + +void ModuleEchoLink::updateEventVariables(void) +{ + stringstream ss; + ss << numConnectedStations(); + string var_name(name()); + var_name += "::num_connected_stations"; + setEventVariable(var_name, ss.str()); +} /* ModuleEchoLink::updateEventVariables */ + + +void ModuleEchoLink::connectByCallsign(string cmd) +{ + stringstream ss; + + if (cmd.length() < 5) + { + ss << "cbc_too_short_cmd " << cmd; + processEvent(ss.str()); + return; + } + + string code; + bool exact; + if (cmd[cmd.size()-1] == '*') + { + code = cmd.substr(2, cmd.size() - 3); + exact = false; + } + else + { + code = cmd.substr(2); + exact = true; + } + + cout << "Looking up callsign code: " << code << " " + << (exact ? "(exact match)" : "(wildcard match)") << endl; + dir->findStationsByCode(cbc_stns, code, exact); + cout << "Found " << cbc_stns.size() << " stations:\n"; + StnList::const_iterator it; + int cnt = 0; + for (it = cbc_stns.begin(); it != cbc_stns.end(); ++it) + { + cout << *it << endl; + if (++cnt >= 9) + { + break; + } + } + + if (cbc_stns.size() == 0) + { + ss << "cbc_no_match " << code; + processEvent(ss.str()); + return; + } + + if (cbc_stns.size() > 9) + { + cout << "Too many matches. The search must be narrowed down.\n"; + processEvent("cbc_too_many_matches"); + return; + } + + ss << "cbc_list [list"; + for (it = cbc_stns.begin(); it != cbc_stns.end(); ++it) + { + ss << " " << (*it).callsign(); + } + ss << "]"; + processEvent(ss.str()); + state = STATE_CONNECT_BY_CALL; + delete cbc_timer; + cbc_timer = new Timer(60000); + cbc_timer->expired.connect(mem_fun(*this, &ModuleEchoLink::cbcTimeout)); + +} /* ModuleEchoLink::connectByCallsign */ + + +void ModuleEchoLink::handleConnectByCall(const string& cmd) +{ + if (cmd.empty()) + { + processEvent("cbc_aborted"); + cbc_stns.clear(); + delete cbc_timer; + cbc_timer = 0; + state = STATE_NORMAL; + return; + } + + unsigned idx = static_cast<unsigned>(atoi(cmd.c_str())); + stringstream ss; + + if (idx == 0) + { + ss << "cbc_list [list"; + StnList::const_iterator it; + for (it = cbc_stns.begin(); it != cbc_stns.end(); ++it) + { + ss << " " << (*it).callsign(); + } + ss << "]"; + processEvent(ss.str()); + cbc_timer->reset(); + return; + } + + if (idx > cbc_stns.size()) + { + ss << "cbc_index_out_of_range " << idx; + processEvent(ss.str()); + cbc_timer->reset(); + return; + } + + createOutgoingConnection(cbc_stns[idx-1]); + cbc_stns.clear(); + delete cbc_timer; + cbc_timer = 0; + state = STATE_NORMAL; +} /* ModuleEchoLink::handleConnectByCall */ + + +void ModuleEchoLink::cbcTimeout(Timer *t) +{ + delete cbc_timer; + cbc_timer = 0; + cbc_stns.clear(); + state = STATE_NORMAL; + cout << "Connect by call command timeout\n"; + processEvent("cbc_timeout"); +} /* ModuleEchoLink::cbcTimeout */ + + +void ModuleEchoLink::disconnectByCallsign(const string &cmd) +{ + if ((cmd.size() != 1) || qsos.empty()) + { + commandFailed(cmd); + return; + } + + stringstream ss; + ss << "dbc_list [list"; + vector<QsoImpl*>::iterator it; + for (it=qsos.begin(); it!=qsos.end(); ++it) + { + if ((*it)->currentState() != Qso::STATE_DISCONNECTED) + { + ss << " " << (*it)->remoteCallsign(); + } + } + ss << "]"; + processEvent(ss.str()); + state = STATE_DISCONNECT_BY_CALL; + delete dbc_timer; + dbc_timer = new Timer(60000); + dbc_timer->expired.connect(mem_fun(*this, &ModuleEchoLink::dbcTimeout)); +} /* ModuleEchoLink::disconnectByCallsign */ + + +void ModuleEchoLink::handleDisconnectByCall(const string& cmd) +{ + if (cmd.empty()) + { + processEvent("dbc_aborted"); + delete dbc_timer; + dbc_timer = 0; + state = STATE_NORMAL; + return; + } + + unsigned idx = static_cast<unsigned>(atoi(cmd.c_str())); + stringstream ss; + + if (idx == 0) + { + ss << "dbc_list [list"; + vector<QsoImpl*>::const_iterator it; + for (it = qsos.begin(); it != qsos.end(); ++it) + { + ss << " " << (*it)->remoteCallsign(); + } + ss << "]"; + processEvent(ss.str()); + dbc_timer->reset(); + return; + } + + if (idx > qsos.size()) + { + ss << "dbc_index_out_of_range " << idx; + processEvent(ss.str()); + dbc_timer->reset(); + return; + } + + qsos[idx-1]->disconnect(); + delete dbc_timer; + dbc_timer = 0; + state = STATE_NORMAL; + +} /* ModuleEchoLink::handleDisconnectByCall */ + + +void ModuleEchoLink::dbcTimeout(Timer *t) +{ + delete dbc_timer; + dbc_timer = 0; + state = STATE_NORMAL; + cout << "Disconnect by call command timeout\n"; + processEvent("dbc_timeout"); +} /* ModuleEchoLink::dbcTimeout */ + + +int ModuleEchoLink::numConnectedStations(void) +{ + int cnt = 0; + vector<QsoImpl*>::iterator it; + for (it=qsos.begin(); it!=qsos.end(); ++it) + { + if ((*it)->currentState() != Qso::STATE_DISCONNECTED) + { + ++cnt; + } + } + + return cnt; + +} /* ModuleEchoLink::numConnectedStations */ + + +int ModuleEchoLink::listQsoCallsigns(list<string>& call_list) +{ + call_list.clear(); + vector<QsoImpl*>::iterator it; + for (it=qsos.begin(); it!=qsos.end(); ++it) + { + call_list.push_back((*it)->remoteCallsign()); + } + + return call_list.size(); + +} /* ModuleEchoLink::listQsoCallsigns */ + + +void ModuleEchoLink::handleCommand(const string& cmd) +{ + if (cmd[0] == '0') // Help + { + playHelpMsg(); + } + else if (cmd[0] == '1') // Connection status + { + if (cmd.size() != 1) + { + commandFailed(cmd); + return; + } + + stringstream ss; + ss << "list_connected_stations [list"; + vector<QsoImpl*>::iterator it; + for (it=qsos.begin(); it!=qsos.end(); ++it) + { + if ((*it)->currentState() != Qso::STATE_DISCONNECTED) + { + ss << " " << (*it)->remoteCallsign(); + } + } + ss << "]"; + processEvent(ss.str()); + } + else if (cmd[0] == '2') // Play own node id + { + if (cmd.size() != 1) + { + commandFailed(cmd); + return; + } + + stringstream ss; + ss << "play_node_id "; + const StationData *station = dir->findCall(dir->callsign()); + ss << (station ? station->id() : 0); + processEvent(ss.str()); + } + else if (cmd[0] == '3') // Random connect + { + stringstream ss; + + if (cmd.size() != 2) + { + commandFailed(cmd); + return; + } + + vector<StationData> nodes; + + if (cmd[1] == '1') // Random connect to link or repeater + { + const list<StationData>& links = dir->links(); + const list<StationData>& repeaters = dir->repeaters(); + list<StationData>::const_iterator it; + for (it=links.begin(); it!=links.end(); it++) + { + nodes.push_back(*it); + } + for (it=repeaters.begin(); it!=repeaters.end(); it++) + { + nodes.push_back(*it); + } + } + else if (cmd[1] == '2') // Random connect to conference + { + const list<StationData>& conferences = dir->conferences(); + list<StationData>::const_iterator it; + for (it=conferences.begin(); it!=conferences.end(); it++) + { + nodes.push_back(*it); + } + } + else + { + commandFailed(cmd); + return; + } + + double count = nodes.size(); + if (count > 0) + { + srand(time(NULL)); + // coverity[dont_call] + size_t random_idx = (size_t)(count * ((double)rand() / (1.0 + RAND_MAX))); + StationData station = nodes[random_idx]; + + cout << "Creating random connection to node:\n"; + cout << station << endl; + + createOutgoingConnection(station); + } + else + { + commandFailed(cmd); + return; + } + } + else if (cmd[0] == '4') // Reconnect to the last disconnected station + { + if ((cmd.size() != 1) || last_disc_stn.callsign().empty()) + { + commandFailed(cmd); + return; + } + + cout << "Trying to reconnect to " << last_disc_stn.callsign() << endl; + connectByNodeId(last_disc_stn.id()); + } + else if (cmd[0] == '5') // Listen only + { + if (cmd.size() < 2) + { + commandFailed(cmd); + return; + } + + bool activate = (cmd[1] != '0'); + + vector<QsoImpl*>::iterator it; + for (it=qsos.begin(); it!=qsos.end(); ++it) + { + (*it)->setListenOnly(activate); + } + + stringstream ss; + ss << "listen_only " << (!listen_only_valve->isOpen() ? "1 " : "0 ") + << (activate ? "1" : "0"); + processEvent(ss.str()); + + listen_only_valve->setOpen(!activate); + } + else if (cmd[0] == '6') // Connect by callsign + { + connectByCallsign(cmd); + } + else if (cmd[0] == '7') // Disconnect by callsign + { + disconnectByCallsign(cmd); + } + else + { + stringstream ss; + ss << "unknown_command " << cmd; + processEvent(ss.str()); + } + +} /* ModuleEchoLink::handleCommand */ + + +void ModuleEchoLink::commandFailed(const string& cmd) +{ + stringstream ss; + ss << "command_failed " << cmd; + processEvent(ss.str()); +} /* ModuleEchoLink::commandFailed */ + + +void ModuleEchoLink::connectByNodeId(int node_id) +{ + if ((dir->status() == StationData::STAT_OFFLINE) || + (dir->status() == StationData::STAT_UNKNOWN)) + { + cout << "*** ERROR: Directory server offline (status=" + << dir->statusStr() << "). Can't create outgoing connection.\n"; + processEvent("directory_server_offline"); + return; + } + + const StationData *station = dir->findStation(node_id); + if (station != 0) + { + createOutgoingConnection(*station); + } + else + { + cout << "EchoLink ID " << node_id << " is not in the list. " + "Refreshing the list...\n"; + getDirectoryList(); + pending_connect_id = node_id; + } +} /* ModuleEchoLink::connectByNodeId */ + + +void ModuleEchoLink::checkIdle(void) +{ + setIdle(qsos.empty() && + logicIsIdle() && + (state == STATE_NORMAL)); +} /* ModuleEchoLink::checkIdle */ + + +/* + *---------------------------------------------------------------------------- + * Method: checkAutoCon + * Purpose: Initiate the process of connecting to autocon_echolink_id + * Input: timer - the timer instance (not used) + * Output: None + * Author: Robbie De Lise / ON4SAX + * Created: 2010-07-30 + * Remarks: + * Bugs: + *---------------------------------------------------------------------------- + */ +void ModuleEchoLink::checkAutoCon(Timer *) +{ + // Only try to activate the link if we are online and not + // currently connected to any station. A connection will only be attempted + // if module activation is successful. + if ((dir->status() == StationData::STAT_ONLINE) + && (numConnectedStations() == 0) + && activateMe()) + { + cout << "ModuleEchoLink: Trying autoconnect to " + << autocon_echolink_id << "\n"; + connectByNodeId(autocon_echolink_id); + } +} /* ModuleEchoLink::checkAutoCon */ + + +bool ModuleEchoLink::numConCheck(const std::string &callsign) +{ + // Get current time + struct timeval con_time; + gettimeofday(&con_time, NULL); + + // Refresh connect watch list + numConUpdate(); + + NumConMap::iterator cit = num_con_map.find(callsign); + if (cit != num_con_map.end()) + { + // Get an alias (reference) to the callsign and NumConStn objects + const string &t_callsign = (*cit).first; + NumConStn &stn = (*cit).second; + + // Calculate time difference from last connection + struct timeval diff_tv; + timersub(&con_time, &stn.last_con, &diff_tv); + + // Bug in Win-Echolink? Number of connect requests up to 5/sec + // do not count stations if it's requesting 3-5/seconds + if (diff_tv.tv_sec > 3) + { + ++stn.num_con; + stn.last_con = con_time; + cout << "### Station " << t_callsign << ", count " << stn.num_con << " of " + << num_con_max << " possible number of connects" << endl; + } + + // Number of connects are too high + if (stn.num_con > num_con_max) + { + time_t next = con_time.tv_sec + num_con_block_time; + char time_str[64]; + strftime(time_str, sizeof(time_str), "%c", localtime(&next)); + cerr << "*** WARNING: Ingnoring incoming connection because " + << "the station (" << callsign << ") has connected " + << "to often (" << stn.num_con << " times). " + << "Next connect is possible after " << time_str << ".\n"; + return false; + } + } + else + { + // Insert initial entry on first connect + cout << "### Register incoming station, count 1 of " << num_con_max + << " possible number of connects" << endl; + num_con_map.insert(make_pair(callsign, NumConStn(1, con_time))); + } + + return true; + +} /* ModuleEchoLink::numConCheck */ + + +void ModuleEchoLink::numConUpdate(void) +{ + // Get current time + struct timeval now; + gettimeofday(&now, NULL); + + NumConMap::iterator cit = num_con_map.begin(); + while (cit != num_con_map.end()) + { + // Get an alias (reference) to the callsign and NumConStn objects + const string &t_callsign = (*cit).first; + const NumConStn &stn = (*cit).second; + + struct timeval remove_at = stn.last_con; + if (stn.num_con > num_con_max) + { + remove_at.tv_sec += num_con_block_time; + } + else + { + remove_at.tv_sec += num_con_ttl; + } + + // If the entry have timed out, delete it + if (timercmp(&remove_at, &now, <)) + { + cout << "### Delete " << t_callsign << " from watchlist" << endl; + num_con_map.erase(cit++); + } + else + { + if (stn.num_con > num_con_max) + { + cout << "### " << t_callsign << " is blocked" << endl; + } + ++cit; + } + } + + num_con_update_timer->reset(); + +} /* ModuleEchoLink::numConUpdate */ + + +void ModuleEchoLink::replaceAll(std::string &str, const std::string &from, + const std::string &to) const +{ + if(from.empty()) + { + return; + } + size_t start_pos = 0; + while((start_pos = str.find(from, start_pos)) != std::string::npos) + { + str.replace(start_pos, from.length(), to); + start_pos += to.length(); + } +} /* ModuleEchoLink::replaceAll */ + + + +/* + * This file has not been truncated + */ diff --git a/SVXLink/echolink/ModuleEchoLink.h b/SVXLink/echolink/ModuleEchoLink.h new file mode 100644 index 0000000..ed9a4ff --- /dev/null +++ b/SVXLink/echolink/ModuleEchoLink.h @@ -0,0 +1,276 @@ +/** +@file ModuleEchoLink.h +@brief A module that provides EchoLink connection possibility +@author Tobias Blomberg / SM0SVX +@date 2004-03-07 + +\verbatim +A module (plugin) for the multi purpose tranciever frontend system. +Copyright (C) 2004-2014 Tobias Blomberg / SM0SVX + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +\endverbatim +*/ + + +#ifndef MODULE_ECHOLINK_INCLUDED +#define MODULE_ECHOLINK_INCLUDED + + +/**************************************************************************** + * + * System Includes + * + ****************************************************************************/ + +#include <string> +#include <vector> + +#include <sys/types.h> +#include <regex.h> + + +/**************************************************************************** + * + * Project Includes + * + ****************************************************************************/ + +#include <Module.h> +#include <EchoLinkQso.h> +#include <EchoLinkStationData.h> + + +/**************************************************************************** + * + * Local Includes + * + ****************************************************************************/ + +#include "version/SVXLINK.h" +#include "BrandMeisterBridge.h" + +/**************************************************************************** + * + * Forward declarations + * + ****************************************************************************/ + +namespace Async +{ + class Timer; + class AudioSplitter; + class AudioValve; + class AudioSelector; +}; +namespace EchoLink +{ + class Directory; + class StationData; + class Proxy; +}; + + +/**************************************************************************** + * + * Namespace + * + ****************************************************************************/ + +//namespace MyNameSpace +//{ + + +/**************************************************************************** + * + * Forward declarations of classes inside of the declared namespace + * + ****************************************************************************/ + +class MsgHandler; +class QsoImpl; +class LocationInfo; + + +/**************************************************************************** + * + * Defines & typedefs + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Exported Global Variables + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Class definitions + * + ****************************************************************************/ + +/** +@brief A module for providing EchoLink connections +@author Tobias Blomberg +@date 2004-03-07 +*/ +class ModuleEchoLink : public Module +{ + public: + ModuleEchoLink(void *dl_handle, Logic *logic, const std::string& cfg_name); + ~ModuleEchoLink(void); + bool initialize(void); + const char *compiledForVersion(void) const { return SVXLINK_VERSION; } + + + protected: + /** + * @brief Notify the module that the logic core idle state has changed + * @param is_idle Set to \em true if the logic core is idle or else + * \em false. + * + * This function is called by the logic core when the idle state changes. + */ + virtual void logicIdleStateChanged(bool is_idle); + + + private: + + BrandMeisterBridge bridge; + + typedef enum + { + STATE_NORMAL, + STATE_CONNECT_BY_CALL, + STATE_DISCONNECT_BY_CALL + } State; + typedef std::vector<EchoLink::StationData> StnList; + struct NumConStn + { + unsigned num_con; + struct timeval last_con; + + NumConStn(unsigned num, struct timeval &t) : num_con(num), last_con(t) {} + }; + typedef std::map<const std::string, NumConStn> NumConMap; + + static const int DEFAULT_AUTOCON_TIME = 3*60*1000; // Three minutes + + EchoLink::Directory *dir; + Async::Timer *dir_refresh_timer; + std::string mycall; + std::string location; + std::string sysop_name; + std::string description; + std::string allow_ip; + bool remote_activation; + int pending_connect_id; + std::string last_message; + std::vector<QsoImpl*> outgoing_con_pending; + std::vector<QsoImpl*> qsos; + unsigned max_connections; + unsigned max_qsos; + QsoImpl *talker; + bool squelch_is_open; + State state; + StnList cbc_stns; + Async::Timer *cbc_timer; + Async::Timer *dbc_timer; + regex_t *drop_incoming_regex; + regex_t *reject_incoming_regex; + regex_t *accept_incoming_regex; + regex_t *reject_outgoing_regex; + regex_t *accept_outgoing_regex; + EchoLink::StationData last_disc_stn; + Async::AudioSplitter *splitter; + Async::AudioValve *listen_only_valve; + Async::AudioSelector *selector; + unsigned num_con_max; + time_t num_con_ttl; + time_t num_con_block_time; + NumConMap num_con_map; + Async::Timer *num_con_update_timer; + bool reject_conf; + int autocon_echolink_id; + int autocon_time; + Async::Timer *autocon_timer; + EchoLink::Proxy *proxy; + + void moduleCleanup(void); + void activateInit(void); + void deactivateCleanup(void); + //bool dtmfDigitReceived(char digit, int duration); + void dtmfCmdReceived(const std::string& cmd); + void dtmfCmdReceivedWhenIdle(const std::string &cmd); + void squelchOpen(bool is_open); + int audioFromRx(float *samples, int count); + void allMsgsWritten(void); + + void onStatusChanged(EchoLink::StationData::Status status); + void onStationListUpdated(void); + void onError(const std::string& msg); + void onIncomingConnection(const Async::IpAddress& ip, + const std::string& callsign, const std::string& name, + const std::string& priv); + void onStateChange(QsoImpl *qso, EchoLink::Qso::State qso_state); + void onChatMsgReceived(QsoImpl *qso, const std::string& msg); + void onIsReceiving(bool is_receiving, QsoImpl *qso); + void destroyQsoObject(QsoImpl *qso); + + void getDirectoryList(Async::Timer *timer=0); + + void createOutgoingConnection(const EchoLink::StationData &station); + int audioFromRemote(float *samples, int count, QsoImpl *qso); + void audioFromRemoteRaw(EchoLink::Qso::RawPacket *packet, + QsoImpl *qso); + QsoImpl *findFirstTalker(void) const; + void broadcastTalkerStatus(void); + void updateDescription(void); + void updateEventVariables(void); + void connectByCallsign(std::string cmd); + void handleConnectByCall(const std::string& cmd); + void cbcTimeout(Async::Timer *t); + void disconnectByCallsign(const std::string &cmd); + void handleDisconnectByCall(const std::string& cmd); + void dbcTimeout(Async::Timer *t); + int numConnectedStations(void); + int listQsoCallsigns(std::list<std::string>& call_list); + void handleCommand(const std::string& cmd); + void commandFailed(const std::string& cmd); + void connectByNodeId(int node_id); + void checkIdle(void); + void checkAutoCon(Async::Timer *timer=0); + bool numConCheck(const std::string &callsign); + void numConUpdate(void); + void replaceAll(std::string &str, const std::string &from, + const std::string &to) const; + +}; /* class ModuleEchoLink */ + + +//} /* namespace */ + +#endif /* MODULE_ECHOLINK_INCLUDED */ + + + +/* + * This file has not been truncated + */ diff --git a/SVXLink/echolink/PatchCordProxy.cpp b/SVXLink/echolink/PatchCordProxy.cpp new file mode 100644 index 0000000..bb3c453 --- /dev/null +++ b/SVXLink/echolink/PatchCordProxy.cpp @@ -0,0 +1,154 @@ +// Copyright 2015 by Artem Prilutskiy + +#include "PatchCordProxy.h" +#include <stdlib.h> +#include <string.h> + +#include <syslog.h> +#include <stdio.h> + +#define INTERFACE_NAME "me.burnaway.BrandMeister" +#define SERVICE_NAME INTERFACE_NAME +#define OBJECT_PATH "/me/burnaway/BrandMeister" + +// From AutoPatch.cpp +#define AUTOPATCH_LINK_NAME "AutoPatch" + +// From PatchCord.h +#define VALUE_CORD_OUTGOING_SOURCE_ID 1 +#define VALUE_CORD_INCOMING_SOURCE_ID 4 + +PatchCordProxy::PatchCordProxy(uint32_t number) : + number(number), + banner(NULL) +{ + connection = dbus_bus_get(DBUS_BUS_SYSTEM, NULL); +} + +PatchCordProxy::~PatchCordProxy() +{ + free(banner); + dbus_connection_unref(connection); +} + +void PatchCordProxy::setTalkerID(uint32_t value) +{ + getContextBanner(); + setVendorSpecificValue(VALUE_CORD_OUTGOING_SOURCE_ID, value); +} + +uint32_t PatchCordProxy::getTalkerID() +{ + getContextBanner(); + return getVendorSpecificValue(VALUE_CORD_INCOMING_SOURCE_ID); +} + +void PatchCordProxy::getContextBanner() +{ + DBusMessage* message = dbus_message_new_method_call( + SERVICE_NAME, OBJECT_PATH, + INTERFACE_NAME, "getContextList"); + + const char* name = AUTOPATCH_LINK_NAME; + + dbus_message_append_args(message, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_UINT32, &number, + DBUS_TYPE_INVALID); + + DBusPendingCall* pending; + if (dbus_connection_send_with_reply(connection, message, &pending, -1)) + { + dbus_connection_flush(connection); + dbus_message_unref(message); + dbus_pending_call_block(pending); + message = dbus_pending_call_steal_reply(pending); + char** array; + int count; + if ((dbus_message_get_args(message, NULL, + DBUS_TYPE_ARRAY, DBUS_TYPE_STRING, &array, &count, + DBUS_TYPE_INVALID)) && + (count > 0)) + { + free(banner); + banner = strdup(*array); + dbus_free_string_array(array); + } + dbus_pending_call_unref(pending); + } + dbus_message_unref(message); +} + +void PatchCordProxy::setVendorSpecificValue(uint32_t key, uint32_t value) +{ + DBusMessage* message = dbus_message_new_method_call( + SERVICE_NAME, OBJECT_PATH, + INTERFACE_NAME, "setVendorSpecificValue"); + + dbus_message_append_args(message, + DBUS_TYPE_STRING, &banner, + DBUS_TYPE_UINT32, &key, + DBUS_TYPE_UINT32, &value, + DBUS_TYPE_INVALID); + + DBusPendingCall* pending; + if (dbus_connection_send_with_reply(connection, message, &pending, -1)) + { + dbus_connection_flush(connection); + dbus_message_unref(message); + dbus_pending_call_block(pending); + message = dbus_pending_call_steal_reply(pending); + dbus_pending_call_unref(pending); + } + dbus_message_unref(message); + + // Each call of dbus_message_unref removes banner from the heap + banner = NULL; +} + +uint32_t PatchCordProxy::getVendorSpecificValue(uint32_t key) +{ + DBusMessage* message = dbus_message_new_method_call( + SERVICE_NAME, OBJECT_PATH, + INTERFACE_NAME, "getContextData"); + + dbus_message_append_args(message, + DBUS_TYPE_STRING, &banner, + DBUS_TYPE_INVALID); + + uint32_t value = 0; + + DBusPendingCall* pending; + if (dbus_connection_send_with_reply(connection, message, &pending, -1)) + { + dbus_connection_flush(connection); + dbus_message_unref(message); + dbus_pending_call_block(pending); + message = dbus_pending_call_steal_reply(pending); + const char* name; + const char* address; + dbus_uint32_t type; + dbus_uint32_t state; + dbus_uint32_t number; + dbus_uint32_t* values; + int count; + if (dbus_message_get_args(message, NULL, + DBUS_TYPE_STRING, &banner, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_UINT32, &type, + DBUS_TYPE_UINT32, &number, + DBUS_TYPE_STRING, &address, + DBUS_TYPE_UINT32, &state, + DBUS_TYPE_ARRAY, DBUS_TYPE_UINT32, &values, &count, + DBUS_TYPE_INVALID)) + value = values[key]; + dbus_pending_call_unref(pending); + } + + dbus_message_unref(message); + + // Each call of dbus_message_unref removes banner from the heap + banner = NULL; + + return value; +} diff --git a/SVXLink/echolink/PatchCordProxy.h b/SVXLink/echolink/PatchCordProxy.h new file mode 100644 index 0000000..75149f0 --- /dev/null +++ b/SVXLink/echolink/PatchCordProxy.h @@ -0,0 +1,31 @@ +// Copyright 2015 by Artem Prilutskiy + +#ifndef PATCHCORDPROXY_H +#define PATCHCORDPROXY_H + +#include <stdint.h> +#include <dbus/dbus.h> + +class PatchCordProxy +{ + public: + + PatchCordProxy(uint32_t number); + ~PatchCordProxy(); + + void setTalkerID(uint32_t value); + uint32_t getTalkerID(); + + private: + + DBusConnection* connection; + uint32_t number; + char* banner; + + void getContextBanner(); + void setVendorSpecificValue(uint32_t key, uint32_t value); + uint32_t getVendorSpecificValue(uint32_t key); + +}; + +#endif \ No newline at end of file diff --git a/SVXLink/echolink/UserDataStore.cpp b/SVXLink/echolink/UserDataStore.cpp new file mode 100644 index 0000000..9fbe681 --- /dev/null +++ b/SVXLink/echolink/UserDataStore.cpp @@ -0,0 +1,137 @@ +// Copyright 2015 by Artem Prilutskiy + +#include "UserDataStore.h" + +#include <stdlib.h> +#include <stdarg.h> +#include <string.h> +#include <syslog.h> + +#define GET_ID_FOR_CALL 0 +#define GET_CREDENTIALS_FOR_ID 1 + +UserDataStore::UserDataStore(const char* file) : + connection(NULL) +{ + const char* queries[] = + { + // GET_ID_FOR_CALL + "SELECT `ID`" + " FROM Users" + " WHERE `Call` = ?" + " ORDER BY `Priority`" + " LIMIT 1", + // GET_CREDENTIALS_FOR_ID + "SELECT `Call`, `Text`" + " FROM Users" + " WHERE `ID` = ?", + }; + + connection = mysql_init(NULL); + + mysql_options(connection, MYSQL_READ_DEFAULT_FILE, realpath(file, NULL)); + mysql_real_connect(connection, NULL, NULL, NULL, NULL, 0, NULL, CLIENT_REMEMBER_OPTIONS); + + my_bool active = true; + mysql_options(connection, MYSQL_OPT_RECONNECT, &active); + + for (size_t index = 0; index < PREPARED_STATEMENT_COUNT; index ++) + { + const char* query = queries[index]; + statements[index] = mysql_stmt_init(connection); + mysql_stmt_prepare(statements[index], query, strlen(query)); + } + + const char* error = mysql_error(connection); + if ((error != NULL) && (*error != '\0')) + syslog(LOG_CRIT, "MySQL error: %s\n", error); +} + +UserDataStore::~UserDataStore() +{ + for (size_t index = 0; index < PREPARED_STATEMENT_COUNT; index ++) + mysql_stmt_close(statements[index]); + + mysql_close(connection); +} + +// Public methods + +uint32_t UserDataStore::getPrivateIDForCall(const char* call) +{ + uint32_t number; + size_t length = strlen(call); + + return + execute(GET_ID_FOR_CALL, 1, 1, + MYSQL_TYPE_STRING, call, length, + MYSQL_TYPE_LONG, &number) * + number; +} + + +bool UserDataStore::getCredentialsForID(uint32_t number, char* call, char* text) +{ + return + execute(GET_CREDENTIALS_FOR_ID, 1, 2, + MYSQL_TYPE_LONG, &number, + MYSQL_TYPE_STRING, call, LONG_CALLSIGN_LENGTH + 1, + MYSQL_TYPE_STRING, text, SLOW_DATA_TEXT_LENGTH + 1); +} + +// Internals + +MYSQL_BIND* UserDataStore::bind(int count, va_list* arguments) +{ + MYSQL_BIND* bindings = NULL; + if (count > 0) + { + bindings = (MYSQL_BIND*)calloc(count, sizeof(MYSQL_BIND)); + for (int index = 0; index < count; index ++) + { + int type = va_arg(*arguments, int); + bindings[index].buffer_type = (enum_field_types)type; + bindings[index].buffer = va_arg(*arguments, void*); + if ((type == MYSQL_TYPE_STRING) || + (type == MYSQL_TYPE_DATETIME)) + { + bindings[index].buffer_length = va_arg(*arguments, int); + bindings[index].length = &bindings[index].buffer_length; + } + } + } + return bindings; +} + +bool UserDataStore::execute(int index, int count1, int count2, ...) +{ + bool result = true; + MYSQL_STMT* statement = statements[index]; + + va_list arguments; + va_start(arguments, count2); + MYSQL_BIND* parameters = bind(count1, &arguments); + MYSQL_BIND* columns = bind(count2, &arguments); + va_end(arguments); + + if ((parameters && mysql_stmt_bind_param(statement, parameters)) || + (columns && mysql_stmt_bind_result(statement, columns)) || + mysql_stmt_execute(statement) || + (columns && mysql_stmt_store_result(statement))) + { + const char* error = mysql_stmt_error(statement); + syslog(LOG_CRIT, "MySQL error while processing statement %d: %s\n", index, error); + result = false; + } + + if (result && columns) + { + result = !mysql_stmt_fetch(statement); + mysql_stmt_free_result(statement); + } + + free(columns); + free(parameters); + return result; +} + diff --git a/SVXLink/echolink/UserDataStore.h b/SVXLink/echolink/UserDataStore.h new file mode 100644 index 0000000..1f2ed01 --- /dev/null +++ b/SVXLink/echolink/UserDataStore.h @@ -0,0 +1,35 @@ +// Copyright 2015 by Artem Prilutskiy + +#ifndef USERDATASTORE_H +#define USERDATASTORE_H + +#include <stdint.h> +#include <stdarg.h> +#include <mysql/mysql.h> + +#define PREPARED_STATEMENT_COUNT 2 + +#define LONG_CALLSIGN_LENGTH 8 +#define SLOW_DATA_TEXT_LENGTH 20 + +class UserDataStore +{ + public: + + UserDataStore(const char* file); + ~UserDataStore(); + + uint32_t getPrivateIDForCall(const char* call); + bool getCredentialsForID(uint32_t number, char* call, char* text); + + protected: + + MYSQL* connection; + MYSQL_STMT* statements[PREPARED_STATEMENT_COUNT]; + + MYSQL_BIND* bind(int count, va_list* arguments); + bool execute(int index, int count1, int count2, ...); + +}; + +#endif diff --git a/SVXLink/readme.txt b/SVXLink/readme.txt new file mode 100644 index 0000000..93d9b59 --- /dev/null +++ b/SVXLink/readme.txt @@ -0,0 +1,45 @@ +SVXLink patch to bridge with BrandMeister +Copyright 2015 by Artem Prilutskiy +----------------------------------------- + +Put UserDataStore.*, PatchCordProxy.* and BrandMeisterBridge.* into src/modules/echolink + +Add following lines to CMakeLists.txt after "set(MODSRC QsoImpl.cpp)": + +pkg_check_modules(DBUS dbus-1) +include_directories(${DBUS_INCLUDE_DIRS}) +set(LIBS ${LIBS} ${DBUS_LIBRARIES} mysqlclient) +set(MODSRC ${MODSRC} PatchCordProxy.cpp UserDataStore.cpp BrandMeisterBridge.cpp) + +Add following lines to ModuleEchoLink.h: + +[#include "BrandMeisterBridge.h"] + after + [#include "version/SVXLINK.h"] + +[BrandMeisterBridge bridge;] + after + [private:] + + +Add following lines to method "ModuleEchoLink::broadcastTalkerStatus" of ModuleEchoLink.cpp: + +[const char* sysop_name = bridge.getTalker();] + before + [msg << "> " << mycall << " " << sysop_name << "\n\n";] + +[bridge.setTalker(talker->remoteCallsign().c_str(), talker->remoteName().c_str());] + before + [msg << "> " << talker->remoteCallsign() << " " << talker->remoteName() << "\n\n";] + + +Add following lines to method "ModuleEchoLink::onChatMsgReceived" of ModuleEchoLink.cpp: +[bridge.handleChatMessage(escaped.c_str());] + before + [processEvent(ss.str());] + + +Configuration of bridge are hand-coded inside BrandMeisterBridge.cpp: + +Please configure PatchCord to number 10 in BrandMeister.conf +Configuration of MySQL connection should be placed in /opt/BrandMeister/Registry.cnf \ No newline at end of file diff --git a/SVXLink/svxlink.conf b/SVXLink/svxlink.conf new file mode 100644 index 0000000..8e87ebb --- /dev/null +++ b/SVXLink/svxlink.conf @@ -0,0 +1,58 @@ +############################################################################### +# # +# Configuration file for the SvxLink server # +# # +############################################################################### + +[GLOBAL] +MODULE_PATH=/opt/SVXLink/lib/svxlink +CFG_DIR=/opt/SVXLink/etc/svxlink/svxlink.d +LOGICS=SimplexLogic +TIMESTAMP_FORMAT="%c" +CARD_SAMPLE_RATE=16000 + +[SimplexLogic] +TYPE=Simplex +RX=Rx1 +TX=Tx1 +MODULES=ModuleEchoLink +EVENT_HANDLER=/opt/SVXLink/share/svxlink/events.tcl + +SQL_DET=VOX +SQL_START_DELAY=0 +SQL_DELAY=0 +SQL_HANGTIME=2000 +VOX_FILTER_DEPTH=20 +VOX_THRESH=1000 + +CALLSIGN=R3ABM-L +SHORT_IDENT_INTERVAL=60 +LONG_IDENT_INTERVAL=60 +EVENT_HANDLER=/opt/SVXLink/share/svxlink/events.tcl +DEFAULT_LANG=ru_RU +RGR_SOUND_DELAY=0 +FX_GAIN_NORMAL=0 +FX_GAIN_LOW=-12 + +[Rx1] +TYPE=Local +AUDIO_DEV=alsa:dsp0 +AUDIO_CHANNEL=0 +SQL_DET=VOX +SQL_START_DELAY=0 +SQL_DELAY=0 +SQL_HANGTIME=2000 +VOX_FILTER_DEPTH=20 +VOX_THRESH=1000 +DTMF_DEC_TYPE=INTERNAL +DEEMPHASIS=0 +PEAK_METER=1 + +[Tx1] +TYPE=Local +AUDIO_DEV=alsa:dsp1 +AUDIO_CHANNEL=0 +PTT_TYPE=NONE +TIMEOUT=300 +TX_DELAY=500 +PREEMPHASIS=0 diff --git a/SVXLink/svxlink.service b/SVXLink/svxlink.service new file mode 100644 index 0000000..ff3d913 --- /dev/null +++ b/SVXLink/svxlink.service @@ -0,0 +1,17 @@ +[Unit] +Description=SvxLink Server +After=network.target sound.target brandmeister.service + +[Service] +Restart=always +KillMode=process +ExecStart=/opt/SVXLink/bin/svxlink –config=/opt/SVXLink/etc/svxlink/svxlink.conf –logfile=/opt/SVXLink/var/log/svxlink +RestartSec=5 +TimeoutSec=5 +User=svxlink +Group=svxlink +Environment="HOME=/opt/SVXLink" + +[Install] +WantedBy=multi-user.target + diff --git a/Scripts/alterfrn.service b/Scripts/alterfrn.service new file mode 100644 index 0000000..4b719da --- /dev/null +++ b/Scripts/alterfrn.service @@ -0,0 +1,17 @@ +[Unit] +Description=AlterFRN Client +Afer=network.target sound.target + +[Service] +; system.service +Type=simple +ExecStart=/opt/AlterFRN/alterfrn.sh +Restart=on-failure +; system.exec +User=frn +Group=frn +StandardOutput=null +WorkingDirectory=/opt/AlterFRN + +[Install] +WantedBy=multi-user.target diff --git a/Scripts/alterfrn.sh b/Scripts/alterfrn.sh new file mode 100755 index 0000000..b2dba6e --- /dev/null +++ b/Scripts/alterfrn.sh @@ -0,0 +1,2 @@ +#!/bin/bash +./FRNClientConsole | ./callcapture --identity AlterFRN --expression "RX is started: ([A-Z0-9]{3,7})[-,]" --connection Registry.cnf --link 11 diff --git a/Scripts/asound.conf b/Scripts/asound.conf new file mode 100644 index 0000000..2de52e1 --- /dev/null +++ b/Scripts/asound.conf @@ -0,0 +1,51 @@ + +# Record device for SVXLink + +pcm.convert1 { + type rate + slave { + pcm "hw:Loopback,1,2" + rate 8000 + } +} + +pcm.dsp0 { + type plug + slave { + pcm "convert1" + rate 16000 + } +} + + +# Playback device for SVXLink + +pcm.convert2 { + type rate + slave { + pcm "hw:Loopback,0,0" + rate 8000 + } +} + +pcm.dsp1 { + type plug + slave { + pcm "convert2" + rate 16000 + } +} + +# Record device for AlterFRN + +pcm.dsp2 { + type plug + slave.pcm "hw:Loopback,1,6" +} + +# Playback device for AlterFRN + +pcm.dsp3 { + type plug + slave.pcm "hw:Loopback,0,4" +} diff --git a/Scripts/frnconsole.cfg.unix b/Scripts/frnconsole.cfg.unix new file mode 100644 index 0000000..4827275 --- /dev/null +++ b/Scripts/frnconsole.cfg.unix @@ -0,0 +1,168 @@ +# Конфигурационный файл программы FRN-линка (FRNClientConsole) +# Configuration for FRN-link (FRNClientConsole) + +# основное руководство тут http://alterfrn.ucoz.ru/index/freebsd_manual_russian/0-5 +# main manual here http://alterfrn.ucoz.ru/index/freebsd_manual_russian/0-5 +# +# примечания для ARM/Linux тут http://alterfrn.ucoz.ru/index/linux_arm_manual_russian/0-6 +# comments for ARM/Linux here http://alterfrn.ucoz.ru/index/linux_arm_manual_russian/0-6 + + +# Ревизия(revision) r2781 + +[Auth] +Callsign=MY0CALL +OperatorName=Name +EMailAddress=my@email +City=N/A +CityPart= +Password=password +DynamicPassword= +Country=Russian Federation +Description=DSTAR.SU DMR Bridge +BandChannel= +ClientType=CROSSLINK +CharsetName=WINDOWS-1251 + + +[Audio] +#### Имя устройства ввода звука в формате вывода по параметрам командной строки "audioconfig" +#### Input audio device name like output with command line parameter "audioconfig" +InDevice=ALSA:dsp2 + +## Частота дискретизации звука при ввода: +## 8000(рекомендуется, не требуется преобразования), 11025, 12000, 16000, +## 22050, 24000, 32000, 44100(по умолчанию), 48000 +## Input (capture) sample rate: +## 8000(recommended, no need samplerate conversion), 11025, 12000, 16000, +## 22050, 24000, 32000, 44100(default), 48000 +InSampleRate=8000 + +#### Качество преобразования частоты дискретизации в 8Кгц: 0/NONE/NO/N - совсем без качества(по умолчанию); 1/LOW/LO/L - низкое качество; 2/MEDIUM/MED/M - среднее качество; 3/HIGH/HI/H - высокое качество(высокая нагрузка процессора) +#### Quality for input(capture) audio samplerate conversion into 8KHz: 0/NONE/NO/N - no any quality(default); 1/LOW/LO/L - low quality; 2/MEDIUM/MED/M - medium quality; 3/HIGH/HI/H - high qualityо(highest CPU usage) +InQuality=L + +InFactor=1 + + +# Автоматическая регулировка усиления (АРУ) по входу с радиостанции +# Input (capture) Automatic Gain Control (AGC) +InAGCEnabled=no +# level after AGC in percents(%), range 50..100; default 90 +InAGCLevel=99 +# AGC maximal gain in decibells(dB), range 0..60; default 30 +InAGCMaxGain=20 + +# Входной фильтр высоких частот Баттерворта, частота среза 300Гц; перед АРУ +# Input High Pass Filter, Butterworth, 300Hz; before AGC +InHPFEnabled=no +InHPFOrder=5 +InHPFDouble=no + + + + +#### Имя устройства вывода звука в формате вывода по параметрам командной строки "audioconfig" +#### Output audio device name like output with command line parameter "audioconfig" +OutDevice=ALSA:dsp3 + +## Частота дискретизации звука при выводе: +## 8000(рекомендуется, не требуется преобразования), 11025, 12000, 16000, +## 22050, 24000, 32000, 44100(по умолчанию), 48000 +## Output (playback) sample rate: +## 8000(recommended, no need samplerate conversion), 11025, 12000, 16000, +## 22050, 24000, 32000, 44100(default), 48000 +OutSampleRate=8000 + +#### Качество преобразования частоты дискретизации из 8КГц: 0/NONE/NO/N - совсем без качества(по умолчанию); 1/LOW/LO/L - низкое качество; 2/MEDIUM/MED/M - среднее качество; 3/HIGH/HI/H - высокое качество(высокая нагрузка процессора) +#### Quality for output(playback) audio conversion from 8KHz: 0/NONE/NO/N - no any quality(default); 1/LOW/LO/L - low quality; 2/MEDIUM/MED/M - medium quality; 3/HIGH/HI/H - high qualityо(highest CPU usage) +OutQuality=L +OutFactor=1 + +# Автоматическая регулировка усиления (АРУ) по выходу на радиостанцию +# Output (playback) Automatic Gain Control (AGC) +OutAGCEnabled=no +# level after AGC in percents(%), range 50..100; default 90 +OutAGCLevel=99 +# AGC maximal gain in decibells(dB), range 0..60; default 30 +OutAGCMaxGain=40 + +# Выходной фильтр высоких частот Баттерворта, частота среза 300Гц; перед АРУ +# Output High Pass Filter, Butterworth, 300Hz; before AGC +OutHPFEnabled=no +OutHPFOrder=5 +OutHPFDouble=no + + + +[Radio] +PTT= +COS=VOX:1200 +LIGHT= +CTCSSWakeTime=500 +CarrierCatchTime=100 +CarrierLostTime=600 + +[Server] +ServerReconnectCount=3 +ServerReconnectInterval=3000 +ServerAddress=frn.radiocult.ru +ServerPort=10024 +CharsetName=WINDOWS-1251 +#### VisibleStatus: Visible status in server list of clients: AVAILABLE/AV (default), NOTAVAILABLE/NA, ABSENT/AB/AS +VisibleStatus=AV +Network=Radiocult +BackupServerMode=YES +ForcedBackupServerAddress=frn3.radiocult.ru +ForcedBackupServerPort=10027 + + +[Manager] +ManagerAddress=sysman.freeradionetwork.eu +ManagerPort=10025 +DynamicPasswordMode=YES + + +[Internet] +#### ProxyType: режим использования прокси-сервера: NONE/NO/N - прокси не используется (по умолчанию); SYSTEM/SYS/S - используется прокси в соответствии с настройками системы; HTTP/HT/H - используется конкретный прокси +## ProxyType: mode for proxy-server usage: NONE/NO/N - do not use proxy-server (default); SYSTEM/SYS/S - use system settings for proxy; HTTP/HT/H - use certain http-proxy +ProxyType=NONE +# ProxyAddress=192.168.1.1 +# ProxyPort=3128 +# CharsetName=UTF-8 + +[Message] +PrivateAutoResponse= + +[Sounds] +SoundsDir= +SoundCourtesy= +EnableCourtesy= +SoundCourtesyEmptyNet= +EnableCourtesyEmptyNet= +SoundRoger= +EnableRoger= +SoundNoConnection= +EnableNoConnection= +SoundReject= +EnableReject= +SoundError= +EnableError= + + +[System] +CharsetName=UTF-8 + +[Hours] +Enabled=no + + +[Informer] +Enabled=no +Dir=informer +Interval=1200 +Mode=SEQ +SilenceEnabled=no +SilenceInterval=300 +SilenceTime=2000 +