Skip to content

Commit

Permalink
Make server and client communication reliable
Browse files Browse the repository at this point in the history
  • Loading branch information
MaddyGuthridge committed Apr 6, 2024
1 parent 37db678 commit 6c00117
Show file tree
Hide file tree
Showing 7 changed files with 101 additions and 34 deletions.
25 changes: 19 additions & 6 deletions flapi/__comms.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
FlapiInvalidMsgError,
FlapiServerError,
FlapiClientError,
FlapiServerExit,
)


Expand All @@ -30,7 +31,7 @@ def send_msg(msg: bytes):
Send a message to FL Studio
"""
mido_msg = MidoMsg("sysex", data=msg)
get_context().port.send(mido_msg)
get_context().req_port.send(mido_msg)


def handle_stdout(output: str):
Expand Down Expand Up @@ -62,6 +63,13 @@ def handle_received_message(msg: bytes) -> Optional[bytes]:
):
return None

# Handle other clients (prevent us from receiving their messages)
if (
msg.startswith(consts.SYSEX_HEADER)
and msg.removeprefix(consts.SYSEX_HEADER)[1] != get_context().client_id
):
return None

# Handle FL Studio stdout
if msg.removeprefix(consts.SYSEX_HEADER)[1] == MessageType.STDOUT:
text = b64decode(msg.removeprefix(consts.SYSEX_HEADER)[3:]).decode()
Expand All @@ -76,8 +84,12 @@ def handle_received_message(msg: bytes) -> Optional[bytes]:
log.info(f"Received exit command with code {code}")
raise SystemExit(code)

# Normal processing
return msg[len(consts.SYSEX_HEADER) + 1:]
# Handle server disconnect
if msg.removeprefix(consts.SYSEX_HEADER)[1] == MessageType.SERVER_GOODBYE:
raise FlapiServerExit()

# Normal processing (remove bytes for header, origin and client ID)
return msg[len(consts.SYSEX_HEADER) + 2:]


def assert_response_is_ok(msg: bytes, expected_msg_type: MessageType):
Expand Down Expand Up @@ -111,7 +123,8 @@ def poll_for_message() -> Optional[bytes]:
Poll for new MIDI messages from FL Studio
"""
ctx = get_context()
if (msg := ctx.port.receive(block=False)) is not None:
if (msg := ctx.res_port.receive(block=False)) is not None:
print([hex(b) for b in msg.bytes()])
# If there was a message, do pre-handling of message
# Make sure to remove the start and end bits to simplify processing
msg = handle_received_message(bytes(msg.bytes()[1:-1]))
Expand Down Expand Up @@ -204,7 +217,7 @@ def fl_exec(code: str) -> None:
send_msg(
consts.SYSEX_HEADER
+ bytes([MessageOrigin.CLIENT, client_id, MessageType.EXEC])
+ code.encode()
+ b64encode(code.encode())
)
response = receive_message()
log.debug("fl_exec: got response")
Expand All @@ -223,7 +236,7 @@ def fl_eval(expression: str) -> Any:
send_msg(
consts.SYSEX_HEADER
+ bytes([MessageOrigin.CLIENT, client_id, MessageType.EVAL])
+ expression.encode()
+ b64encode(expression.encode())
)
response = receive_message()
log.debug("fl_eval: got response")
Expand Down
9 changes: 7 additions & 2 deletions flapi/__context.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,14 @@

@dataclass
class FlapiContext:
port: BaseIOPort
req_port: BaseIOPort
"""
The Mido port that Flapi uses to communicate with FL Studio
The Mido port that Flapi uses to send requests to FL Studio
"""

res_port: BaseIOPort
"""
The Mido port that Flapi uses to receive responses from FL Studio
"""

