Compare commits

...

4 Commits

11 changed files with 352 additions and 49 deletions

View File

@ -0,0 +1,23 @@
# Defaults for hostapd initscript
#
# WARNING: The DAEMON_CONF setting has been deprecated and will be removed
# in future package releases.
#
# See /usr/share/doc/hostapd/README.Debian for information about alternative
# methods of managing hostapd.
#
# Uncomment and set DAEMON_CONF to the absolute path of a hostapd configuration
# file and hostapd will be started during system boot. An example configuration
# file can be found at /usr/share/doc/hostapd/examples/hostapd.conf.gz
#
DAEMON_CONF="/etc/hostapd/hostapd.conf"
# Additional daemon options to be appended to hostapd command:-
# -d show more debug messages (-dd for even more)
# -K include key data in debug messages
# -t include timestamps in some debug messages
#
# Note that -B (daemon mode) and -P (pidfile) options are automatically
# configured by the init.d script and must not be added to DAEMON_OPTS.
#
#DAEMON_OPTS=""

64
config/ap/etc/dhcpcd.conf Normal file
View File

@ -0,0 +1,64 @@
# A sample configuration for dhcpcd.
# See dhcpcd.conf(5) for details.
# Allow users of this group to interact with dhcpcd via the control socket.
#controlgroup wheel
# Inform the DHCP server of our hostname for DDNS.
hostname
# Use the hardware address of the interface for the Client ID.
clientid
# or
# Use the same DUID + IAID as set in DHCPv6 for DHCPv4 ClientID as per RFC4361.
# Some non-RFC compliant DHCP servers do not reply with this set.
# In this case, comment out duid and enable clientid above.
#duid
# Persist interface configuration when dhcpcd exits.
persistent
# Rapid commit support.
# Safe to enable by default because it requires the equivalent option set
# on the server to actually work.
option rapid_commit
# A list of options to request from the DHCP server.
option domain_name_servers, domain_name, domain_search, host_name
option classless_static_routes
# Respect the network MTU. This is applied to DHCP routes.
option interface_mtu
# Most distributions have NTP support.
#option ntp_servers
# A ServerID is required by RFC2131.
require dhcp_server_identifier
# Generate SLAAC address using the Hardware Address of the interface
#slaac hwaddr
# OR generate Stable Private IPv6 Addresses based from the DUID
slaac private
# Example static IP configuration:
#interface eth0
#static ip_address=192.168.0.10/24
#static ip6_address=fd51:42f8:caae:d92e::ff/64
#static routers=192.168.0.1
#static domain_name_servers=192.168.0.1 8.8.8.8 fd51:42f8:caae:d92e::1
# It is possible to fall back to a static IP if DHCP fails:
# define static profile
#profile static_eth0
#static ip_address=192.168.1.23/24
#static routers=192.168.1.1
#static domain_name_servers=192.168.1.1
# fallback to static profile on eth0
#interface eth0
#fallback static_eth0
#
#
interface wlan0
static ip_address=192.168.45.1/24
static routers=192.168.45.1

View File

@ -0,0 +1,5 @@
interface=wlan0 # Listening interface
dhcp-range=192.168.45.10,192.168.45.20,255.255.255.0,24h # Pool of IP addresses for wireless clients
domain=wlan # Domain
address=/gw.wlan/192.168.45.1 # Alias for router

View File

@ -0,0 +1,8 @@
country_code=GB
interface=wlan0
ssid=AGENT_GSM
hw_mode=g
channel=7
macaddr_acl=0
auth_algs=1
ignore_broadcast_ssid=0

View File

@ -0,0 +1,31 @@
[Unit]
Description=dnsmasq - A lightweight DHCP and caching DNS server
Requires=network.target
Wants=network-online.target
Before=nss-lookup.target
After=network-online.target
[Service]
Type=forking
PIDFile=/run/dnsmasq/dnsmasq.pid
# Test the config file and refuse starting if it is not valid.
ExecStartPre=/usr/sbin/dnsmasq --test
# We run dnsmasq via the /etc/init.d/dnsmasq script which acts as a
# wrapper picking up extra configuration files and then execs dnsmasq
# itself, when called with the "systemd-exec" function.
ExecStart=/etc/init.d/dnsmasq systemd-exec
# The systemd-*-resolvconf functions configure (and deconfigure)
# resolvconf to work with the dnsmasq DNS server. They're called like
# this to get correct error handling (ie don't start-resolvconf if the
# dnsmasq daemon fails to start.
ExecStartPost=/etc/init.d/dnsmasq systemd-start-resolvconf
ExecStop=/etc/init.d/dnsmasq systemd-stop-resolvconf
ExecReload=/bin/kill -HUP $MAINPID
[Install]
WantedBy=multi-user.target

