Finish controller to log correctly, make port configurable.

This commit is contained in:
Matthew Stratford 2021-04-05 22:13:53 +01:00
parent fe80b803f8
commit 433f1ef92c
7 changed files with 163 additions and 80 deletions

View file

@ -11,6 +11,9 @@ class Controller():
self.handler()
return
def _state_handler(self):
return
# Registers a function for the controller class to call to tell BAPSicle to do something.
def register_callback(self, callback: Callable):
self.callbacks.append(callback)

View file

@ -1,52 +1,105 @@
from typing import List
from helpers.logging_manager import LoggingManager
from helpers.state_manager import StateManager
from typing import List, Optional
from controllers.controller import Controller
from multiprocessing import Queue
import serial
import sys
import time
from setproctitle import setproctitle
class MattchBox(Controller):
ser: serial.Serial
def __init__(self, player_to_q: List[Queue], player_from_q: List[Queue]):
class MattchBox(Controller):
ser: Optional[serial.Serial]
port: Optional[str]
next_port: Optional[str]
server_state: StateManager
logger: LoggingManager
def __init__(self, server_to_q: List[Queue], server_from_q: List[Queue], state: StateManager):
process_title = "ControllerHandler"
setproctitle(process_title)
self.ser = None
self.logger = LoggingManager("ControllerMattchBox")
#current_process().name = process_title
# connect to serial port
self.ser = serial.serial_for_url("/dev/cu.usbserial-210", do_not_open=True)
self.ser.baudrate = 2400
self.server_state = state # This is a copy, will not update :/
# This doesn't run, the callback function gets lost due to state being a copy in the multiprocessing process.
#self.server_state.add_callback(self._state_handler) # Allow server config changes to trigger controller reload if required.
self.port = None
self.next_port = self.server_state.state["serial_port"]
# TOOD: These need to be split in the player handler.
self.player_from_q = player_from_q
self.player_to_q = player_to_q
try:
self.ser.open()
except serial.SerialException as e:
sys.stderr.write('Could not open serial port {}: {}\n'.format(self.ser.name, e))
return
self.server_from_q = server_from_q
self.server_to_q = server_to_q
self.handler()
# This doesn't run, the callback function gets lost in StateManager.
def _state_handler(self):
new_port = self.server_state.state["serial_port"]
self.logger.log.info("Got server config update. New port: {}".format(new_port))
if new_port != self.port:
self.logger.log.info("Switching from port {} to {}".format(self.port, new_port))
# The serial port config has changed. Let's reload the serial.
self.port = None
self.next_port = new_port
def connect(self, port: Optional[str]):
if port:
# connect to serial port
self.ser = serial.serial_for_url(port, do_not_open=True)
self.ser.baudrate = 2400
try:
self.ser.open()
self.logger.log.info('Connected to serial port {}'.format(port))
except serial.SerialException as e:
self.logger.log.error('Could not open serial port {}: {}'.format(port, e))
self.ser = None
else:
self.ser = None
def handler(self):
while True:
try:
if self.ser.is_open:
line = int.from_bytes(self.ser.read(1), "big") # Endianness doesn't matter for 1 byte.
print("Controller got:", line)
if (line == 255):
print("Sending back KeepAlive")
self.ser.write(b'\xff') # Send 255 back.
elif (line in [1,3,5]):
self.sendToPlayer(int(line / 2), "PLAY")
elif (line in [2,4,6]):
self.sendToPlayer(int(line / 2)-1, "STOP")
if self.ser and self.ser.is_open and self.port: # If self.port is changing (via state_handler), we should stop.
try:
line = int.from_bytes(self.ser.read(1), "big") # Endianness doesn't matter for 1 byte.
self.logger.log.info("Received from controller:", line)
if (line == 255):
self.ser.write(b'\xff') # Send 255 back.
elif (line in [1,3,5]):
self.sendToPlayer(int(line / 2), "PLAY")
elif (line in [2,4,6]):
self.sendToPlayer(int(line / 2)-1, "STOP")
except:
continue
finally:
time.sleep(0.01)
elif self.port:
# If there's still a port set, just wait a moment and see if it's been reconnected.
self.server_state.update("ser_connected", False)
time.sleep(10)
self.connect(self.port)
else:
# We're not already connected, or a new port connection is to be made.
if self.ser:
self.ser.close()
self.server_state.update("ser_connected", False)
if self.next_port != None:
self.connect(self.next_port)
if self.ser.is_open:
self.port = self.next_port # We connected successfully, make it stick.
self.server_state.update("ser_connected", True)
continue # skip the sleep.
time.sleep(10)
except:
continue
def sendToPlayer(self, channel: int, msg:str):
self.player_to_q[channel].put("CONTROLLER:" + msg)
self.logger.log.info("Sending message to server: " + msg)
self.server_to_q[channel].put("CONTROLLER:" + msg)