functions_backup: 'ApiCopyType'
Expand Down
9 changes: 4 additions & 5 deletions flapi/__enable.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import random
import mido # type: ignore
from typing import Protocol, Generic, TypeVar, Optional
from mido.ports import BaseOutput, BaseInput, IOPort # type: ignore
from mido.ports import BaseOutput, BaseInput # type: ignore
from . import _consts as consts
from .__context import set_context, get_context, pop_context, FlapiContext
from .__comms import fl_exec, hello, version_query, poll_for_message
Expand Down Expand Up @@ -108,13 +108,11 @@ def enable(
log.exception("Could not open create new port")
raise FlapiPortError((req_port, res_port)) from e

port = IOPort(res, req)

# Now decorate all of the API functions
functions_backup = add_wrappers()

# Register the context
set_context(FlapiContext(port, functions_backup, None))
set_context(FlapiContext(req, res, functions_backup, None))

return try_init(random.randrange(1, 0x7F))

Expand Down Expand Up @@ -193,7 +191,8 @@ def disable():
"""
# Close all the ports
ctx = pop_context()
ctx.port.close()
ctx.req_port.close()
ctx.res_port.close()

# Then restore the functions
restore_original_functions(ctx.functions_backup)
11 changes: 11 additions & 0 deletions flapi/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,17 @@ def __init__(self, msg: str) -> None:
)


class FlapiServerExit(Exception):
"""
The Flapi server exited.
"""

def __init__(self) -> None:
super().__init__(
"The Flapi server exited, likely because FL Studio was closed."
)


class FlapiClientError(Exception):
"""
An unexpected error occurred on the client side.
Expand Down
13 changes: 9 additions & 4 deletions flapi/server/device_flapi_receive.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# name=Flapi Receive
# name=Flapi Request
# supportedDevices=Flapi Request
"""
# Flapi / Server / Flapi Receive
Expand Down Expand Up @@ -44,17 +44,18 @@ def OnInit():
f"Device assigned: {bool(device.isAssigned())}",
f"FL Studio port number: {device.getPortNumber()}",
]))
capout.enable()
# capout.enable()


def OnDeInit():
capout.disable()
# def OnDeInit():
# capout.disable()


connected_clients: set[int] = set()


def client_hello(res: FlapiResponse, data: bytes):
print(f"Hello from {res.client_id}")
if res.client_id in connected_clients:
# Client ID already taken, take no action
log.debug(f"Client tried to connect to in-use ID {res.client_id}")
Expand All @@ -78,6 +79,7 @@ def version_query(res: FlapiResponse, data: bytes):


def fl_exec(res: FlapiResponse, data: bytes):
print(f"Data: {[hex(b) for b in data]}")
statement = b64decode(data)
try:
# Exec in global scope so that the imports are remembered
Expand Down Expand Up @@ -120,6 +122,9 @@ def receive_stdout(res: FlapiResponse, data: bytes):


def OnSysEx(event: 'FlMidiMsg'):

print(f"Msg: {[hex(b) for b in event.sysex]}")

header = event.sysex[1:len(consts.SYSEX_HEADER)+1] # Sysex header
# Remaining sysex data
sysex_data = event.sysex[len(consts.SYSEX_HEADER)+1:-1]
Expand Down
43 changes: 33 additions & 10 deletions flapi/server/device_flapi_respond.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# name=Flapi Respond
# name=Flapi Response
# supportedDevices=Flapi Response
# receiveFrom=Flapi Receive
# receiveFrom=Flapi Request
"""
# Flapi / Server / Flapi Respond
Expand All @@ -19,22 +19,44 @@
pass


def OnSysEx(msg: 'FlMidiMsg'):
# def print_msg(name: str, msg: bytes):
# print(f"{name}: {[hex(b) for b in msg]}")


def OnSysEx(event: 'FlMidiMsg'):

header = event.sysex[1:len(SYSEX_HEADER)+1] # Sysex header
# print_msg("Header", header)
# Remaining sysex data
sysex_data = event.sysex[len(SYSEX_HEADER)+1:]
# print_msg("Data", sysex_data)

# Ignore events that don't target the respond script
if not msg.sysex.startswith(SYSEX_HEADER):
if header != SYSEX_HEADER:
print("Header no match")
return
sysex = msg.sysex.removeprefix(SYSEX_HEADER)

# Check message origin
if sysex[0] != MessageOrigin.INTERNAL:
if sysex_data[0] != MessageOrigin.INTERNAL:
print("Origin")
return

# Forward message back to client
# print_msg(
# "Result",
# (
# bytes([0xF0])
# + SYSEX_HEADER
# + bytes([MessageOrigin.SERVER])
# + sysex_data[1:]
# )
# )

device.midiOutSysex(
SYSEX_HEADER
bytes([0xF0])
+ SYSEX_HEADER
+ bytes([MessageOrigin.SERVER])
+ sysex[1:]
# + bytes([0xF7])
+ sysex_data[1:]
)


Expand All @@ -43,7 +65,8 @@ def OnDeInit():
Send server goodbye message
"""
device.midiOutSysex(
SYSEX_HEADER
bytes([0xF0])
+ SYSEX_HEADER
+ bytes(MessageOrigin.SERVER)
# Target all clients by giving 0x00 client ID
+ bytes([0x00])
Expand Down
25 changes: 18 additions & 7 deletions flapi/server/flapi_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ def __init__(self, client_id: int) -> None:

def fail(self, type: MessageType, info: str):
send_sysex(
SYSEX_HEADER
bytes([0xF0])
+ SYSEX_HEADER
+ bytes([MessageOrigin.INTERNAL])
+ bytes([self.client_id])
+ bytes([type])
Expand All @@ -63,19 +64,23 @@ def fail(self, type: MessageType, info: str):

def client_hello(self):
send_sysex(
SYSEX_HEADER
bytes([0xF0])
+ SYSEX_HEADER
+ bytes([MessageOrigin.INTERNAL])
+ bytes([self.client_id])
+ bytes([MessageType.CLIENT_HELLO])
+ bytes([MessageStatus.OK])
+ bytes([0xF7])
)

def client_goodbye(self, exit_code: int):
send_sysex(
SYSEX_HEADER
bytes([0xF0])
+ SYSEX_HEADER
+ bytes([MessageOrigin.INTERNAL])
+ bytes([self.client_id])
+ bytes([MessageType.CLIENT_GOODBYE])
+ bytes([MessageStatus.OK])
+ encode_python_object(exit_code)
+ bytes([0xF7])
)
Expand All @@ -84,10 +89,12 @@ def client_goodbye(self, exit_code: int):

def version_query(self, version_info: tuple[int, int, int]):
send_sysex(
SYSEX_HEADER
bytes([0xF0])
+ SYSEX_HEADER
+ bytes([MessageOrigin.INTERNAL])
+ bytes([self.client_id])
+ bytes([MessageType.VERSION_QUERY])
+ bytes([MessageStatus.OK])
+ bytes(version_info)
+ bytes([0xF7])
)
Expand Down Expand Up @@ -124,7 +131,8 @@ def exec(
response_data = bytes()

send_sysex(
SYSEX_HEADER
bytes([0xF0])
+ SYSEX_HEADER
+ bytes([MessageOrigin.INTERNAL])
+ bytes([self.client_id])
+ bytes([MessageType.EXEC])
Expand Down Expand Up @@ -164,7 +172,8 @@ def eval(
data: Exception | str | Any,
):
send_sysex(
SYSEX_HEADER
bytes([0xF0])
+ SYSEX_HEADER
+ bytes([MessageOrigin.INTERNAL])
+ bytes([self.client_id])
+ bytes([MessageType.EVAL])
Expand All @@ -175,10 +184,12 @@ def eval(

def stdout(self, content: str):
send_sysex(
SYSEX_HEADER
bytes([0xF0])
+ SYSEX_HEADER
+ bytes([MessageOrigin.INTERNAL])
+ bytes([self.client_id])
+ bytes([MessageType.STDOUT])
+ bytes([MessageStatus.OK])
+ encode_python_object(content)
+ bytes([0xF7])
)

0 comments on commit 6c00117

Please sign in to comment.