View File

@ -10,8 +10,8 @@ INSTALL_DIR=agent_gsm
GIT_SOURCE=https://git.sevana.biz/public/agent_gsm GIT_SOURCE=https://git.sevana.biz/public/agent_gsm
# Install prerequisites # Install prerequisites
sudo apt install --assume-yes git mc python3 sox vim libffi-dev screen python3-pip python3-numpy sudo apt install --assume-yes git mc python3 sox vim libffi-dev screen python3-pip python3-numpy dnsmasq hostapd
sudo pip3 install pyyaml sox pyrabbit soundfile dbus_python pexpect pydub requests rabbitpy pydub sudo pip3 install pyyaml sox pyrabbit soundfile dbus_python pexpect pydub requests rabbitpy pydub reachability bottle
if [ -f "$INSTALL_DIR" ]; then if [ -f "$INSTALL_DIR" ]; then
rm -rf "$INSTALL_DIR" rm -rf "$INSTALL_DIR"

View File

@ -24,6 +24,7 @@ from bt_signal import SignalBoundaries
from bt_call_controller import INTERRUPT_SIGNAL from bt_call_controller import INTERRUPT_SIGNAL
import bt_call_controller import bt_call_controller
import agent_point
CONFIG = AgentConfig() CONFIG = AgentConfig()
@ -68,23 +69,23 @@ def detect_degraded_signal(file_test: Path, file_reference: Path) -> SignalBound
# Seems some problem with recording, return zero boundaries # Seems some problem with recording, return zero boundaries
return SignalBoundaries() return SignalBoundaries()
r = SignalBoundaries()
if CONFIG.UseSpeechDetector: if CONFIG.UseSpeechDetector:
r = bt_signal.find_reference_signal_via_speechdetector(file_test) r = bt_signal.find_reference_signal_via_speechdetector(file_test)
else:
r = bt_signal.find_reference_signal(file_test)
if r.offset_start == 0.0 and is_caller: if r.offset_start == 0.0 and is_caller:
r.offset_start = 5.0 # Skip ringing tones r.offset_start = 5.0 # Skip ringing tones
return r return r
def detect_reference_signal(file_reference: Path) -> SignalBoundaries: def detect_reference_signal(file_reference: Path) -> SignalBoundaries:
# Run silence eraser on reference file as well # Run silence eraser on reference file as well
result = SignalBoundaries()
if CONFIG.UseSpeechDetector: if CONFIG.UseSpeechDetector:
result = bt_signal.find_reference_signal_via_speechdetector(file_reference) result = bt_signal.find_reference_signal_via_speechdetector(file_reference)
else:
result = bt_signal.find_reference_signal(file_reference)
return result return result
@ -352,12 +353,12 @@ def run_caller_task(t):
# Start call. It will analyse audio as well and upload results # Start call. It will analyse audio as well and upload results
make_call(target_addr) make_call(target_addr)
# Runs caller probe - load task list and perform calls # Runs caller probe - load task list and perform calls
def run_probe(): def run_probe():
global TASK_LIST, CURRENT_TASK global TASK_LIST, CURRENT_TASK
offline_mode : bool = False
while True: while True:
# Get task list update # Get task list update
@ -366,6 +367,7 @@ def run_probe():
# Check in cache # Check in cache
utils.log('Checking for task list in cache...') utils.log('Checking for task list in cache...')
new_tasks = CACHE.get_tasks(BACKEND.phone.name) new_tasks = CACHE.get_tasks(BACKEND.phone.name)
offline_mode = True
# Did we fetch anything ? # Did we fetch anything ?
if new_tasks: if new_tasks:
@ -386,7 +388,7 @@ def run_probe():
if TASK_LIST.tasks is not None: if TASK_LIST.tasks is not None:
utils.log_verbose(f"Resulting task list: {TASK_LIST.tasks}") utils.log_verbose(f"Resulting task list: {TASK_LIST.tasks}")
# Run test immediately if specified
if CONFIG.ForceRun and len(TASK_LIST.tasks) > 0: if CONFIG.ForceRun and len(TASK_LIST.tasks) > 0:
run_caller_task(TASK_LIST.tasks[0]) run_caller_task(TASK_LIST.tasks[0])
break break
@ -401,8 +403,10 @@ def run_probe():
# Remove sheduled time # Remove sheduled time
del t['scheduled_time'] del t['scheduled_time']
# Run task # Run task if we are online
run_caller_task(t) # Otherwise tasks run from the API point - via helper .apk
if not offline_mode:
run_caller_task(t)
utils.log_verbose(f'Call #{CALL_COUNTER.value} finished') utils.log_verbose(f'Call #{CALL_COUNTER.value} finished')
if CALL_COUNTER.value >= CONFIG.TaskLimit and CONFIG.TaskLimit > 0: if CALL_COUNTER.value >= CONFIG.TaskLimit and CONFIG.TaskLimit > 0:
@ -413,15 +417,35 @@ def run_probe():
except Exception as err: except Exception as err:
utils.log_error(message="Unexpected error.", err=err) utils.log_error(message="Unexpected error.", err=err)
spent_time = utils.get_monotonic_time() - start_time # Sleep for
spent_time = utils.get_monotonic_time() - start_time
# Wait 1 minute # Wait 1 minute
if spent_time < 60: if spent_time < 60:
time.sleep(60 - spent_time) timeout_time = 60 - spent_time
else:
timeout_time = 0
# Try to get next task
try:
if agent_point.WEB_QUEUE is None:
utils.log('Web task queue is None')
task = agent_point.WEB_QUEUE.get(block=True, timeout=timeout_time)
if task is not None:
run_caller_task(task)
except multiprocessing.Queue.empty:
# Ignore this exception, this is normal
pass
except Exception as err:
utils.log_error(message='Error when running t')
# In case of empty task list wait 1 minute before refresh # In case of empty task list wait 1 minute before refresh
if len(TASK_LIST.tasks) == 0: # if len(TASK_LIST.tasks) == 0:
time.sleep(60) # time.sleep(60)
def remove_pid_on_exit(): def remove_pid_on_exit():
@ -436,6 +460,9 @@ def receive_signal(signal_number, frame):
# Delete PID file # Delete PID file
remove_pid_on_exit() remove_pid_on_exit()
# Stop optional access point
agent_point.stop()
# Debugging info # Debugging info
print(f'Got signal {signal_number} from {frame}') print(f'Got signal {signal_number} from {frame}')
@ -447,7 +474,6 @@ def receive_signal(signal_number, frame):
return return
# Check if Python version is ok # Check if Python version is ok
assert sys.version_info >= (3, 6) assert sys.version_info >= (3, 6)
@ -471,6 +497,14 @@ if __name__ == '__main__':
signal.signal(signal.SIGINT, receive_signal) signal.signal(signal.SIGINT, receive_signal)
signal.signal(signal.SIGQUIT, receive_signal) signal.signal(signal.SIGQUIT, receive_signal)
if CONFIG.CacheDir:
CACHE = utils_cache.InfoCache(dir=CONFIG.CacheDir)
# Start own hotspot and API server
agent_point.CONFIG = CONFIG
agent_point.CACHE = CACHE
agent_point.start()
# Preconnect the phone # Preconnect the phone
if CONFIG.BT_MAC: if CONFIG.BT_MAC:
# Connect to phone before # Connect to phone before
@ -482,7 +516,7 @@ if __name__ == '__main__':
utils.log_error(f'No BT MAC specified, cannot connect. Exiting.') utils.log_error(f'No BT MAC specified, cannot connect. Exiting.')
raise SystemExit(EXIT_ERROR) raise SystemExit(EXIT_ERROR)
# Init BT modem # Init BT modem - here we wait for it
bt_call_controller.init() bt_call_controller.init()
# Logging settings # Logging settings
@ -491,18 +525,12 @@ if __name__ == '__main__':
if CONFIG.LogPath: if CONFIG.LogPath:
utils.open_log_file(CONFIG.LogPath, 'at') utils.open_log_file(CONFIG.LogPath, 'at')
if CONFIG.CacheDir:
CACHE = utils_cache.InfoCache(dir=CONFIG.CacheDir)
# Update path to pvqa/aqua-wb # Update path to pvqa/aqua-wb
VOICE_QUALITY_AVAILABLE = utils_sevana.find_binaries(DIR_PROJECT / 'bin') VOICE_QUALITY_AVAILABLE = utils_sevana.find_binaries(DIR_PROJECT / 'bin')
# Load latest licenses & configs - this requires utils_sevana.find_binaries() to be called before # Load latest licenses & configs - this requires utils_sevana.find_binaries() to be called before
# utils_sevana.load_config_and_licenses(config['backend']) # utils_sevana.load_config_and_licenses(config['backend'])
# Limit number of calls # Limit number of calls
if CONFIG.TaskLimit: if CONFIG.TaskLimit:
utils.log(f'Limiting number of calls to {CONFIG.TaskLimit}') utils.log(f'Limiting number of calls to {CONFIG.TaskLimit}')
@ -529,7 +557,7 @@ if __name__ == '__main__':
if BACKEND.phone is None: if BACKEND.phone is None:
utils.log_error(f'Failed to obtain information about {BACKEND.instance}. Exiting.') utils.log_error(f'Failed to obtain information about {BACKEND.instance}. Exiting.')
exit(EXIT_ERROR) exit(EXIT_ERROR)
# Cache phone information # Cache phone information
CACHE.put_phone(BACKEND.phone) CACHE.put_phone(BACKEND.phone)
@ -570,4 +598,7 @@ if __name__ == '__main__':
# Close log file # Close log file
utils.close_log_file() utils.close_log_file()
# Stop optional access point
agent_point.stop()
sys.exit(EXIT_OK) sys.exit(EXIT_OK)

