Compare commits
10 Commits
mos
...
89da140bb4
| Author | SHA1 | Date | |
|---|---|---|---|
| 89da140bb4 | |||
| 95cfea04ad | |||
| 3b9e74d5ee | |||
| 6feb10f49a | |||
| 2f67aec5bb | |||
| 85181b7d1e | |||
| 598456b830 | |||
| cd9c250c95 | |||
| d0032364ee | |||
| 8ed8e5f255 |
7
config/ap/add_ap.sh
Executable file
7
config/ap/add_ap.sh
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
sudo nmcli con add con-name hotspot ifname wlan0 type wifi ssid "AGENT_GSM"
|
||||
#sudo nmcli con modify hotspot wifi-sec.key-mgmt wpa-psk
|
||||
#sudo nmcli con modify hotspot wifi-sec.psk "159357159357"
|
||||
sudo nmcli con modify hotspot 802-11-wireless.mode ap 802-11-wireless.band bg ipv4.method shared ipv6.method shared
|
||||
|
||||
@@ -10,8 +10,8 @@ INSTALL_DIR=agent_gsm
|
||||
GIT_SOURCE=https://git.sevana.biz/public/agent_gsm
|
||||
|
||||
# Install prerequisites
|
||||
sudo apt install --assume-yes git mc python3 sox vim libffi-dev screen python3-pip python3-numpy
|
||||
sudo pip3 install pyyaml sox pyrabbit soundfile dbus_python pexpect pydub requests rabbitpy pydub
|
||||
sudo apt install --assume-yes git mc python3 sox vim libffi-dev screen python3-pip python3-numpy dnsmasq hostapd screen
|
||||
sudo pip3 install pyyaml sox pyrabbit soundfile dbus_python pexpect requests rabbitpy bottle --break-system-packages
|
||||
|
||||
if [ -f "$INSTALL_DIR" ]; then
|
||||
rm -rf "$INSTALL_DIR"
|
||||
@@ -45,7 +45,7 @@ cp config/agent.in.yaml config/agent.yaml
|
||||
mkdir -p ~/.config/mc
|
||||
cp config/mc/ini ~/.config/mc
|
||||
|
||||
# Replace the values
|
||||
# Replace the values - finish preparing the agent configuration file
|
||||
if [[ $BACKEND_URL != "" ]]; then
|
||||
sed -i "s|BACKEND|$BACKEND|" config/agent.yaml
|
||||
fi
|
||||
@@ -56,11 +56,45 @@ fi
|
||||
|
||||
sed -i "s|TASK_NAME|$TASK_NAME|" config/agent.yaml
|
||||
|
||||
# Update systemD unit file
|
||||
cp config/systemd/agent_gsm.in.service config/systemd/agent_gsm.service
|
||||
ABSOLUTE_INSTALL_DIR=`realpath .`
|
||||
|
||||
sed -i "s|ABSOLUTE_INSTALL_DIR|$ABSOLUTE_INSTALL_DIR|" config/systemd/agent_gsm.service
|
||||
# Update systemD unit file
|
||||
# cp config/systemd/agent_gsm.in.service config/systemd/agent_gsm.service
|
||||
# sed -i "s|ABSOLUTE_INSTALL_DIR|$ABSOLUTE_INSTALL_DIR|" config/systemd/agent_gsm.service
|
||||
|
||||
install_ap() {
|
||||
# $1 is AP name
|
||||
sudo cp $ABSOLUTE_INSTALL_DIR/config/ap/etc/dhcpcd.conf /etc
|
||||
sudo cp $ABSOLUTE_INSTALL_DIR/config/ap/etc/dnsmasq.conf /etc
|
||||
sudo mkdir -p /etc/hostapd
|
||||
sudo cp $ABSOLUTE_INSTALL_DIR/config/ap/etc/hostapd.conf /etc/hostapd
|
||||
sudo sed -i "s|AGENT_GSM|$1|" /etc/hostapt/hostapd.conf
|
||||
sudo cp $ABSOLUTE_INSTALL_DIR/config/ap/etc/default/hostapt /etd/default
|
||||
sudo systemctl enable dnsmasq
|
||||
sudo systemctl enable hostapd
|
||||
sudo systemctl start dnsmasq
|
||||
sudo systemctl start hostapd
|
||||
}
|
||||
|
||||
function enable_autologin() {
|
||||
sudo systemctl --quiet set-default multi-user.target
|
||||
sudo cat > /etc/systemd/system/getty@tty1.service.d/autologin.conf << EOF
|
||||
[Service]
|
||||
ExecStart=
|
||||
ExecStart=-/sbin/agetty --autologin $USER --noclear %I \$TERM
|
||||
EOF
|
||||
|
||||
}
|
||||
|
||||
# ToDo:
|
||||
# - allow autologin in console mode for 'pi' user
|
||||
enable_autologin
|
||||
|
||||
# - add $ABSOLUTE_INSTALL_DIR/run_agent_screen.sh to ~/.bashrc
|
||||
echo "$ABSOLUTE_INSTALL_DIR/run_agent_screen.sh" >> ~/.bashrc
|
||||
|
||||
# - install wifi AP with name $PHONE_NAME
|
||||
install_ap $PHONE_NAME
|
||||
|
||||
echo "Now the remaining prerequisites will be installed and system will reboot."
|
||||
echo "You can connect the phone via Bluetooth after the reboot."
|
||||
|
||||
@@ -24,6 +24,7 @@ from bt_signal import SignalBoundaries
|
||||
from bt_call_controller import INTERRUPT_SIGNAL
|
||||
import bt_call_controller
|
||||
|
||||
import agent_point
|
||||
|
||||
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
|
||||
return SignalBoundaries()
|
||||
|
||||
r = SignalBoundaries()
|
||||
if CONFIG.UseSpeechDetector:
|
||||
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:
|
||||
r.offset_start = 5.0 # Skip ringing tones
|
||||
|
||||
return r
|
||||
|
||||
|
||||
|
||||
def detect_reference_signal(file_reference: Path) -> SignalBoundaries:
|
||||
# Run silence eraser on reference file as well
|
||||
result = SignalBoundaries()
|
||||
if CONFIG.UseSpeechDetector:
|
||||
result = bt_signal.find_reference_signal_via_speechdetector(file_reference)
|
||||
else:
|
||||
result = bt_signal.find_reference_signal(file_reference)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -354,10 +355,10 @@ def run_caller_task(t):
|
||||
make_call(target_addr)
|
||||
|
||||
|
||||
|
||||
# Runs caller probe - load task list and perform calls
|
||||
def run_probe():
|
||||
global TASK_LIST, CURRENT_TASK
|
||||
offline_mode : bool = False
|
||||
|
||||
while True:
|
||||
# Get task list update
|
||||
@@ -366,6 +367,7 @@ def run_probe():
|
||||
# Check in cache
|
||||
utils.log('Checking for task list in cache...')
|
||||
new_tasks = CACHE.get_tasks(BACKEND.phone.name)
|
||||
offline_mode = True
|
||||
|
||||
# Did we fetch anything ?
|
||||
if new_tasks:
|
||||
@@ -386,7 +388,7 @@ def run_probe():
|
||||
if TASK_LIST.tasks is not None:
|
||||
utils.log_verbose(f"Resulting task list: {TASK_LIST.tasks}")
|
||||
|
||||
|
||||
# Run test immediately if specified
|
||||
if CONFIG.ForceRun and len(TASK_LIST.tasks) > 0:
|
||||
run_caller_task(TASK_LIST.tasks[0])
|
||||
break
|
||||
@@ -401,8 +403,10 @@ def run_probe():
|
||||
# Remove sheduled time
|
||||
del t['scheduled_time']
|
||||
|
||||
# Run task
|
||||
run_caller_task(t)
|
||||
# Run task if we are online
|
||||
# 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')
|
||||
if CALL_COUNTER.value >= CONFIG.TaskLimit and CONFIG.TaskLimit > 0:
|
||||
@@ -413,15 +417,35 @@ def run_probe():
|
||||
except Exception as 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
|
||||
if spent_time < 60:
|
||||
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')
|
||||
|
||||
# Wait 1 minute
|
||||
if spent_time < 60:
|
||||
time.sleep(60 - spent_time)
|
||||
|
||||
# In case of empty task list wait 1 minute before refresh
|
||||
if len(TASK_LIST.tasks) == 0:
|
||||
time.sleep(60)
|
||||
# if len(TASK_LIST.tasks) == 0:
|
||||
# time.sleep(60)
|
||||
|
||||
|
||||
def remove_pid_on_exit():
|
||||
@@ -436,6 +460,9 @@ def receive_signal(signal_number, frame):
|
||||
# Delete PID file
|
||||
remove_pid_on_exit()
|
||||
|
||||
# Stop optional access point
|
||||
agent_point.stop()
|
||||
|
||||
# Debugging info
|
||||
print(f'Got signal {signal_number} from {frame}')
|
||||
|
||||
@@ -447,7 +474,6 @@ def receive_signal(signal_number, frame):
|
||||
return
|
||||
|
||||
|
||||
|
||||
# Check if Python version is ok
|
||||
assert sys.version_info >= (3, 6)
|
||||
|
||||
@@ -471,6 +497,14 @@ if __name__ == '__main__':
|
||||
signal.signal(signal.SIGINT, 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
|
||||
if CONFIG.BT_MAC:
|
||||
# Connect to phone before
|
||||
@@ -482,7 +516,7 @@ if __name__ == '__main__':
|
||||
utils.log_error(f'No BT MAC specified, cannot connect. Exiting.')
|
||||
raise SystemExit(EXIT_ERROR)
|
||||
|
||||
# Init BT modem
|
||||
# Init BT modem - here we wait for it
|
||||
bt_call_controller.init()
|
||||
|
||||
# Logging settings
|
||||
@@ -491,18 +525,12 @@ if __name__ == '__main__':
|
||||
if CONFIG.LogPath:
|
||||
utils.open_log_file(CONFIG.LogPath, 'at')
|
||||
|
||||
|
||||
if CONFIG.CacheDir:
|
||||
CACHE = utils_cache.InfoCache(dir=CONFIG.CacheDir)
|
||||
|
||||
|
||||
# Update path to pvqa/aqua-wb
|
||||
VOICE_QUALITY_AVAILABLE = utils_sevana.find_binaries(DIR_PROJECT / 'bin')
|
||||
|
||||
# Load latest licenses & configs - this requires utils_sevana.find_binaries() to be called before
|
||||
# utils_sevana.load_config_and_licenses(config['backend'])
|
||||
|
||||
|
||||
# Limit number of calls
|
||||
if CONFIG.TaskLimit:
|
||||
utils.log(f'Limiting number of calls to {CONFIG.TaskLimit}')
|
||||
@@ -570,4 +598,7 @@ if __name__ == '__main__':
|
||||
# Close log file
|
||||
utils.close_log_file()
|
||||
|
||||
# Stop optional access point
|
||||
agent_point.stop()
|
||||
|
||||
sys.exit(EXIT_OK)
|
||||
|
||||
133
src/agent_point.py
Normal file
133
src/agent_point.py
Normal 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()
|
||||
44
src/bt_phone_test.py
Executable file
44
src/bt_phone_test.py
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import bt_phone
|
||||
import bt_controller
|
||||
import time
|
||||
|
||||
EXIT_OK = 0
|
||||
EXIT_ERROR = 1
|
||||
|
||||
PHONE_BT_MAC = '40:B0:76:B4:36:98'
|
||||
|
||||
|
||||
# Connection to phone
|
||||
|
||||
bluetooth_ctl = bt_controller.Bluetoothctl()
|
||||
devices = bluetooth_ctl.get_paired_devices()
|
||||
print(f'Paired devices: {devices}')
|
||||
|
||||
# disconnect before connect
|
||||
bluetooth_ctl.disconnect( PHONE_BT_MAC )
|
||||
ret = bluetooth_ctl.connect( PHONE_BT_MAC )
|
||||
if ret == False:
|
||||
print(f'Connect to {PHONE_BT_MAC} failed')
|
||||
exit(EXIT_ERROR)
|
||||
|
||||
print(f'Connect to {PHONE_BT_MAC} success')
|
||||
|
||||
# Call
|
||||
# Initialize phone - this brings Ofono via D-Bus
|
||||
phone = bt_phone.Phone()
|
||||
phone.setup_dbus_loop()
|
||||
phone.call_number('111222')
|
||||
|
||||
# Wait 5 seconds
|
||||
time.sleep(5)
|
||||
|
||||
# Just to be sure - finish the call
|
||||
phone.hangup_call()
|
||||
phone.quit_dbus_loop()
|
||||
|
||||
# Disconnect BT transport from the phone
|
||||
bluetooth_ctl.disconnect(PHONE_BT_MAC)
|
||||
|
||||
exit(EXIT_OK)
|
||||
@@ -6,36 +6,36 @@ import pathlib
|
||||
from utils_types import SignalBoundaries
|
||||
from utils_sevana import speech_detector
|
||||
|
||||
from pydub import silence, AudioSegment
|
||||
# from pydub import silence, AudioSegment
|
||||
|
||||
SILENCE_DELTA = 16
|
||||
|
||||
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))
|
||||
dBFS = myaudio.dBFS
|
||||
# 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))
|
||||
# dBFS = myaudio.dBFS
|
||||
|
||||
# Find silence intervals
|
||||
intervals = silence.detect_nonsilent(myaudio, min_silence_len=1000, silence_thresh=dBFS-SILENCE_DELTA, seek_step=50)
|
||||
# # Find silence intervals
|
||||
# intervals = silence.detect_nonsilent(myaudio, min_silence_len=1000, silence_thresh=dBFS-SILENCE_DELTA, seek_step=50)
|
||||
|
||||
# Translate to seconds
|
||||
intervals = [((start/1000),(stop/1000)) for start,stop in intervals] # in sec
|
||||
# # Translate to seconds
|
||||
# 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)]
|
||||
for p in intervals:
|
||||
if p[1] - p[0] > 17:
|
||||
bounds = SignalBoundaries(offset_start=p[0], offset_finish=p[1])
|
||||
if output_file is not None:
|
||||
signal = myaudio[bounds.offset_start * 1000 : bounds.offset_finish * 1000]
|
||||
signal.export(str(output_file), format='wav', parameters=['-ar', '44100', '-sample_fmt', 's16'])
|
||||
# # Example of intervals: [(5.4, 6.4), (18.7, 37.05)]
|
||||
# for p in intervals:
|
||||
# if p[1] - p[0] > 17:
|
||||
# bounds = SignalBoundaries(offset_start=p[0], offset_finish=p[1])
|
||||
# if output_file is not None:
|
||||
# signal = myaudio[bounds.offset_start * 1000 : bounds.offset_finish * 1000]
|
||||
# signal.export(str(output_file), format='wav', parameters=['-ar', '44100', '-sample_fmt', 's16'])
|
||||
|
||||
if use_end_offset:
|
||||
bounds.offset_finish = myaudio.duration_seconds - bounds.offset_finish
|
||||
# if use_end_offset:
|
||||
# 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:
|
||||
|
||||
@@ -19,8 +19,6 @@ class InfoCache:
|
||||
utils.log_error(str(e))
|
||||
self.dir = None
|
||||
|
||||
|
||||
|
||||
def is_active(self) -> bool:
|
||||
return self.dir is not None
|
||||
|
||||
|
||||
@@ -66,12 +66,13 @@ def trace_function(frame, event, arg):
|
||||
class QualtestBackend:
|
||||
address: str
|
||||
instance: str
|
||||
online: bool
|
||||
|
||||
def __init__(self):
|
||||
self.address = ""
|
||||
self.instance = ""
|
||||
self.__phone = None
|
||||
|
||||
self.online = False
|
||||
|
||||
@property
|
||||
def phone(self) -> Phone:
|
||||
@@ -190,8 +191,17 @@ class QualtestBackend:
|
||||
response = urllib.request.urlopen(url, timeout=utils.NETWORK_TIMEOUT)
|
||||
if response.getcode() != 200:
|
||||
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:
|
||||
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)
|
||||
if r is None:
|
||||
raise RuntimeError(f'No cached phone definition.')
|
||||
|
||||
Reference in New Issue
Block a user