From d9e86f02ffe0ed645f364b2230ea47a737a2640f Mon Sep 17 00:00:00 2001 From: Laurent NOEL Date: Wed, 12 Feb 2020 16:21:04 +0100 Subject: [PATCH 01/11] Move settings panel from scene properties to 3D View side bar --- .gitignore | 6 ++++++ VRtistPanel.py | 10 +++++----- vrtistOperators.py | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index b7a97ac1..e27cbbd2 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,9 @@ $RECYCLE.BIN/ *.zip *.7z *.rar + +# Virtual envs +.venv + +# VS Code +.vscode \ No newline at end of file diff --git a/VRtistPanel.py b/VRtistPanel.py index 4efbee83..34de6037 100644 --- a/VRtistPanel.py +++ b/VRtistPanel.py @@ -5,11 +5,11 @@ class VRtistPanel(bpy.types.Panel): """Creates a Panel in the Object properties window""" - bl_label = "VRtist Panel" - bl_idname = "OBJECT_PT_vrtist" - bl_space_type = 'PROPERTIES' - bl_region_type = 'WINDOW' - bl_context = "scene" + bl_label = "VRtist" + bl_idname = "VRTIST_PT_settings" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_category = "VRtist" def draw(self, context): layout = self.layout diff --git a/vrtistOperators.py b/vrtistOperators.py index d9bfb909..54d8a5df 100644 --- a/vrtistOperators.py +++ b/vrtistOperators.py @@ -709,7 +709,7 @@ def set_handlers(connect: bool): class VRtistCreateRoomOperator(bpy.types.Operator): bl_idname = "scene.vrtistcreateroom" bl_label = "VRtist Create Room" - bl_options = {'REGISTER'} + bl_options = {'REGISTER'} def execute(self, context): connected = shareData.client is not None and shareData.client.isConnected() From 7ac7dadc4f33fbfc12c09bfee9a86e65269d6f2e Mon Sep 17 00:00:00 2001 From: Laurent NOEL Date: Wed, 12 Feb 2020 16:34:01 +0100 Subject: [PATCH 02/11] Use binary_path_python to find python --- vrtistOperators.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/vrtistOperators.py b/vrtistOperators.py index 54d8a5df..ecfe122c 100644 --- a/vrtistOperators.py +++ b/vrtistOperators.py @@ -745,11 +745,13 @@ def execute(self, context): VRtistRoomListUpdateOperator.rooms_cached = False return {'FINISHED'} -def start_local_server(): + +def start_local_server(self): dir_path = Path(__file__).parent - pythonPath = Path(sys.argv[0]).parent / (str(bpy.app.version[0]) + "." + str(bpy.app.version[1])) / 'python' / 'bin' / 'python' serverPath = dir_path / 'broadcaster' / 'dccBroadcaster.py' - shareData.localServerProcess = subprocess.Popen([str(pythonPath), str(serverPath)],stdout=subprocess.PIPE,stderr=subprocess.STDOUT, shell=False) + shareData.localServerProcess = subprocess.Popen([bpy.app.binary_path_python, str( + serverPath)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False) + def server_is_up(address, port): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) From ede7bb6d4ae66bf4af40215901ff23237d61e0dd Mon Sep 17 00:00:00 2001 From: Laurent NOEL Date: Wed, 12 Feb 2020 17:20:26 +0100 Subject: [PATCH 03/11] Use logging in dccBroadcaster and log all commands --- broadcaster/dccBroadcaster.py | 37 +++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/broadcaster/dccBroadcaster.py b/broadcaster/dccBroadcaster.py index be3b9d29..df2fb755 100644 --- a/broadcaster/dccBroadcaster.py +++ b/broadcaster/dccBroadcaster.py @@ -1,17 +1,19 @@ +import common import socket import select import threading +import logging import os import sys sys.path.append(os.getcwd()) -import common TIMEOUT = 60.0 HOST = '' PORT = 12800 + class Connection: def __init__(self, socket, address): self.socket = socket @@ -28,7 +30,7 @@ def joinRoom(self, roomName): with common.mutex: room = rooms.get(roomName) if room is None: - print ("Create room : " + roomName) + logging.info(f"Room {roomName} does not exist. Creating it.") room = Room(roomName) rooms[roomName] = room room.addClient(self) @@ -56,7 +58,8 @@ def listRoomClients(self, name): with common.mutex: room = rooms.get(name) if room is not None: - command = common.Command(common.MessageType.LIST_ROOM_CLIENTS, common.encodeStringArray([f'{client.address[0]}:{client.address[1]}' for client in room.clients])) + command = common.Command(common.MessageType.LIST_ROOM_CLIENTS, common.encodeStringArray( + [f'{client.address[0]}:{client.address[1]}' for client in room.clients])) self.commands.append(command) def listClients(self): @@ -76,6 +79,8 @@ def run(self): break if command is not None: + logging.info(f"client {self.address}: {command.type} received") + if command.type == common.MessageType.JOIN_ROOM: self.joinRoom(command.data.decode()) @@ -99,7 +104,7 @@ def run(self): if self.room is not None: self.room.addCommand(command, self) else: - print("COMMAND received but no room was joined") + logging.error("COMMAND received but no room was joined") if len(self.commands) > 0: with common.mutex: @@ -119,7 +124,8 @@ def close(self): self.socket.close() except Exception: pass - print (f"{self.address} closed") + logging.info(f"{self.address} closed") + class Room: def __init__(self, roomName): @@ -128,7 +134,7 @@ def __init__(self, roomName): self.commands = [] def addClient(self, client): - print (f"Add Client {client.address} to Room {self.name}") + logging.info(f"Add Client {client.address} to Room {self.name}") self.clients.append(client) if len(self.clients) == 1: command = common.Command(common.MessageType.CONTENT) @@ -146,26 +152,26 @@ def close(self): self.clients = [] with common.mutex: del rooms[self.name] - print(f'Room {self.name} deleted') + logging.info(f'Room {self.name} deleted') def clear(self): self.commands = [] def removeClient(self, client): - print (f"Remove Client {client.address} from Room {self.name}") + logging.info(f"Remove Client {client.address} from Room {self.name}") self.clients.remove(client) if len(self.clients) == 0: with common.mutex: del rooms[self.name] - print (f'No more clients in room "{self.name}". Room deleted') + logging.info(f'No more clients in room "{self.name}". Room deleted') def mergeCommands(self, command): commandType = command.type if commandType.value > common.MessageType.OPTIMIZED_COMMANDS.value: - commandPath = common.decodeString(command.data,0)[0] + commandPath = common.decodeString(command.data, 0)[0] if len(self.commands) > 0: storedCommand = self.commands[-1] - if commandType == storedCommand.type and commandPath == common.decodeString(storedCommand.data,0)[0]: + if commandType == storedCommand.type and commandPath == common.decodeString(storedCommand.data, 0)[0]: self.commands.pop() self.commands.append(command) @@ -179,17 +185,22 @@ def addCommand(self, command, sender): rooms = {} + def runServer(): connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM) connection.bind((HOST, PORT)) connection.listen(1000) - print(f"Listening on port {PORT}") + logging.info(f"Listening on port {PORT}") while True: newConnection = connection.accept() - print(f"New connection {newConnection[1]}") + logging.info(f"New connection {newConnection[1]}") Connection(*newConnection) connection.close() + +logging.basicConfig( + format='%(asctime)s - %(levelname)s - %(message)s', level=logging.INFO) + runServer() From 779cd61c8a542c4fc210db7ba40ba90716eee453 Mon Sep 17 00:00:00 2001 From: Laurent NOEL Date: Wed, 12 Feb 2020 19:24:18 +0100 Subject: [PATCH 04/11] Fix python error --- vrtistOperators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vrtistOperators.py b/vrtistOperators.py index ecfe122c..9dc9f188 100644 --- a/vrtistOperators.py +++ b/vrtistOperators.py @@ -746,7 +746,7 @@ def execute(self, context): return {'FINISHED'} -def start_local_server(self): +def start_local_server(): dir_path = Path(__file__).parent serverPath = dir_path / 'broadcaster' / 'dccBroadcaster.py' shareData.localServerProcess = subprocess.Popen([bpy.app.binary_path_python, str( From b05ea9c6eca40d4a12af875f3da35356595d36a7 Mon Sep 17 00:00:00 2001 From: Laurent NOEL Date: Wed, 12 Feb 2020 19:27:48 +0100 Subject: [PATCH 05/11] Refactor: Regroup code according to purpose - All operartors in vrtistOperators - All UI in VRtistPanel - register and unregister cleanup - code formatting - VRtistSayHello operator (for my quick tests) --- VRtistPanel.py | 41 ++++++++++++++++++++++++++++-------- __init__.py | 52 +++++++++++++++++++--------------------------- vrtistOperators.py | 43 ++++++++++++++++++++++++++++++++------ 3 files changed, 90 insertions(+), 46 deletions(-) diff --git a/VRtistPanel.py b/VRtistPanel.py index 34de6037..1000c2ad 100644 --- a/VRtistPanel.py +++ b/VRtistPanel.py @@ -3,6 +3,12 @@ from . import vrtistOperators +class ROOM_UL_ItemRenderer(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + split = layout.row() + split.label(text=item.name) # avoids renaming the item by accident + + class VRtistPanel(bpy.types.Panel): """Creates a Panel in the Object properties window""" bl_label = "VRtist" @@ -19,36 +25,53 @@ def draw(self, context): row = layout.row() row.label(text="VRtist", icon='SCENE_DATA') - row = layout.column() - row.operator("scene.vrtist", text="Launch VRTist") + row = layout.column() + row.operator("scene.vrtist", text="Launch VRTist") + row.operator(vrtistOperators.VRtistSayHello.bl_idname, text="Say Hello") connected = vrtistOperators.shareData.client is not None and vrtistOperators.shareData.client.isConnected() if not connected: # Room list row = layout.row() - row.template_list("ROOM_UL_ItemRenderer", "", scene.vrtistconnect, "rooms", scene.vrtistconnect, "room_index", rows=4) + row.template_list("ROOM_UL_ItemRenderer", "", scene.vrtistconnect, + "rooms", scene.vrtistconnect, "room_index", rows=4) # Join room col = row.column() col.operator("scene.vrtistroomlistupdate", text="Refresh") col.operator("scene.vrtistjoinroom", text="Join Room") if scene.vrtistconnect.remoteServerIsUp: - row = layout.row() + row = layout.row() row.prop(scene.vrtistconnect, "room", text="Room") row.operator('scene.vrtistcreateroom', text='Create Room') - col = layout.column() + col = layout.column() row = col.row() row.prop(scene.vrtistconnect, "advanced", - icon="TRIA_DOWN" if scene.vrtistconnect.advanced else "TRIA_RIGHT", - icon_only=True, emboss=False) - row.label(text = "Advanced options") + icon="TRIA_DOWN" if scene.vrtistconnect.advanced else "TRIA_RIGHT", + icon_only=True, emboss=False) + row.label(text="Advanced options") if scene.vrtistconnect.advanced: col.prop(scene.vrtistconnect, "host", text="Host") col.prop(scene.vrtistconnect, "port", text="Port") col.prop(scene.vrtistconnect, "VRtist", text="VRtist Path") - else: + else: row.operator("scene.vrtistjoinroom", text="Leave Room") + +classes = ( + ROOM_UL_ItemRenderer, + VRtistPanel +) + + +def register(): + for _ in classes: + bpy.utils.register_class(_) + + +def unregister(): + for _ in classes: + bpy.utils.unregister_class(_) diff --git a/__init__.py b/__init__.py index 63609f6b..b9f2e84d 100644 --- a/__init__.py +++ b/__init__.py @@ -1,23 +1,24 @@ +from . import VRtistPanel +from . import vrtistOperators import bpy import atexit bl_info = { - "name" : "VRtist", - "author" : "Ubisoft", - "description" : "VR manipultation", - "blender" : (2 ,80, 0), - "location" : "", - "warning" : "", - "category" : "Generic" + "name": "VRtist", + "author": "Ubisoft", + "description": "VR manipultation", + "blender": (2, 80, 0), + "location": "", + "warning": "", + "category": "Generic" } -from . import vrtistOperators -from . import VRtistPanel def refreshRoomListHack(): bpy.ops.scene.vrtistroomlistupdate() return None + def cleanup(): try: if vrtistOperators.shareData.localServerProcess: @@ -25,37 +26,26 @@ def cleanup(): except Exception: pass -def register(): - bpy.utils.register_class(vrtistOperators.ROOM_UL_ItemRenderer) - bpy.utils.register_class(vrtistOperators.VRtistOperator) - bpy.utils.register_class(vrtistOperators.VRtistRoomItem) - bpy.utils.register_class(vrtistOperators.VRtistConnectProperties) - bpy.utils.register_class(vrtistOperators.VRtistCreateRoomOperator) - bpy.utils.register_class(vrtistOperators.VRtistRoomListUpdateOperator) - bpy.utils.register_class(vrtistOperators.VRtistSendSelectionOperator) - bpy.utils.register_class(vrtistOperators.VRtistJoinRoomOperator) +def register(): + vrtistOperators.register() + VRtistPanel.register() + bpy.types.Scene.vrtistconnect = bpy.props.PointerProperty(type=vrtistOperators.VRtistConnectProperties) - bpy.utils.register_class(VRtistPanel.VRtistPanel) bpy.app.timers.register(refreshRoomListHack, first_interval=0) atexit.register(cleanup) - + + def unregister(): - bpy.utils.unregister_class(VRtistPanel.VRtistPanel) + vrtistOperators.unregister() + VRtistPanel.unregister() + del bpy.types.Scene.vrtistconnect - bpy.utils.unregister_class(vrtistOperators.VRtistSendSelectionOperator) - bpy.utils.unregister_class(vrtistOperators.VRtistConnectProperties) - bpy.utils.unregister_class(vrtistOperators.VRtistCreateRoomOperator) - bpy.utils.unregister_class(vrtistOperators.VRtistRoomListUpdateOperator) - bpy.utils.unregister_class(vrtistOperators.VRtistJoinRoomOperator) - bpy.utils.unregister_class(vrtistOperators.VRtistRoomItem) - bpy.utils.unregister_class(vrtistOperators.VRtistOperator) - bpy.utils.unregister_class(vrtistOperators.ROOM_UL_ItemRenderer) cleanup() atexit.unregister(cleanup) - + + if __name__ == "__main__": register() - \ No newline at end of file diff --git a/vrtistOperators.py b/vrtistOperators.py index 9dc9f188..8209bd53 100644 --- a/vrtistOperators.py +++ b/vrtistOperators.py @@ -2,6 +2,7 @@ import sys import subprocess import shutil +import logging from pathlib import Path import bpy @@ -16,6 +17,12 @@ PORT = 12800 +logger = logging.Logger("VRtist") +logger.setLevel(logging.INFO) +handler = logging.StreamHandler() +logger.addHandler(handler) +handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) + class TransformStruct: def __init__(self, translate, quaternion, scale, visible): self.translate = translate @@ -591,11 +598,6 @@ def execute(self, context): updateListRoomsProperty() return {'FINISHED'} -class ROOM_UL_ItemRenderer(UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): - split = layout.row() - split.label(text=item.name) # avoids renaming the item by accident - def clear_scene_content(): set_handlers(False) @@ -854,4 +856,33 @@ def execute(self, context): hostname = vrtistconnect.host args = [bpy.data.scenes[0].vrtistconnect.VRtist, "--room",room, "--hostname", hostname, "--port", str(vrtistconnect.port)] subprocess.Popen(args, stdout=subprocess.PIPE,stderr=subprocess.STDOUT, shell=False) - return {'FINISHED'} + return {'FINISHED'} + + +class VRtistSayHello(bpy.types.Operator): + bl_idname = "vrtist.say_hello" + bl_label = "VRtist Say Hello" + bl_options = {'REGISTER'} + + def execute(self, context): + logger.info("I say hello") + return {'FINISHED'} + +classes = ( + VRtistOperator, + VRtistRoomItem, + VRtistConnectProperties, + VRtistCreateRoomOperator, + VRtistRoomListUpdateOperator, + VRtistSendSelectionOperator, + VRtistJoinRoomOperator, + VRtistSayHello, +) + +def register(): + for _ in classes: + bpy.utils.register_class(_) + +def unregister(): + for _ in classes: + bpy.utils.unregister_class(_) From 0dab1d21cf327ce76c993f8e2d17214c69461244 Mon Sep 17 00:00:00 2001 From: Laurent NOEL Date: Wed, 12 Feb 2020 19:39:32 +0100 Subject: [PATCH 06/11] Rename files for coherency --- __init__.py | 18 +++++++++--------- vrtistOperators.py => operators.py | 0 VRtistPanel.py => ui.py | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) rename vrtistOperators.py => operators.py (100%) rename VRtistPanel.py => ui.py (90%) diff --git a/__init__.py b/__init__.py index b9f2e84d..175f3722 100644 --- a/__init__.py +++ b/__init__.py @@ -1,5 +1,5 @@ -from . import VRtistPanel -from . import vrtistOperators +from . import ui +from . import operators import bpy import atexit @@ -21,25 +21,25 @@ def refreshRoomListHack(): def cleanup(): try: - if vrtistOperators.shareData.localServerProcess: - vrtistOperators.shareData.localServerProcess.kill() + if operators.shareData.localServerProcess: + operators.shareData.localServerProcess.kill() except Exception: pass def register(): - vrtistOperators.register() - VRtistPanel.register() + operators.register() + ui.register() - bpy.types.Scene.vrtistconnect = bpy.props.PointerProperty(type=vrtistOperators.VRtistConnectProperties) + bpy.types.Scene.vrtistconnect = bpy.props.PointerProperty(type=operators.VRtistConnectProperties) bpy.app.timers.register(refreshRoomListHack, first_interval=0) atexit.register(cleanup) def unregister(): - vrtistOperators.unregister() - VRtistPanel.unregister() + operators.unregister() + ui.unregister() del bpy.types.Scene.vrtistconnect diff --git a/vrtistOperators.py b/operators.py similarity index 100% rename from vrtistOperators.py rename to operators.py diff --git a/VRtistPanel.py b/ui.py similarity index 90% rename from VRtistPanel.py rename to ui.py index 1000c2ad..37b5ff3f 100644 --- a/VRtistPanel.py +++ b/ui.py @@ -1,6 +1,6 @@ import os import bpy -from . import vrtistOperators +from . import operators class ROOM_UL_ItemRenderer(bpy.types.UIList): @@ -27,9 +27,9 @@ def draw(self, context): row = layout.column() row.operator("scene.vrtist", text="Launch VRTist") - row.operator(vrtistOperators.VRtistSayHello.bl_idname, text="Say Hello") + row.operator(operators.VRtistSayHello.bl_idname, text="Say Hello") - connected = vrtistOperators.shareData.client is not None and vrtistOperators.shareData.client.isConnected() + connected = operators.shareData.client is not None and operators.shareData.client.isConnected() if not connected: # Room list From 2ef2ecf3144d1cc9bc047e00f47ec8b11d91c834 Mon Sep 17 00:00:00 2001 From: Laurent NOEL Date: Wed, 12 Feb 2020 19:45:26 +0100 Subject: [PATCH 07/11] Move cli.py to broadcaster module --- cli.py => broadcaster/cli.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) rename cli.py => broadcaster/cli.py (97%) diff --git a/cli.py b/broadcaster/cli.py similarity index 97% rename from cli.py rename to broadcaster/cli.py index f4305b68..2057e119 100644 --- a/cli.py +++ b/broadcaster/cli.py @@ -2,8 +2,8 @@ import queue import argparse -import broadcaster.common as common -import broadcaster.client as client +import common +import client TIMEOUT = 10 # in seconds @@ -116,6 +116,7 @@ def process_room_command(args): if client: client.disconnect() + def process_client_command(args): client = None @@ -136,7 +137,8 @@ def process_client_command(args): # Room commands are relative to... a room! room_parser = sub_parsers.add_parser('room', help='Rooms related commands') -room_parser.add_argument('command', help='Commands. Use "list" to list all the rooms of the server. Use "delete" to delete one or more rooms. Use "clear" to clear the commands stack of rooms. Use "clients" to list the clients connected to rooms.', choices=('list', 'delete', 'clear', 'clients')) +room_parser.add_argument('command', help='Commands. Use "list" to list all the rooms of the server. Use "delete" to delete one or more rooms. Use "clear" to clear the commands stack of rooms. Use "clients" to list the clients connected to rooms.', choices=( + 'list', 'delete', 'clear', 'clients')) room_parser.add_argument('name', help='Room name. You can specify multiple room names separated by spaces.', nargs='*') room_parser.set_defaults(func=process_room_command) From aa4f6a6ce0fe1aa1f510be7bdd2a36435069a286 Mon Sep 17 00:00:00 2001 From: Laurent NOEL Date: Thu, 13 Feb 2020 12:46:47 +0100 Subject: [PATCH 08/11] Naming refactor - Rename symbols according to their purpose (DCCSync vs VRtist) - Move Properties to window_manager instead of scene - Add data.py to define Blender's data - Add and init logger - Avoid contant values duplication when possible (operator names in UI, network host and port) --- __init__.py | 17 +++- broadcaster/cli.py | 4 +- broadcaster/client.py | 9 +- broadcaster/common.py | 62 +++++++++--- broadcaster/dccBroadcaster.py | 5 +- clientBlender.py | 9 +- data.py | 42 ++++++++ operators.py | 182 ++++++++++++++-------------------- ui.py | 49 +++++---- 9 files changed, 217 insertions(+), 162 deletions(-) create mode 100644 data.py diff --git a/__init__.py b/__init__.py index 175f3722..2ba9319e 100644 --- a/__init__.py +++ b/__init__.py @@ -1,7 +1,9 @@ from . import ui from . import operators +from . import data import bpy import atexit +import logging bl_info = { "name": "VRtist", @@ -15,7 +17,7 @@ def refreshRoomListHack(): - bpy.ops.scene.vrtistroomlistupdate() + bpy.ops.dcc_sync.update_room_list() return None @@ -28,10 +30,16 @@ def cleanup(): def register(): + logger = logging.getLogger("DCCSync") + if len(logger.handlers) == 0: + logger.setLevel(logging.INFO) + handler = logging.StreamHandler() + logger.addHandler(handler) + handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) + operators.register() ui.register() - - bpy.types.Scene.vrtistconnect = bpy.props.PointerProperty(type=operators.VRtistConnectProperties) + data.register() bpy.app.timers.register(refreshRoomListHack, first_interval=0) atexit.register(cleanup) @@ -40,8 +48,7 @@ def register(): def unregister(): operators.unregister() ui.unregister() - - del bpy.types.Scene.vrtistconnect + data.unregister() cleanup() atexit.unregister(cleanup) diff --git a/broadcaster/cli.py b/broadcaster/cli.py index 2057e119..9c784918 100644 --- a/broadcaster/cli.py +++ b/broadcaster/cli.py @@ -131,8 +131,8 @@ def process_client_command(args): parser = argparse.ArgumentParser(prog='cli', description='Command Line Interface for VRtist server') sub_parsers = parser.add_subparsers() -parser.add_argument('--host', help='Host name', default=client.HOST) -parser.add_argument('--port', help='Port', default=client.PORT) +parser.add_argument('--host', help='Host name', default=common.DEFAULT_HOST) +parser.add_argument('--port', help='Port', default=common.DEFAULT_PORT) parser.add_argument('--timeout', help='Timeout for server response', default=TIMEOUT) # Room commands are relative to... a room! diff --git a/broadcaster/client.py b/broadcaster/client.py index f037ff01..685b1ff5 100644 --- a/broadcaster/client.py +++ b/broadcaster/client.py @@ -8,11 +8,9 @@ except ImportError: import common -HOST = "localhost" -PORT = 12800 class Client: - def __init__(self, host = HOST, port = PORT): + def __init__(self, host=common.DEFAULT_HOST, port=common.DEFAULT_PORT): self.host = host self.port = port self.receivedCommands = queue.Queue() @@ -26,7 +24,7 @@ def __init__(self, host = HOST, port = PORT): self.socket.connect((host, port)) print(f"Connection on port {port}") except Exception as e: - print("Connection error ",e) + print("Connection error ", e) self.socket = None if self.socket: @@ -59,7 +57,7 @@ def addCommand(self, command): self.pendingCommands.put(command) def joinRoom(self, roomName): - common.writeMessage(self.socket,common.Command(common.MessageType.JOIN_ROOM, roomName.encode('utf8'), 0) ) + common.writeMessage(self.socket, common.Command(common.MessageType.JOIN_ROOM, roomName.encode('utf8'), 0)) def send(self, data): with common.mutex: @@ -115,4 +113,3 @@ def blenderExists(self): client.addCommand(common.Command(common.MessageType.DELETE, encodedMsg[6:])) elif msg.startswith("Room"): client.joinRoom(msg[4:]) - diff --git a/broadcaster/common.py b/broadcaster/common.py index 4a2d34c7..b84d97bf 100644 --- a/broadcaster/common.py +++ b/broadcaster/common.py @@ -7,6 +7,9 @@ mutex = threading.RLock() +DEFAULT_HOST = "localhost" +DEFAULT_PORT = 12800 + class MessageType(Enum): JOIN_ROOM = 1 @@ -44,34 +47,42 @@ class MessageType(Enum): MESH = 202 MATERIAL = 203 + class LightType(Enum): - SPOT = 0 # directly mapped from Unity enum + SPOT = 0 # directly mapped from Unity enum SUN = 1 POINT = 2 + class SensorFitMode(Enum): AUTO = 0 VERTICAL = 1 HORIZONTAL = 2 + class ClientDisconnectedException(Exception): '''When a client is disconnected and we try to read from it.''' -def intToBytes(value, size = 8): + +def intToBytes(value, size=8): return value.to_bytes(size, byteorder='little') + def bytesToInt(value): return int.from_bytes(value, 'little') + def intToMessageType(value): return MessageType(value) + def encodeBool(value): - if value: + if value: return intToBytes(1, 4) else: return intToBytes(0, 4) + def decodeBool(data, index): value = bytesToInt(data[index:index+4]) if value == 1: @@ -79,9 +90,11 @@ def decodeBool(data, index): else: return False, index+4 + def encodeString(value): encodedValue = value.encode() - return intToBytes(len(encodedValue),4) + encodedValue + return intToBytes(len(encodedValue), 4) + encodedValue + def decodeString(data, index): stringLength = bytesToInt(data[index:index+4]) @@ -90,60 +103,75 @@ def decodeString(data, index): value = data[start:end].decode() return value, end + def encodeFloat(value): return struct.pack('f', value) + def decodeFloat(data, index): return struct.unpack('f', data[index:index+4])[0], index+4 + def encodeInt(value): return struct.pack('I', value) + def decodeInt(data, index): return struct.unpack('I', data[index:index+4])[0], index+4 + def encodeVector2(value): return struct.pack('2f', *(value.x, value.y)) + def decodeVector2(data, index): return struct.unpack('2f', data[index:index+2*4]), index+2*4 + def encodeVector3(value): return struct.pack('3f', *(value.x, value.y, value.z)) + def decodeVector3(data, index): return struct.unpack('3f', data[index:index+3*4]), index+3*4 + def encodeColor(value): if len(value) == 3: return struct.pack('4f', *(value[0], value[1], value[2], 1.0)) else: return struct.pack('4f', *(value[0], value[1], value[2], value[3])) + def decodeColor(data, index): return struct.unpack('4f', data[index:index+4*4]), index+4*4 + def encodeVector4(value): return struct.pack('4f', *(value.x, value.y, value.z, value.w)) + def decodeVector4(data, index): return struct.unpack('4f', data[index:index+4*4]), index+4*4 + def encodeStringArray(values): buffer = encodeInt(len(values)) for item in values: buffer += encodeString(item) return buffer + def decodeStringArray(data, index): count = bytesToInt(data[index:index+4]) index = index + 4 values = [] for _ in range(count): - string, index = decodeString(data, index) + string, index = decodeString(data, index) values.append(string) return values, index + def decodeArray(data, index, schema, inc): count = bytesToInt(data[index:index+4]) start = index+4 @@ -155,9 +183,11 @@ def decodeArray(data, index, schema, inc): start = end return values, end + def decodeFloatArray(data, index): return decodeArray(data, index, 'f', 4) + def decodeIntArray(data, index): count = bytesToInt(data[index:index+4]) start = index+4 @@ -168,22 +198,27 @@ def decodeIntArray(data, index): start = end return values, end + def decodeInt2Array(data, index): return decodeArray(data, index, '2I', 2*4) + def decodeInt3Array(data, index): return decodeArray(data, index, '3I', 3*4) + def decodeVector3Array(data, index): return decodeArray(data, index, '3f', 3*4) + def decodeVector2Array(data, index): return decodeArray(data, index, '2f', 2*4) + def readMessage(socket): if not socket: return None - r,_,_ = select.select([socket],[],[],0.0001) + r, _, _ = select.select([socket], [], [], 0.0001) if len(r) > 0: try: msg = socket.recv(14) @@ -203,23 +238,24 @@ def readMessage(socket): except ClientDisconnectedException: raise except Exception as e: - print (e) + print(e) raise ClientDisconnectedException() return None + def writeMessage(socket, command): if not socket: return - size = intToBytes(len(command.data),8) - commandId = intToBytes(command.id,4) - mtype = intToBytes(command.type.value,2) + size = intToBytes(len(command.data), 8) + commandId = intToBytes(command.id, 4) + mtype = intToBytes(command.type.value, 2) buffer = size + commandId + mtype + command.data remainingSize = len(buffer) currentIndex = 0 while remainingSize > 0: - _,w,_ = select.select([],[socket],[],0.0001) + _, w, _ = select.select([], [socket], [], 0.0001) if len(w) > 0: sent = socket.send(buffer[currentIndex:]) remainingSize -= sent @@ -228,11 +264,11 @@ def writeMessage(socket, command): class Command: _id = 100 - def __init__(self, commandType, data = b'', commandId = 0): + + def __init__(self, commandType, data=b'', commandId=0): self.data = data or b'' self.type = commandType self.id = commandId if commandId == 0: self.id = Command._id Command._id += 1 - diff --git a/broadcaster/dccBroadcaster.py b/broadcaster/dccBroadcaster.py index df2fb755..9a1b89c9 100644 --- a/broadcaster/dccBroadcaster.py +++ b/broadcaster/dccBroadcaster.py @@ -10,8 +10,7 @@ TIMEOUT = 60.0 -HOST = '' -PORT = 12800 +BINDING_HOST = '' class Connection: @@ -188,7 +187,7 @@ def addCommand(self, command, sender): def runServer(): connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - connection.bind((HOST, PORT)) + connection.bind((BINDING_HOST, common.DEFAULT_PORT)) connection.listen(1000) logging.info(f"Listening on port {PORT}") diff --git a/clientBlender.py b/clientBlender.py index 73d3e0ca..c6a97cfd 100644 --- a/clientBlender.py +++ b/clientBlender.py @@ -5,17 +5,14 @@ _STILL_ACTIVE = 259 from .broadcaster.client import Client +from .broadcaster import common import bpy import bmesh import struct import queue -from .broadcaster import common - -HOST = "localhost" -PORT = 12800 class ClientBlender(Client): - def __init__(self, host = HOST, port = PORT): + def __init__(self, host=common.DEFAULT_HOST, port=common.DEFAULT_PORT): super(ClientBlender, self).__init__(host, port) self.objectNames = {} # object name / object @@ -900,4 +897,4 @@ def networkConsumer(self): self.receivedCommands.task_done() self.blockSignals = False - \ No newline at end of file + diff --git a/data.py b/data.py new file mode 100644 index 00000000..3d826e66 --- /dev/null +++ b/data.py @@ -0,0 +1,42 @@ +import bpy +import os +from .broadcaster import common + + +def get_dcc_sync_props(): + return bpy.context.window_manager.dcc_sync + + +class RoomItem(bpy.types.PropertyGroup): + name: bpy.props.StringProperty(name="Name") + + +class DCCSyncProperties(bpy.types.PropertyGroup): + #host: bpy.props.StringProperty(name="Host", default="lgy-wks-052279") + host: bpy.props.StringProperty(name="Host", default=os.environ.get("VRTIST_HOST", common.DEFAULT_HOST)) + port: bpy.props.IntProperty(name="Port", default=common.DEFAULT_PORT) + room: bpy.props.StringProperty(name="Room", default=os.getlogin()) + rooms: bpy.props.CollectionProperty(name="Rooms", type=RoomItem) + room_index: bpy.props.IntProperty() # index in the list of rooms + advanced: bpy.props.BoolProperty(default=False) + remoteServerIsUp: bpy.props.BoolProperty(default=False) + VRtist: bpy.props.StringProperty(name="VRtist", default=os.environ.get( + "VRTIST_EXE", "D:/unity/VRtist/Build/VRtist.exe")) + + +classes = ( + RoomItem, + DCCSyncProperties, +) + + +def register(): + for _ in classes: + bpy.utils.register_class(_) + bpy.types.WindowManager.dcc_sync = bpy.props.PointerProperty(type=DCCSyncProperties) + + +def unregister(): + for _ in classes: + bpy.utils.unregister_class(_) + del bpy.types.WindowManager.dcc_sync diff --git a/operators.py b/operators.py index 8209bd53..6c17211b 100644 --- a/operators.py +++ b/operators.py @@ -13,15 +13,9 @@ from bpy.app.handlers import persistent from bpy.types import UIList -HOST = 'localhost' -PORT = 12800 +from .data import get_dcc_sync_props - -logger = logging.Logger("VRtist") -logger.setLevel(logging.INFO) -handler = logging.StreamHandler() -logger.addHandler(handler) -handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) +logger = logging.getLogger("DCC Sync") class TransformStruct: def __init__(self, translate, quaternion, scale, visible): @@ -70,22 +64,6 @@ def clearLists(self): shareData = ShareData() - -class VRtistRoomItem(bpy.types.PropertyGroup): - name: bpy.props.StringProperty(name="Name") - - -class VRtistConnectProperties(bpy.types.PropertyGroup): - #host: bpy.props.StringProperty(name="Host", default="lgy-wks-052279") - host: bpy.props.StringProperty(name="Host", default=os.environ.get("VRTIST_HOST","localhost")) - port: bpy.props.IntProperty(name="Port", default=PORT) - room: bpy.props.StringProperty(name="Room", default=os.getlogin()) - rooms: bpy.props.CollectionProperty(name="Rooms", type=VRtistRoomItem) - room_index: bpy.props.IntProperty() # index in the list of rooms - advanced: bpy.props.BoolProperty(default=False) - remoteServerIsUp: bpy.props.BoolProperty(default=False) - VRtist: bpy.props.StringProperty(name="VRtist", default=os.environ.get("VRTIST_EXE","D:/unity/VRtist/Build/VRtist.exe")) - def updateParams(obj): # send collection instances if obj.instance_type == 'COLLECTION': @@ -128,7 +106,7 @@ def leave_current_room(): shareData.client.disconnect() del(shareData.client) shareData.client = None - VRtistRoomListUpdateOperator.rooms_cached = False + UpdateRoomListOperator.rooms_cached = False @persistent def onLoad(scene): @@ -529,10 +507,10 @@ def onUndoRedoPost(scene): getting_rooms = False def updateListRoomsProperty(): - bpy.data.scenes[0].vrtistconnect.rooms.clear() + get_dcc_sync_props().rooms.clear() if rooms_cache: for room in rooms_cache: - item = bpy.data.scenes[0].vrtistconnect.rooms.add() + item = get_dcc_sync_props().rooms.add() item.name = room def onRooms(rooms): @@ -554,10 +532,10 @@ def onRooms(rooms): getting_rooms = False def addLocalRoom(): - bpy.data.scenes[0].vrtistconnect.rooms.clear() - localItem = bpy.data.scenes[0].vrtistconnect.rooms.add() + get_dcc_sync_props().rooms.clear() + localItem = get_dcc_sync_props().rooms.add() localItem.name = "Local" - VRtistRoomListUpdateOperator.rooms_cached = True + UpdateRoomListOperator.rooms_cached = True def getRooms(force=False): global getting_rooms, rooms_cache @@ -567,12 +545,12 @@ def getRooms(force=False): if not force and rooms_cache: return - host = bpy.data.scenes[0].vrtistconnect.host - port = bpy.data.scenes[0].vrtistconnect.port + host = get_dcc_sync_props().host + port = get_dcc_sync_props().port shareData.roomListUpdateClient = None up = server_is_up(host, port) - bpy.data.scenes[0].vrtistconnect.remoteServerIsUp = up + get_dcc_sync_props().remoteServerIsUp = up if up: shareData.roomListUpdateClient = clientBlender.ClientBlender(host, port) @@ -588,16 +566,6 @@ def getRooms(force=False): shareData.roomListUpdateClient.addCallback('roomsList', onRooms) shareData.roomListUpdateClient.sendListRooms() -class VRtistRoomListUpdateOperator(bpy.types.Operator): - bl_idname = "scene.vrtistroomlistupdate" - bl_label = "VRtist Update Room List" - bl_options = {'REGISTER'} - - def execute(self, context): - getRooms(force=True) - updateListRoomsProperty() - return {'FINISHED'} - def clear_scene_content(): set_handlers(False) @@ -708,9 +676,28 @@ def set_handlers(connect: bool): print ("Error setting handlers") -class VRtistCreateRoomOperator(bpy.types.Operator): - bl_idname = "scene.vrtistcreateroom" - bl_label = "VRtist Create Room" +def start_local_server(): + dir_path = Path(__file__).parent + serverPath = dir_path / 'broadcaster' / 'dccBroadcaster.py' + shareData.localServerProcess = subprocess.Popen([bpy.app.binary_path_python, str( + serverPath)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False) + + +def server_is_up(address, port): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + s.connect((address, port)) + s.shutdown(socket.SHUT_RDWR) + s.close() + return True + except: + return False + + +class CreateRoomOperator(bpy.types.Operator): + """Create a new room on DCC Sync server""" + bl_idname = "dcc_sync.create_room" + bl_label = "DCCSync Create Room" bl_options = {'REGISTER'} def execute(self, context): @@ -727,13 +714,13 @@ def execute(self, context): shareData.client = None else: shareData.isLocal = False - room = bpy.data.scenes[0].vrtistconnect.room - host = bpy.data.scenes[0].vrtistconnect.host - port = bpy.data.scenes[0].vrtistconnect.port + room = get_dcc_sync_props().room + host = get_dcc_sync_props().host + port = get_dcc_sync_props().port shareData.client = clientBlender.ClientBlender(host, port) - shareData.client.addCallback('SendContent',send_scene_content) - shareData.client.addCallback('ClearContent',clear_scene_content) + shareData.client.addCallback('SendContent', send_scene_content) + shareData.client.addCallback('ClearContent', clear_scene_content) if not shareData.client.isConnected(): return {'CANCELLED'} @@ -744,30 +731,14 @@ def execute(self, context): shareData.currentRoom = room set_handlers(True) - VRtistRoomListUpdateOperator.rooms_cached = False + UpdateRoomListOperator.rooms_cached = False return {'FINISHED'} -def start_local_server(): - dir_path = Path(__file__).parent - serverPath = dir_path / 'broadcaster' / 'dccBroadcaster.py' - shareData.localServerProcess = subprocess.Popen([bpy.app.binary_path_python, str( - serverPath)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False) - - -def server_is_up(address, port): - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - try: - s.connect((address, port)) - s.shutdown(socket.SHUT_RDWR) - s.close() - return True - except: - return False - -class VRtistJoinRoomOperator(bpy.types.Operator): - bl_idname = "scene.vrtistjoinroom" - bl_label = "VRtist Join Room" +class JoinOrLeaveRoomOperator(bpy.types.Operator): + """Join a room, or leave the one that was already joined""" + bl_idname = "dcc_sync.join_or_leave_room" + bl_label = "DCCSync Join or Leave Room" bl_options = {'REGISTER'} def execute(self, context): @@ -782,23 +753,23 @@ def execute(self, context): shareData.isLocal = False try: - roomIndex = bpy.data.scenes[0].vrtistconnect.room_index - room = bpy.data.scenes[0].vrtistconnect.rooms[roomIndex].name + roomIndex = get_dcc_sync_props().room_index + room = get_dcc_sync_props().rooms[roomIndex].name except IndexError: room = "Local" localServerIsUp = True if room == 'Local': - host = HOST - port = PORT + host = common.DEFAULT_HOST + port = common.DEFAULT_PORT localServerIsUp = server_is_up(host, port) # Launch local server? if it doesn't exist if not localServerIsUp: start_local_server() shareData.isLocal = True else: - host = bpy.data.scenes[0].vrtistconnect.host - port = bpy.data.scenes[0].vrtistconnect.port + host = get_dcc_sync_props().host + port = get_dcc_sync_props().port shareData.client = clientBlender.ClientBlender(host, port) shareData.client.addCallback('SendContent',send_scene_content) @@ -816,9 +787,22 @@ def execute(self, context): return {'FINISHED'} -class VRtistSendSelectionOperator(bpy.types.Operator): - bl_idname = "scene.vrtistsendselection" - bl_label = "VRtist Send Selection" +class UpdateRoomListOperator(bpy.types.Operator): + """Fetch and update the list of DCC Sync rooms""" + bl_idname = "dcc_sync.update_room_list" + bl_label = "DCCSync Update Room List" + bl_options = {'REGISTER'} + + def execute(self, context): + logger.info("UpdateRoomListOperator") + getRooms(force=True) + updateListRoomsProperty() + return {'FINISHED'} + +class SendSelectionOperator(bpy.types.Operator): + """Send current selection to DCC Sync server""" + bl_idname = "dcc_sync.send_selection" + bl_label = "DCCSync Send selection" bl_options = {'REGISTER'} def execute(self, context): @@ -839,44 +823,32 @@ def execute(self, context): return {'FINISHED'} -class VRtistOperator(bpy.types.Operator): - bl_idname = "scene.vrtist" - bl_label = "VRtist" +class LaunchVRtistOperator(bpy.types.Operator): + """Launch a VRtist instance""" + bl_idname = "vrtist.launch" + bl_label = "Launch VRtist" bl_options = {'REGISTER'} def execute(self, context): - vrtistconnect = bpy.data.scenes[0].vrtistconnect + dcc_sync_props = get_dcc_sync_props() room = shareData.currentRoom if not room: - bpy.ops.scene.vrtistjoinroom() + bpy.ops.dcc_sync.joinroom() room = shareData.currentRoom hostname = "localhost" if not shareData.isLocal: hostname = vrtistconnect.host - args = [bpy.data.scenes[0].vrtistconnect.VRtist, "--room",room, "--hostname", hostname, "--port", str(vrtistconnect.port)] + args = [dcc_sync_props.VRtist, "--room",room, "--hostname", hostname, "--port", str(vrtistconnect.port)] subprocess.Popen(args, stdout=subprocess.PIPE,stderr=subprocess.STDOUT, shell=False) return {'FINISHED'} - -class VRtistSayHello(bpy.types.Operator): - bl_idname = "vrtist.say_hello" - bl_label = "VRtist Say Hello" - bl_options = {'REGISTER'} - - def execute(self, context): - logger.info("I say hello") - return {'FINISHED'} - classes = ( - VRtistOperator, - VRtistRoomItem, - VRtistConnectProperties, - VRtistCreateRoomOperator, - VRtistRoomListUpdateOperator, - VRtistSendSelectionOperator, - VRtistJoinRoomOperator, - VRtistSayHello, + LaunchVRtistOperator, + CreateRoomOperator, + UpdateRoomListOperator, + SendSelectionOperator, + JoinOrLeaveRoomOperator, ) def register(): diff --git a/ui.py b/ui.py index 37b5ff3f..ed253053 100644 --- a/ui.py +++ b/ui.py @@ -1,6 +1,7 @@ import os import bpy from . import operators +from .data import get_dcc_sync_props class ROOM_UL_ItemRenderer(bpy.types.UIList): @@ -9,61 +10,65 @@ def draw_item(self, context, layout, data, item, icon, active_data, active_propn split.label(text=item.name) # avoids renaming the item by accident -class VRtistPanel(bpy.types.Panel): +class SettingsPanel(bpy.types.Panel): """Creates a Panel in the Object properties window""" - bl_label = "VRtist" - bl_idname = "VRTIST_PT_settings" + bl_label = "DCC Sync" + bl_idname = "DCCSYNC_PT_settings" bl_space_type = 'VIEW_3D' bl_region_type = 'UI' - bl_category = "VRtist" + bl_category = "DCC Sync" def draw(self, context): layout = self.layout - scene = context.scene + dcc_sync_props = get_dcc_sync_props() row = layout.row() row.label(text="VRtist", icon='SCENE_DATA') row = layout.column() - row.operator("scene.vrtist", text="Launch VRTist") - row.operator(operators.VRtistSayHello.bl_idname, text="Say Hello") + row.operator(operators.LaunchVRtistOperator.bl_idname, text="Launch VRTist") + + row = layout.row() + row.label(text="DCC Sync", icon='SCENE_DATA') + + row = layout.column() connected = operators.shareData.client is not None and operators.shareData.client.isConnected() if not connected: # Room list row = layout.row() - row.template_list("ROOM_UL_ItemRenderer", "", scene.vrtistconnect, - "rooms", scene.vrtistconnect, "room_index", rows=4) + row.template_list("ROOM_UL_ItemRenderer", "", dcc_sync_props, + "rooms", dcc_sync_props, "room_index", rows=4) # Join room col = row.column() - col.operator("scene.vrtistroomlistupdate", text="Refresh") - col.operator("scene.vrtistjoinroom", text="Join Room") + col.operator(operators.UpdateRoomListOperator.bl_idname, text="Refresh") + col.operator(operators.JoinOrLeaveRoomOperator.bl_idname, text="Join Room") - if scene.vrtistconnect.remoteServerIsUp: + if dcc_sync_props.remoteServerIsUp: row = layout.row() - row.prop(scene.vrtistconnect, "room", text="Room") - row.operator('scene.vrtistcreateroom', text='Create Room') + row.prop(dcc_sync_props, "room", text="Room") + row.operator(operators.CreateRoomOperator.bl_idname, text='Create Room') col = layout.column() row = col.row() - row.prop(scene.vrtistconnect, "advanced", - icon="TRIA_DOWN" if scene.vrtistconnect.advanced else "TRIA_RIGHT", + row.prop(dcc_sync_props, "advanced", + icon="TRIA_DOWN" if dcc_sync_props.advanced else "TRIA_RIGHT", icon_only=True, emboss=False) row.label(text="Advanced options") - if scene.vrtistconnect.advanced: - col.prop(scene.vrtistconnect, "host", text="Host") - col.prop(scene.vrtistconnect, "port", text="Port") - col.prop(scene.vrtistconnect, "VRtist", text="VRtist Path") + if dcc_sync_props.advanced: + col.prop(dcc_sync_props, "host", text="Host") + col.prop(dcc_sync_props, "port", text="Port") + col.prop(dcc_sync_props, "VRtist", text="VRtist Path") else: - row.operator("scene.vrtistjoinroom", text="Leave Room") + row.operator(operators.JoinOrLeaveRoomOperator.bl_idname, text="Leave Room") classes = ( ROOM_UL_ItemRenderer, - VRtistPanel + SettingsPanel ) From 48146a4cfec887cf2f0a58113857193b5d8e2cfb Mon Sep 17 00:00:00 2001 From: Laurent NOEL Date: Thu, 13 Feb 2020 15:50:32 +0100 Subject: [PATCH 09/11] Rename logger --- __init__.py | 2 +- operators.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/__init__.py b/__init__.py index 2ba9319e..2c75a2b8 100644 --- a/__init__.py +++ b/__init__.py @@ -30,7 +30,7 @@ def cleanup(): def register(): - logger = logging.getLogger("DCCSync") + logger = logging.getLogger(__package__) if len(logger.handlers) == 0: logger.setLevel(logging.INFO) handler = logging.StreamHandler() diff --git a/operators.py b/operators.py index 6c17211b..751d3f18 100644 --- a/operators.py +++ b/operators.py @@ -15,7 +15,7 @@ from .data import get_dcc_sync_props -logger = logging.getLogger("DCC Sync") +logger = logging.getLogger(__package__) class TransformStruct: def __init__(self, translate, quaternion, scale, visible): From 6ae2b2ccd3ef5ebb439e957d55d82c2fd8fa39b4 Mon Sep 17 00:00:00 2001 From: Laurent NOEL Date: Thu, 13 Feb 2020 18:44:25 +0100 Subject: [PATCH 10/11] Fix type of objects collection --- operators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/operators.py b/operators.py index 8992bcc0..2174ee5e 100644 --- a/operators.py +++ b/operators.py @@ -51,7 +51,7 @@ def __init__(self): self.objectsTransforms = {} self.objectsVisibilityChanged = set() self.objectsVisibility = {} - self.objects = [] + self.objects = set() def clearLists(self): self.objectsAddedToCollection.clear() From 2f04c67b4d5a7492325b78b0fad8f7767b820285 Mon Sep 17 00:00:00 2001 From: Laurent NOEL Date: Thu, 13 Feb 2020 19:35:18 +0100 Subject: [PATCH 11/11] Server side improvements - Handle CTRL+C to kill server - WIP: Handle clientname - Send clients information as json list of objects (+fix getClients) - Handle server errors in cli.py --- broadcaster/cli.py | 108 ++++++++++++++++++++-------------- broadcaster/common.py | 38 ++++++++---- broadcaster/dccBroadcaster.py | 56 ++++++++++++++---- 3 files changed, 131 insertions(+), 71 deletions(-) diff --git a/broadcaster/cli.py b/broadcaster/cli.py index 9c784918..b8d93ad5 100644 --- a/broadcaster/cli.py +++ b/broadcaster/cli.py @@ -1,14 +1,23 @@ import time import queue import argparse +import logging import common import client +logging.basicConfig( + format='%(asctime)s - %(levelname)s - %(message)s', level=logging.INFO) + TIMEOUT = 10 # in seconds +class ServerError(RuntimeError): + def __init__(self, message): + super().__init__(message) + + class CliClient(client.Client): def __init__(self, args): super().__init__(args.host, args.port) @@ -36,24 +45,25 @@ def clearRoom(self, name): def listRoomClients(self, name): def _printResult(command: common.Command): - clients, _ = common.decodeStringArray(command.data, 0) + clients, _ = common.decodeJson(command.data, 0) if len(clients) == 0: print(f'No clients in "{name}" room') else: - print(f'{len(clients)} client(s) in "{name}" room:\n - ', end='') - print('\n - '.join(clients)) - + print(f'{len(clients)} client(s) in "{name}" room :') + for c in clients: + print(f' - {c["ip"]}:{c["port"]} name = {c["name"]}') command = common.Command(common.MessageType.LIST_ROOM_CLIENTS, name.encode()) self.processCommand(command, _printResult) def listClients(self): def _printResult(command: common.Command): - clients, _ = common.decodeStringArray(command.data, 0) + clients, _ = common.decodeJson(command.data, 0) if len(clients) == 0: print('No clients') else: - print(f'{len(clients)} client(s):\n - ', end='') - print('\n - '.join(clients)) + print(f'{len(clients)} client(s):') + for c in clients: + print(f' - {c["ip"]}:{c["port"]} name = {c["name"]} room = {c["room"]}') command = common.Command(common.MessageType.LIST_CLIENTS) self.processCommand(command, _printResult) @@ -67,6 +77,8 @@ def processCommand(self, command: common.Command, callback=None): if callback is not None: command = self.consume() if command: + if command.type == common.MessageType.SEND_ERROR: + raise ServerError(common.decodeString(command.data, 0)[0]) callback(command) def consume(self): @@ -82,50 +94,56 @@ def consume(self): def process_room_command(args): client = None - if args.command == 'list': - client = CliClient(args) - client.listRooms() - - elif args.command == 'delete': - count = len(args.name) - if count: - client = CliClient(args) - for name in args.name: - client.deleteRoom(name) - else: - print('Expected one or more room names') - - elif args.command == 'clear': - count = len(args.name) - if count: - client = CliClient(args) - for name in args.name: - client.clearRoom(name) - else: - print('Expected one or more room names') - - elif args.command == 'clients': - count = len(args.name) - if count: + try: + if args.command == 'list': client = CliClient(args) - for name in args.name: - client.listRoomClients(name) - else: - print('Expected one or more room names') - - if client: - client.disconnect() + client.listRooms() + + elif args.command == 'delete': + count = len(args.name) + if count: + client = CliClient(args) + for name in args.name: + client.deleteRoom(name) + else: + print('Expected one or more room names') + + elif args.command == 'clear': + count = len(args.name) + if count: + client = CliClient(args) + for name in args.name: + client.clearRoom(name) + else: + print('Expected one or more room names') + + elif args.command == 'clients': + count = len(args.name) + if count: + client = CliClient(args) + for name in args.name: + client.listRoomClients(name) + else: + print('Expected one or more room names') + except ServerError as e: + logging.error(e) + finally: + if client: + client.disconnect() def process_client_command(args): client = None - if args.command == 'list': - client = CliClient(args) - client.listClients() - - if client is not None: - client.disconnect() + try: + if args.command == 'list': + client = CliClient(args) + client.listClients() + except ServerError as e: + logging.error(e) + finally: + if client is not None: + client.disconnect() parser = argparse.ArgumentParser(prog='cli', description='Command Line Interface for VRtist server') diff --git a/broadcaster/common.py b/broadcaster/common.py index f2951d8b..7f6e716b 100644 --- a/broadcaster/common.py +++ b/broadcaster/common.py @@ -3,6 +3,7 @@ import select import socket import struct +import json mutex = threading.RLock() @@ -22,6 +23,8 @@ class MessageType(Enum): CLEAR_ROOM = 8 LIST_ROOM_CLIENTS = 9 LIST_CLIENTS = 10 + SET_CLIENT_NAME = 11 + SEND_ERROR = 12 COMMAND = 100 DELETE = 101 CAMERA = 102 @@ -110,6 +113,15 @@ def decodeString(data, index): return value, end +def encodeJson(value: dict): + return encodeString(json.dumps(value)) + + +def decodeJson(data, index): + value, end = decodeString(data, index) + return json.loads(value), end + + def encodeFloat(value): return struct.pack('f', value) @@ -250,7 +262,19 @@ def readMessage(socket): return None -def writeMessage(socket, command): +class Command: + _id = 100 + + def __init__(self, commandType: MessageType, data=b'', commandId=0): + self.data = data or b'' + self.type = commandType + self.id = commandId + if commandId == 0: + self.id = Command._id + Command._id += 1 + + +def writeMessage(socket, command: Command): if not socket: return size = intToBytes(len(command.data), 8) @@ -266,15 +290,3 @@ def writeMessage(socket, command): sent = socket.send(buffer[currentIndex:]) remainingSize -= sent currentIndex += sent - - -class Command: - _id = 100 - - def __init__(self, commandType, data=b'', commandId=0): - self.data = data or b'' - self.type = commandType - self.id = commandId - if commandId == 0: - self.id = Command._id - Command._id += 1 diff --git a/broadcaster/dccBroadcaster.py b/broadcaster/dccBroadcaster.py index 9a1b89c9..3e50be5a 100644 --- a/broadcaster/dccBroadcaster.py +++ b/broadcaster/dccBroadcaster.py @@ -3,6 +3,7 @@ import select import threading import logging +import time import os import sys @@ -12,13 +13,18 @@ TIMEOUT = 60.0 BINDING_HOST = '' +SHUTDOWN = False + class Connection: + """ Represent a connection with a client """ + def __init__(self, socket, address): self.socket = socket self.address = address + self.clientname = None self.room = None - self.commands = [] + self.commands = [] # Pending commands to send to the client self.start() def start(self): @@ -57,21 +63,25 @@ def listRoomClients(self, name): with common.mutex: room = rooms.get(name) if room is not None: - command = common.Command(common.MessageType.LIST_ROOM_CLIENTS, common.encodeStringArray( - [f'{client.address[0]}:{client.address[1]}' for client in room.clients])) - self.commands.append(command) + command = common.Command(common.MessageType.LIST_ROOM_CLIENTS, common.encodeJson(room.getClients())) + else: + command = common.Command(common.MessageType.SEND_ERROR, common.encodeString(f'No room named {name}.')) + self.commands.append(command) def listClients(self): - clients = set() with common.mutex: + clients = [] for room in rooms.values(): - for client in room.clients: - clients.add(client) - command = common.Command(common.MessageType.LIST_CLIENTS, common.encodeStringArray(clients)) + clients += room.getClients() + command = common.Command(common.MessageType.LIST_CLIENTS, common.encodeJson(clients)) self.commands.append(command) + def setClientName(self, name): + self.clientname = name + def run(self): - while(True): + global SHUTDOWN + while not SHUTDOWN: try: command = common.readMessage(self.socket) except common.ClientDisconnectedException: @@ -98,6 +108,9 @@ def run(self): elif command.type == common.MessageType.LIST_CLIENTS: self.listClients() + elif command.type == common.MessageType.SET_CLIENT_NAME: + self.setClientName(command.data.decode()) + # Other commands elif command.type.value > common.MessageType.COMMAND.value: if self.room is not None: @@ -145,6 +158,10 @@ def addClient(self, client): for command in self.commands: client.addCommand(command) + def getClients(self): + return [dict(ip=client.address[0], port=client.address[1], + name=client.clientname, room=self.name) for client in self.clients] + def close(self): command = common.Command(common.MessageType.LEAVE_ROOM, common.encodeString(self.name)) self.addCommand(command, None) @@ -186,16 +203,29 @@ def addCommand(self, command, sender): def runServer(): + global SHUTDOWN + connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM) connection.bind((BINDING_HOST, common.DEFAULT_PORT)) + connection.setblocking(0) connection.listen(1000) - logging.info(f"Listening on port {PORT}") + logging.info(f"Listening on port {common.DEFAULT_PORT}") while True: - newConnection = connection.accept() - logging.info(f"New connection {newConnection[1]}") - Connection(*newConnection) + try: + newConnection = connection.accept() + logging.info(f"New connection {newConnection[1]}") + Connection(*newConnection) + except KeyboardInterrupt: + break + except BlockingIOError: + try: + time.sleep(60.0 / 1000.0) + except KeyboardInterrupt: + break + logging.info("Shutting down server") + SHUTDOWN = True connection.close()