View file

@ -1,8 +1,7 @@
from typing import Any, Dict, List
from typing import Any, Dict, List, Optional
import sounddevice as sd
from helpers.os_environment import isMacOS
from helpers.os_environment import isLinux, isMacOS, isWindows
import glob
class DeviceManager():
@classmethod
@ -24,3 +23,35 @@ class DeviceManager():
outputs: List[Dict] = list(filter(cls._isOutput, cls._getAudioDevices()))
outputs = sorted(outputs, key=lambda k: k['name'])
return [{"name": None}] + outputs
@classmethod
def getSerialPorts(cls) -> List[Optional[str]]:
""" Lists serial port names
:raises EnvironmentError:
On unsupported or unknown platforms
:returns:
A list of the serial ports available on the system
"""
# TODO: Get list of COM ports properly. (Can't use )
if isWindows():
ports = ['COM%s' % (i + 1) for i in range(8)]
elif isLinux():
# this excludes your current terminal "/dev/tty"
ports = glob.glob('/dev/tty[A-Za-z]*')
elif isMacOS():
ports = glob.glob('/dev/tty.*')
else:
raise EnvironmentError('Unsupported platform')
valid: List[str] = ports
result: List[Optional[str]] = []
if len(valid) > 0:
valid.sort()
result.append(None) # Add the None option
result.extend(valid)
return result

View file

@ -15,8 +15,8 @@ from helpers.types import ServerState
from typing import Any, Dict, List, NewType, Optional, Union
class StateManager:
filepath = None
logger = None
filepath: str
logger: LoggingManager
callbacks: List[Any] = []
__state = {}
__state_in_file = {}
@ -148,6 +148,7 @@ class StateManager:
self.logger.log.critical("Failed to execute status callback: {}".format(e))
def add_callback(self, function):
self._log("Adding callback: {}".format(str(function)))
self.callbacks.append(function)
def _log(self, text:str, level: int = INFO):

View file