133
src/agent_point.py Executable file
View File

@ -0,0 +1,133 @@
#!/usr/bin/python3
import bottle
import multiprocessing
import time
import os
import json
import utils_cache
from agent_config import AgentConfig
class AccessPoint:
active: bool = False
def __init__(self) -> None:
pass
def start(self):
pass
def stop(self):
pass
# Just a stub for now
ACCESS_POINT = AccessPoint()
# Web server process
SERVER_PROCESS = None
# Good status response
RESPONSE_OK = {'status': 'ok'}
# Available information in cache
CACHE : utils_cache.InfoCache = None
CONFIG: AgentConfig = None
# Web queue
WEB_QUEUE = multiprocessing.Manager().Queue()
@bottle.route('/status')
def web_status():
print(f'Serving /status request...')
r = RESPONSE_OK
if CONFIG is not None:
r['name'] = CONFIG.Name
r['backend'] = CONFIG.Backend
r['bt_mac'] = CONFIG.BT_MAC
if CACHE is not None:
print('Cache is found...')
# Phone information
phone = CACHE.get_phone(CONFIG.Name)
if phone is not None:
print('Phone information is found...')
r['phone'] = phone.to_dict()
# Task list information
task_list = CACHE.get_tasks(CONFIG.Name)
if task_list is not None and task_list.tasks is not None:
r['task_list'] = task_list.tasks
else:
print('Cache not found.')
return r
@bottle.route('/reboot')
def web_reboot():
os.system('sudo reboot')
return RESPONSE_OK
@bottle.route('/halt')
def web_reboot():
os.system('sudo halt')
return RESPONSE_OK
@bottle.route('/cache')
def web_list_cache():
result = []
if CACHE is None:
return result
# Iterate cache and return available files list
for f in os.listdir(CACHE.dir):
result.append(f)
return result
@bottle.route('/call', method=['POST'])
def web_call():
global WEB_QUEUE
try:
data = bottle.request.json
# Send task definition
print('Sending data to ougoing queue...')
WEB_QUEUE.put_nowait(data)
print('Returning OK response.')
return RESPONSE_OK
except Exception as e:
print(f'{str(e)}')
return RESPONSE_OK
def web_process(mp_queue: multiprocessing.Queue):
#global WEB_QUEUE
#WEB_QUEUE = mp_queue
print(f'Run web process...')
bottle.run(host='0.0.0.0', port=8080)
def start():
global ACCESS_POINT, SERVER_PROCESS
ACCESS_POINT.start()
SERVER_PROCESS = multiprocessing.Process(target=web_process, args=(None,), name='agent_gsm_web')
SERVER_PROCESS.start()
def stop():
global ACCESS_POINT, SERVER_PROCESS
ACCESS_POINT.stop()
SERVER_PROCESS.kill()
if __name__ == '__main__':
# Start test stuff
start()
# Wait 120 seconds for tests
time.sleep(120.0)
# Stop test
stop()

