FreeDATA/modem/server.py

381 lines
14 KiB
Python
Raw Permalink Normal View History

2024-03-11 20:07:15 +01:00
import time
from flask import Flask, request, jsonify, make_response, abort, Response
2023-11-06 13:49:37 +01:00
from flask_sock import Sock
2023-11-08 10:54:08 +01:00
from flask_cors import CORS
2023-11-06 15:36:11 +01:00
import os
2024-02-04 13:44:03 +01:00
import sys
import serial_ports
2023-11-06 13:49:37 +01:00
from config import CONFIG
import audio
import queue
2023-11-09 22:11:53 +01:00
import service_manager
import state_manager
2024-02-28 20:01:26 +01:00
import json
2023-11-22 12:04:07 +01:00
import websocket_manager as wsm
import api_validations as validations
import command_cq
import command_beacon
import command_ping
import command_feq
import command_test
import command_arq_raw
2024-01-18 11:35:44 +01:00
import command_message_send
import event_manager
2024-03-11 20:07:15 +01:00
import atexit
from message_system_db_manager import DatabaseManager
from message_system_db_messages import DatabaseManagerMessages
from message_system_db_attachments import DatabaseManagerAttachments
from message_system_db_beacon import DatabaseManagerBeacon
2024-02-02 19:37:02 +01:00
from schedule_manager import ScheduleManager
2023-11-06 13:49:37 +01:00
app = Flask(__name__)
2023-11-08 10:54:08 +01:00
CORS(app, resources={r"/*": {"origins": "*"}})
2023-11-06 13:49:37 +01:00
sock = Sock(app)
MODEM_VERSION = "0.14.5-alpha"
2023-11-19 12:31:56 +01:00
2023-11-06 15:36:11 +01:00
# set config file to use
def set_config():
if 'FREEDATA_CONFIG' in os.environ:
config_file = os.environ['FREEDATA_CONFIG']
else:
2024-03-01 21:29:11 +01:00
script_dir = os.path.dirname(os.path.abspath(__file__))
config_file = os.path.join(script_dir, 'config.ini')
2023-11-06 13:49:37 +01:00
2023-11-06 15:36:11 +01:00
if os.path.exists(config_file):
print(f"Using config from {config_file}")
2023-11-06 15:36:11 +01:00
else:
print(f"Config file '{config_file}' not found. Exiting.")
2024-02-04 13:44:03 +01:00
sys.exit(1)
2023-11-06 13:49:37 +01:00
2024-01-14 21:05:53 +01:00
return config_file
2023-11-06 15:36:11 +01:00
2023-11-09 22:11:53 +01:00
2023-11-09 22:11:53 +01:00
# returns a standard API response
def api_response(data, status = 200):
return make_response(jsonify(data), status)
def api_abort(message, code):
jsonError = json.dumps({'error': message})
abort(Response(jsonError, code))
def api_ok(message = "ok"):
return api_response({'message': message})
# validates a parameter
def validate(req, param, validator, isRequired = True):
if param not in req:
if isRequired:
api_abort(f"Required parameter '{param}' is missing.", 400)
else:
return True
if not validator(req[param]):
api_abort(f"Value of '{param}' is invalid.", 400)
2023-11-23 16:59:53 +01:00
# Takes a transmit command and puts it in the transmit command queue
def enqueue_tx_command(cmd_class, params = {}):
2024-02-21 17:05:28 +01:00
try:
command = cmd_class(app.config_manager.read(), app.state_manager, app.event_manager, params)
app.logger.info(f"Command {command.get_name()} running...")
if command.run(app.modem_events, app.service_manager.modem): # TODO remove the app.modem_event custom queue
return True
except Exception as e:
app.logger.warning(f"Command {command.get_name()} failed...: {e}")
return False
2024-02-02 19:37:02 +01:00
2023-11-06 13:49:37 +01:00
## REST API
@app.route('/', methods=['GET'])
def index():
2023-11-06 15:36:11 +01:00
return api_response({'name': 'FreeDATA API',
2023-11-06 13:49:37 +01:00
'description': '',
'api_version': 1,
2024-01-14 21:05:53 +01:00
'modem_version': MODEM_VERSION,
2023-11-06 13:49:37 +01:00
'license': 'GPL3.0',
'documentation': 'https://wiki.freedata.app',
})
# get and set config
@app.route('/config', methods=['GET', 'POST'])
def config():
if request.method in ['POST']:
if not validations.validate_remote_config(request.json):
return api_abort("wrong config", 500)
# check if config already exists
if app.config_manager.read() == request.json:
return api_response(request.json)
2023-11-06 20:52:33 +01:00
set_config = app.config_manager.write(request.json)
2023-11-06 13:49:37 +01:00
if not set_config:
response = api_response(None, 'error writing config')
else:
2024-02-18 21:34:10 +01:00
app.modem_service.put("restart")
2023-11-06 13:49:37 +01:00
response = api_response(set_config)
return response
elif request.method == 'GET':
2023-11-06 15:36:11 +01:00
return api_response(app.config_manager.read())
2023-11-06 13:49:37 +01:00
@app.route('/devices/audio', methods=['GET'])
def get_audio_devices():
dev_in, dev_out = audio.get_audio_devices()
devices = { 'in': dev_in, 'out': dev_out }
return api_response(devices)
@app.route('/devices/serial', methods=['GET'])
def get_serial_devices():
devices = serial_ports.get_ports()
return api_response(devices)
2023-11-17 22:35:52 +01:00
@app.route('/modem/state', methods=['GET'])
def get_modem_state():
2023-11-22 18:05:31 +01:00
return api_response(app.state_manager.sendState())
2023-11-17 22:35:52 +01:00
2023-11-09 10:37:45 +01:00
@app.route('/modem/cqcqcq', methods=['POST', 'GET'])
def post_cqcqcq():
if request.method not in ['POST']:
2023-11-09 10:37:45 +01:00
return api_response({"info": "endpoint for triggering a CQ via POST"})
if not app.state_manager.is_modem_running:
api_abort('Modem not running', 503)
enqueue_tx_command(command_cq.CQCommand)
return api_ok()
2023-11-09 10:37:45 +01:00
2023-11-09 15:48:10 +01:00
@app.route('/modem/beacon', methods=['POST'])
def post_beacon():
if request.method not in ['POST']:
2023-11-09 15:48:10 +01:00
return api_response({"info": "endpoint for controlling BEACON STATE via POST"})
2024-01-04 10:01:29 +01:00
2023-11-22 18:05:31 +01:00
if not isinstance(request.json['enabled'], bool):
api_abort(f"Incorrect value for 'enabled'. Shoud be bool.")
if not app.state_manager.is_modem_running:
api_abort('Modem not running', 503)
2023-12-21 17:47:48 +01:00
if not app.state_manager.is_beacon_running:
app.state_manager.set('is_beacon_running', request.json['enabled'])
if not app.state_manager.getARQ():
enqueue_tx_command(command_beacon.BeaconCommand, request.json)
2023-12-21 17:47:48 +01:00
else:
app.state_manager.set('is_beacon_running', request.json['enabled'])
return api_response(request.json)
2023-11-09 15:48:10 +01:00
@app.route('/modem/ping_ping', methods=['POST'])
def post_ping():
if request.method not in ['POST']:
2023-11-09 15:48:10 +01:00
return api_response({"info": "endpoint for controlling PING via POST"})
2023-11-22 18:05:31 +01:00
if not app.state_manager.is_modem_running:
api_abort('Modem not running', 503)
validate(request.json, 'dxcall', validations.validate_freedata_callsign)
enqueue_tx_command(command_ping.PingCommand, request.json)
return api_ok()
@app.route('/modem/send_test_frame', methods=['POST'])
def post_send_test_frame():
if request.method not in ['POST']:
return api_response({"info": "endpoint for triggering a TEST_FRAME via POST"})
if not app.state_manager.is_modem_running:
api_abort('Modem not running', 503)
enqueue_tx_command(command_test.TestCommand)
return api_ok()
@app.route('/modem/fec_transmit', methods=['POST'])
def post_send_fec_frame():
if request.method not in ['POST']:
return api_response({"info": "endpoint for triggering a FEC frame via POST"})
if not app.state_manager.is_modem_running:
api_abort('Modem not running', 503)
enqueue_tx_command(command_feq.FecCommand, request.json)
return api_ok()
@app.route('/modem/fec_is_writing', methods=['POST'])
def post_send_fec_is_writing():
if request.method not in ['POST']:
return api_response({"info": "endpoint for triggering a IS WRITING frame via POST"})
if not app.state_manager.is_modem_running:
api_abort('Modem not running', 503)
#server_commands.modem_fec_is_writing(request.json)
return 'Not implemented yet'
2023-11-09 15:48:10 +01:00
2023-11-09 22:11:53 +01:00
@app.route('/modem/start', methods=['POST'])
def post_modem_start():
if request.method not in ['POST']:
2023-11-09 22:11:53 +01:00
return api_response({"info": "endpoint for STARTING modem via POST"})
print("start received...")
app.modem_service.put("start")
return api_response(request.json)
2023-11-09 22:11:53 +01:00
@app.route('/modem/stop', methods=['POST'])
def post_modem_stop():
if request.method not in ['POST']:
2023-11-09 22:11:53 +01:00
return api_response({"info": "endpoint for STOPPING modem via POST"})
print("stop received...")
app.modem_service.put("stop")
return api_ok()
2023-11-13 18:50:46 +01:00
@app.route('/version', methods=['GET'])
def get_modem_version():
2023-11-23 07:01:29 +01:00
return api_response({"version": app.MODEM_VERSION})
2023-11-13 18:50:46 +01:00
@app.route('/modem/send_arq_raw', methods=['POST'])
def post_modem_send_raw():
if request.method not in ['POST']:
return api_response({"info": "endpoint for SENDING RAW DATA via POST"})
if not app.state_manager.is_modem_running:
api_abort('Modem not running', 503)
if app.state_manager.check_if_running_arq_session():
api_abort('Modem busy', 503)
2024-01-04 21:44:59 +01:00
if enqueue_tx_command(command_arq_raw.ARQRawCommand, request.json):
return api_response(request.json)
else:
api_abort('Error executing command...', 500)
2023-12-21 15:05:22 +01:00
@app.route('/modem/stop_transmission', methods=['POST'])
def post_modem_send_raw_stop():
if request.method not in ['POST']:
return api_response({"info": "endpoint for SENDING a STOP command via POST"})
if not app.state_manager.is_modem_running:
api_abort('Modem not running', 503)
2023-12-28 22:27:49 +01:00
if app.state_manager.getARQ():
for id in app.state_manager.arq_irs_sessions:
app.state_manager.arq_irs_sessions[id].abort_transmission()
for id in app.state_manager.arq_iss_sessions:
app.state_manager.arq_iss_sessions[id].abort_transmission()
2023-12-28 22:27:49 +01:00
2023-12-21 15:05:22 +01:00
return api_response(request.json)
@app.route('/radio', methods=['GET', 'POST'])
def get_post_radio():
if request.method in ['POST']:
2024-02-20 09:13:34 +01:00
if "radio_frequency" in request.json:
app.radio_manager.set_frequency(request.json['radio_frequency'])
2024-02-20 09:13:34 +01:00
if "radio_mode" in request.json:
app.radio_manager.set_mode(request.json['radio_mode'])
2024-02-20 09:13:34 +01:00
if "radio_rf_level" in request.json:
app.radio_manager.set_rf_level(int(request.json['radio_rf_level']))
return api_response(request.json)
elif request.method == 'GET':
return api_response(app.state_manager.get_radio_status())
2023-12-21 15:05:22 +01:00
@app.route('/freedata/messages', methods=['POST', 'GET'])
2024-01-25 15:48:00 +01:00
def get_post_freedata_message():
if request.method in ['GET']:
result = DatabaseManagerMessages(app.event_manager).get_all_messages_json()
2024-02-21 17:05:28 +01:00
return api_response(result)
if request.method in ['POST']:
enqueue_tx_command(command_message_send.SendMessageCommand, request.json)
return api_response(request.json)
api_abort('Error executing command...', 500)
2024-01-18 11:35:44 +01:00
@app.route('/freedata/messages/<string:message_id>', methods=['GET', 'POST', 'PATCH', 'DELETE'])
2024-01-28 12:08:55 +01:00
def handle_freedata_message(message_id):
if request.method == 'GET':
message = DatabaseManagerMessages(app.event_manager).get_message_by_id_json(message_id)
2024-01-28 12:08:55 +01:00
return message
2024-02-03 14:16:23 +01:00
elif request.method == 'POST':
result = DatabaseManagerMessages(app.event_manager).update_message(message_id, update_data={'status': 'queued'})
DatabaseManagerMessages(app.event_manager).increment_message_attempts(message_id)
return api_response(result)
elif request.method == 'PATCH':
# Fixme We need to adjust this
result = DatabaseManagerMessages(app.event_manager).mark_message_as_read(message_id)
2024-02-03 14:16:23 +01:00
return api_response(result)
2024-01-28 12:08:55 +01:00
elif request.method == 'DELETE':
result = DatabaseManagerMessages(app.event_manager).delete_message(message_id)
2024-01-28 12:08:55 +01:00
return api_response(result)
else:
api_abort('Error executing command...', 500)
@app.route('/freedata/messages/<string:message_id>/attachments', methods=['GET'])
def get_message_attachments(message_id):
attachments = DatabaseManagerAttachments(app.event_manager).get_attachments_by_message_id_json(message_id)
2024-01-28 12:08:55 +01:00
return api_response(attachments)
2024-02-02 19:37:02 +01:00
@app.route('/freedata/messages/attachment/<string:data_sha512>', methods=['GET'])
def get_message_attachment(data_sha512):
attachment = DatabaseManagerAttachments(app.event_manager).get_attachment_by_sha512(data_sha512)
return api_response(attachment)
@app.route('/freedata/beacons', methods=['GET'])
def get_all_beacons():
beacons = DatabaseManagerBeacon(app.event_manager).get_all_beacons()
return api_response(beacons)
@app.route('/freedata/beacons/<string:callsign>', methods=['GET'])
def get_beacons_by_callsign(callsign):
beacons = DatabaseManagerBeacon(app.event_manager).get_beacons_by_callsign(callsign)
return api_response(beacons)
2023-11-08 19:21:36 +01:00
2023-11-06 13:49:37 +01:00
# Event websocket
@sock.route('/events')
2023-11-09 17:14:22 +01:00
def sock_events(sock):
2024-01-04 21:13:32 +01:00
wsm.handle_connection(sock, wsm.events_client_list, app.modem_events) # TODO remove the app.modem_event custom queue
2023-11-09 17:14:22 +01:00
@sock.route('/fft')
def sock_fft(sock):
2023-11-22 12:04:07 +01:00
wsm.handle_connection(sock, wsm.fft_client_list, app.modem_fft)
2023-11-11 20:46:49 +01:00
@sock.route('/states')
def sock_states(sock):
2023-11-22 12:04:07 +01:00
wsm.handle_connection(sock, wsm.states_client_list, app.state_queue)
2023-11-23 07:01:29 +01:00
2024-03-11 20:07:15 +01:00
@atexit.register
def stop_server():
2024-03-12 19:48:50 +01:00
try:
2024-03-16 10:29:13 +01:00
app.service_manager.modem_service.put("stop")
2024-03-16 13:44:58 +01:00
app.socket_interface_manager.stop_servers()
if app.service_manager.modem:
app.service_manager.modem.sd_input_stream.stop
audio.sd._terminate()
except Exception as e:
2024-03-12 19:48:50 +01:00
print("Error stopping modem")
2024-03-11 20:07:15 +01:00
time.sleep(1)
print("------------------------------------------")
2024-03-11 20:07:15 +01:00
print('Server shutdown...')
if __name__ == "__main__":
2024-01-14 21:05:53 +01:00
app.config['SOCK_SERVER_OPTIONS'] = {'ping_interval': 10}
# define global MODEM_VERSION
app.MODEM_VERSION = MODEM_VERSION
config_file = set_config()
app.config_manager = CONFIG(config_file)
# start modem
2024-03-09 10:47:27 +01:00
app.p2p_data_queue = queue.Queue() # queue which holds processing data of p2p connections
2024-01-14 21:05:53 +01:00
app.state_queue = queue.Queue() # queue which holds latest states
app.modem_events = queue.Queue() # queue which holds latest events
app.modem_fft = queue.Queue() # queue which holds latest fft data
app.modem_service = queue.Queue() # start / stop modem service
app.event_manager = event_manager.EventManager([app.modem_events]) # TODO remove the app.modem_event custom queue
# init state manager
app.state_manager = state_manager.StateManager(app.state_queue)
2024-02-02 19:37:02 +01:00
# initialize message system schedule manager
app.schedule_manager = ScheduleManager(app.MODEM_VERSION, app.config_manager, app.state_manager, app.event_manager)
2024-01-14 21:05:53 +01:00
# start service manager
app.service_manager = service_manager.SM(app)
2024-03-16 13:44:58 +01:00
2024-01-14 21:05:53 +01:00
# start modem service
app.modem_service.put("start")
2024-02-02 19:37:02 +01:00
# initialize database default values
2024-01-27 12:07:07 +01:00
DatabaseManager(app.event_manager).initialize_default_values()
2024-01-14 21:05:53 +01:00
wsm.startThreads(app)
2024-03-04 15:50:08 +01:00
conf = app.config_manager.read()
modemaddress = conf['NETWORK']['modemaddress']
modemport = conf['NETWORK']['modemport']
if not modemaddress:
modemaddress = '127.0.0.1'
if not modemport:
modemport = 5000
2024-03-04 15:50:08 +01:00
app.run(modemaddress, modemport)
2024-03-16 13:44:58 +01:00