@ -16,7 +16,6 @@ from api_handler import APIHandler
from controllers.mattchbox_usb import MattchBox
import multiprocessing
import queue
#import threading
import time
import player
from flask import Flask, render_template, send_from_directory, request, jsonify, abort
@ -61,8 +60,6 @@ class BAPSicleServer():
state.update("server_version", config.VERSION)
startServer()
#asyncio.get_event_loop().run_until_complete(startServer())
#asyncio.get_event_loop().run_forever()
def __del__(self):
stopServer()
@ -75,18 +72,15 @@ default_state = {
"host": "localhost",
"port": 13500,
"ws_port": 13501,
"num_channels": 3
"num_channels": 3,
"ser_port": None,
"ser_connected": False,
}
app = Flask(__name__, static_url_path='')
CORS(app, supports_credentials=True) # Allow ALL CORS!!!
log = logging.getLogger('werkzeug')
log.disabled = True
app.logger.disabled = True
api_from_q: queue.Queue
api_to_q: queue.Queue
@ -171,26 +165,27 @@ def server_config():
data = {
"ui_page": "server",
"ui_title": "Server Config",
"state": state.state
"state": state.state,
"ser_ports": DeviceManager.getSerialPorts()
}
return render_template("server.html", data=data)
@app.route("/restart", methods=["POST"])
async def restart_server():
@app.route("/server/update", methods=["POST"])
def update_server():
state.update("server_name", request.form["name"])
state.update("host", request.form["host"])
state.update("port", int(request.form["port"]))
state.update("num_channels", int(request.form["channels"]))
state.update("ws_port", int(request.form["ws_port"]))
stopServer(restart=True)
await startServer()
state.update("serial_port", request.form["serial_port"])
#stopServer()
return server_config()
# Get audio for UI to generate waveforms.
@app.route("/audiofile/<type>/<int:id>")
#@cross_origin()
def audio_file(type: str, id: int):
if type not in ["managed", "track"]:
abort(404)
@ -334,10 +329,6 @@ def list_showplans():
response = api_from_q.get_nowait()
if response.startswith("LIST_PLANS:"):
response = response[response.index(":")+1:]
#try:
# response = json.loads(response)
#except Exception as e:
# raise e
return response
except queue.Empty:
@ -539,11 +530,12 @@ def startServer():
player_handler = multiprocessing.Process(target=PlayerHandler, args=(channel_from_q, websocket_to_q, ui_to_q, controller_to_q))
player_handler.start()
# Note, state here will become a copy in the process.
# It will not update, and callbacks will not work :/
websockets_server = multiprocessing.Process(target=WebsocketServer, args=(channel_to_q, websocket_to_q, state))
websockets_server.start()
controller_handler = multiprocessing.Process(target=MattchBox, args=(channel_to_q, controller_to_q))
controller_handler = multiprocessing.Process(target=MattchBox, args=(channel_to_q, controller_to_q, state))
controller_handler.start()
# TODO Move this to player or installer.
@ -579,19 +571,33 @@ def startServer():
channel_to_q[0].put("PLAY")
# Don't use reloader, it causes Nested Processes!
def runWebServer():
CORS(app, supports_credentials=True) # Allow ALL CORS!!!
log = logging.getLogger('werkzeug')
log.disabled = True
app.logger.disabled = True
app.run(host=state.state["host"], port=state.state["port"], debug=True, use_reloader=False)
global webserver
webserver = multiprocessing.Process(
app.run(host=state.state["host"], port=state.state["port"], debug=True, use_reloader=False)
runWebServer()
)
webserver.start()
def stopServer(restart=False):
global channel_p, channel_from_q, channel_to_q, websockets_server, webserver
def stopServer():
global channel_p, channel_from_q, channel_to_q, websockets_server, webserver, controller_handler
print("Stopping Controllers")
controller_handler.terminate()
controller_handler.join()
print("Stopping Websockets")
websocket_to_q[0].put("WEBSOCKET:QUIT")
websockets_server.join()
del websockets_server
print("Stopping server.py")
for q in channel_to_q:
q.put("QUIT")
@ -607,21 +613,13 @@ def stopServer(restart=False):
del channel_to_q
print("Stopped all players.")
print("Stopping webserver")
global webserver
webserver.terminate()
webserver.join()
return
## Caused an outside context error, presuably because called outside of a page request.
#shutdown = request.environ.get('werkzeug.server.shutdown')
#if shutdown is None:
# print("Shutting down Server.")
#else:
# print("Shutting down Flask.")
# if not restart:
# shutdown()
print("Stopped webserver")
if __name__ == "__main__":
print("BAPSicle is a service. Please run it like one.")
raise Exception("BAPSicle is a service. Please run it like one.")

View file

@ -7,7 +7,7 @@
Set for:
{% for channel in data.channels %}
<a href="/player/{{channel.channel}}/output/none">Channel {{channel.channel}}</a>
<a href="/player/{{channel.channel}}/output/{{output.name}}">Channel {{channel.channel}}</a>
{% endfor %}
- {% if output.name %}{{output.name}}{% else %}System Default Output{% endif %}<br>
{% endfor %}

View file

@ -1,7 +1,7 @@
{% extends 'base.html' %}
{% block content_inner %}
{% if data %}
<form action="/restart" method="POST">
<form action="/server/update" method="POST">
<label for="host">Server Host:</label>
<input type="text" id="host" name="host" class="form-control" value="{{data.state.host}}">
<br>
@ -17,15 +17,12 @@
<label for="channels">Number of Channels:</label>
<input type="number" id="channels" name="channels" class="form-control" value="{{data.state.num_channels}}">
<br>
<label for="ser_port">BAPS Controller Serial Port:</label>
<select class="form-control" name="ser_port">
<label for="serial_port">BAPS Controller Serial Port:</label>
<select class="form-control" name="serial_port">
<label>Serial Ports</label>
{% for port in data.ser_ports %}
<option value="{{port.name}}">{{port.name}}</option>
<option value="{{port}}" {% if port == data.state.serial_port %}selected{% endif %}>{{port}}</option>
{% endfor %}
<option value="none">{% if not data.ser_ports %}No ports found.{% else %}None{% endif %}</option>
</select>
<br>
<input type="submit" class="btn btn-primary" value="Save & Restart Server">