View File

@ -6,36 +6,36 @@ import pathlib
from utils_types import SignalBoundaries from utils_types import SignalBoundaries
from utils_sevana import speech_detector from utils_sevana import speech_detector
from pydub import silence, AudioSegment # from pydub import silence, AudioSegment
SILENCE_DELTA = 16 SILENCE_DELTA = 16
def find_reference_signal(input_file: pathlib.Path, output_file: pathlib.Path = None, use_end_offset: bool = True) -> SignalBoundaries: # def find_reference_signal(input_file: pathlib.Path, output_file: pathlib.Path = None, use_end_offset: bool = True) -> SignalBoundaries:
myaudio = AudioSegment.from_wav(str(input_file)) # myaudio = AudioSegment.from_wav(str(input_file))
dBFS = myaudio.dBFS # dBFS = myaudio.dBFS
# Find silence intervals # # Find silence intervals
intervals = silence.detect_nonsilent(myaudio, min_silence_len=1000, silence_thresh=dBFS-SILENCE_DELTA, seek_step=50) # intervals = silence.detect_nonsilent(myaudio, min_silence_len=1000, silence_thresh=dBFS-SILENCE_DELTA, seek_step=50)
# Translate to seconds # # Translate to seconds
intervals = [((start/1000),(stop/1000)) for start,stop in intervals] # in sec # intervals = [((start/1000),(stop/1000)) for start,stop in intervals] # in sec
# print(intervals) # # print(intervals)
# Example of intervals: [(5.4, 6.4), (18.7, 37.05)] # # Example of intervals: [(5.4, 6.4), (18.7, 37.05)]
for p in intervals: # for p in intervals:
if p[1] - p[0] > 17: # if p[1] - p[0] > 17:
bounds = SignalBoundaries(offset_start=p[0], offset_finish=p[1]) # bounds = SignalBoundaries(offset_start=p[0], offset_finish=p[1])
if output_file is not None: # if output_file is not None:
signal = myaudio[bounds.offset_start * 1000 : bounds.offset_finish * 1000] # signal = myaudio[bounds.offset_start * 1000 : bounds.offset_finish * 1000]
signal.export(str(output_file), format='wav', parameters=['-ar', '44100', '-sample_fmt', 's16']) # signal.export(str(output_file), format='wav', parameters=['-ar', '44100', '-sample_fmt', 's16'])
if use_end_offset: # if use_end_offset:
bounds.offset_finish = myaudio.duration_seconds - bounds.offset_finish # bounds.offset_finish = myaudio.duration_seconds - bounds.offset_finish
return bounds # return bounds
return SignalBoundaries() # return SignalBoundaries()
def find_reference_signal_via_speechdetector(input_file: pathlib.Path) -> SignalBoundaries: def find_reference_signal_via_speechdetector(input_file: pathlib.Path) -> SignalBoundaries:

