Finish controller to log correctly, make port configurable.
This commit is contained in:
parent
fe80b803f8
commit
433f1ef92c
7 changed files with 163 additions and 80 deletions
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
68
server.py
68
server.py
|
@ -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.")
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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">
|
||||
|
|
Loading…
Reference in a new issue