View File

@ -19,8 +19,6 @@ class InfoCache:
utils.log_error(str(e)) utils.log_error(str(e))
self.dir = None self.dir = None
def is_active(self) -> bool: def is_active(self) -> bool:
return self.dir is not None return self.dir is not None

View File

@ -66,12 +66,13 @@ def trace_function(frame, event, arg):
class QualtestBackend: class QualtestBackend:
address: str address: str
instance: str instance: str
online: bool
def __init__(self): def __init__(self):
self.address = "" self.address = ""
self.instance = "" self.instance = ""
self.__phone = None self.__phone = None
self.online = False
@property @property
def phone(self) -> Phone: def phone(self) -> Phone:
@ -190,8 +191,17 @@ class QualtestBackend:
response = urllib.request.urlopen(url, timeout=utils.NETWORK_TIMEOUT) response = urllib.request.urlopen(url, timeout=utils.NETWORK_TIMEOUT)
if response.getcode() != 200: if response.getcode() != 200:
raise RuntimeError(f'Failed to load phone definition from server. Error code: {response.getcode()}') raise RuntimeError(f'Failed to load phone definition from server. Error code: {response.getcode()}')
# Consider backend as working one
self.online = True
except Exception as e: except Exception as e:
utils.log_error(f'Problem when loading the phone definition from backend. Error: {str(e)}') utils.log_error(f'Problem when loading the phone definition from backend. Error: {str(e)}')
# Consider backend as non-working
self.online = False
# Try to get data from the cache
r = cache.get_phone(self.instance) r = cache.get_phone(self.instance)
if r is None: if r is None:
raise RuntimeError(f'No cached phone definition.') raise RuntimeError(f'No cached phone definition.')