Skip to content

Commit

Permalink
EDITOR-#518 [BLENDER] Command center (#537)
Browse files Browse the repository at this point in the history
* init command panel

* complete command center core

* added controller server websocket path in env

* Change controller server env to local, fixed command response bug

* fixed bug

* fixed bug
  • Loading branch information
Chalkman071 authored Feb 19, 2024
1 parent 8ff3a6d commit 3f4298e
Show file tree
Hide file tree
Showing 22 changed files with 1,331 additions and 8 deletions.
1 change: 1 addition & 0 deletions editor-blender/.env.development
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ HTTP_PATH="/api"
GRAPHQL_PATH="/graphql"
GRAPHQL_WS_PATH="/graphql-websocket"
FILE_SERVER_URL="http://localhost:8081"
CONTROLLER_WS_URL="ws://localhost:8082"
21 changes: 21 additions & 0 deletions editor-blender/api/command_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import json
from dataclasses import dataclass

from ..client import client
from ..graphqls.command import ToControllerServerPartial


@dataclass
class CommandAgent:
async def send_to_controller_server(self, msg_partial: ToControllerServerPartial):
msg_full = json.dumps(
{
**(msg_partial.to_dict()),
"from": "controlPanel",
"statusCode": 0,
}
)
await client.send_command(msg_full)


command_agent = CommandAgent()
51 changes: 51 additions & 0 deletions editor-blender/client/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import asyncio
import json
from asyncio import Task
from inspect import isclass
from typing import Any, AsyncGenerator, Dict, Optional, Type, TypeVar, Union
Expand All @@ -10,9 +11,15 @@
from gql.transport.aiohttp import AIOHTTPTransport
from gql.transport.websockets import WebsocketsTransport
from graphql import DocumentNode
from websockets.client import WebSocketClientProtocol, connect

from ..core.config import config
from ..core.states import state
from ..graphqls.command import (
FromControllerServer,
FromControllerServerBoardInfo,
FromControllerServerCommandResponse,
)
from .cache import InMemoryCache, query_defs_to_field_table

GQLSession = Union[AsyncClientSession, ReconnectingAsyncClientSession]
Expand Down Expand Up @@ -79,6 +86,7 @@ def __init__(self):
self.client: Optional[GQLSession] = None
self.sub_client: Optional[GQLSession] = None
self.file_client: Optional[ClientSession] = None
self.command_client: Optional[WebSocketClientProtocol] = None

self.cache = InMemoryCache()

Expand Down Expand Up @@ -220,6 +228,32 @@ async def execute(

return response

async def subscribe_command(self) -> AsyncGenerator[FromControllerServer, None]:
if self.command_client is None:
raise Exception("Command client is not initialized")
async for data in self.command_client:
if isinstance(data, str):
data_dict = json.loads(data)
match data_dict["topic"]:
case "boardInfo":
sub_data: FromControllerServer = deserialize(
FromControllerServerBoardInfo, data_dict
)
case "command":
sub_data: FromControllerServer = deserialize(
FromControllerServerCommandResponse, data_dict
)
case _:
raise Exception("Invalid command data recieved")
yield sub_data
else:
raise Exception("Invalid command data recieved")

async def send_command(self, data: str):
if self.command_client is None:
raise Exception("Command client is not initialized")
await self.command_client.send(data)

async def open_http(self) -> None:
await self.close_http()

Expand Down Expand Up @@ -290,5 +324,22 @@ async def restart_graphql(self) -> None:
await self.close_graphql()
await self.open_graphql()

async def open_command(self):
self.command_client = await connect(
uri=config.CONTROLLER_WS_URL,
extra_headers=[("token", state.token)],
)
print("Command client opened")

async def close_command(self):
if self.command_client is not None:
await self.command_client.close()
else:
return

async def restart_command(self):
await self.close_command()
await self.open_command()


client = Clients()
40 changes: 40 additions & 0 deletions editor-blender/client/subscription.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@

from ..client import Clients, client
from ..client.cache import Modifiers
from ..core.actions.property.command import set_command_status
from ..core.actions.state.app_state import set_requesting
from ..core.actions.state.color_map import add_color, delete_color, update_color
from ..core.actions.state.command import read_board_info_payload, read_command_response
from ..core.actions.state.control_map import (
add_control,
delete_control,
Expand All @@ -26,6 +29,7 @@
pos_frame_query_to_state,
pos_frame_sub_to_query,
)
from ..core.utils.notification import notify
from ..graphqls.queries import (
QueryColorMapData,
QueryColorMapPayloadItem,
Expand Down Expand Up @@ -375,3 +379,39 @@ async def subscribe():

print("Reconnecting subscription...")
await asyncio.sleep(3)


async def sub_controller_server(client: Clients):
async for controller_data in client.subscribe_command():
match controller_data.topic:
case "boardInfo":
notify("INFO", "Board info updated")
print("Board info from controller server")
read_board_info_payload(controller_data.payload)
case "command":
notify("INFO", "Command response received")
print(f"Command response from controller server: {controller_data}")
read_command_response(controller_data)


async def subscribe_command():
while True:
try:
print("Subscribing controller server...")

tasks = [
asyncio.create_task(sub_controller_server(client)),
]

await asyncio.gather(*tasks)

except asyncio.CancelledError:
print("Subscription cancelled.")
break

except Exception as e:
print("Subscription closed with error:", e)

set_command_status(False)
print("Reconnecting subscription...")
await asyncio.sleep(3)
92 changes: 92 additions & 0 deletions editor-blender/core/actions/property/command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import asyncio
from typing import Any, List, Optional

import bpy

from ....properties.ui.types import CommandCenterRPiStatusType, CommandCenterStatusType
from ...asyncio import AsyncTask
from ...states import state


class Countdown_task_class:
task: Optional[asyncio.Task[Any]] = None


countdown_task = Countdown_task_class()


def set_command_status(connected: bool):
command_status: CommandCenterStatusType = getattr(
bpy.context.window_manager, "ld_ui_command_center"
)
command_status.connected = connected


def set_RPi_props_from_state():
rpi_props: List[CommandCenterRPiStatusType] = getattr(
bpy.context.window_manager, "ld_ui_rpi_status"
)

rpi_status = state.rpi_status
if not rpi_props:
for dancer_name, dancer_item in rpi_status.items():
rpi_item: CommandCenterRPiStatusType = getattr(
bpy.context.window_manager, "ld_ui_rpi_status"
).add()
rpi_item.name = dancer_name
if dancer_item.ethernet.connected == dancer_item.wifi.connected:
rpi_item.interface_type = "ethernet"
interface_status = dancer_item.ethernet
elif dancer_item.ethernet.connected:
rpi_item.interface_type = "ethernet"
interface_status = dancer_item.ethernet
else:
rpi_item.interface_type = "wifi"
interface_status = dancer_item.wifi

rpi_item.IP = interface_status.IP
rpi_item.MAC = interface_status.MAC
rpi_item.connected = interface_status.connected
rpi_item.message = interface_status.message
rpi_item.statusCode = interface_status.statusCode
rpi_item.selected = False
else:
for dancer_name, dancer_item in rpi_status.items():
rpi_item: CommandCenterRPiStatusType = next(
item for item in rpi_props if item.name == dancer_name
)
if dancer_item.ethernet.connected == dancer_item.wifi.connected:
interface_status = dancer_item.ethernet
elif dancer_item.ethernet.connected:
interface_status = dancer_item.ethernet
else:
interface_status = dancer_item.wifi
rpi_item.connected = interface_status.connected
rpi_item.message = interface_status.message
rpi_item.statusCode = interface_status.statusCode


def get_selected_dancer() -> List[str]:
rpi_status_list: List[CommandCenterRPiStatusType] = getattr(
bpy.context.window_manager, "ld_ui_rpi_status"
)
return [item.name for item in rpi_status_list if item.selected]


def set_countdown(delay: int):
async def countdown(delay: int):
command_status: CommandCenterStatusType = getattr(
bpy.context.window_manager, "ld_ui_command_center"
)
for t in range(delay + 1):
seconds = delay - t
m, s = divmod(seconds, 60)
countdown = f"{m:02d}:{s:02d}"
command_status.countdown = countdown
print(countdown)
if seconds > 0:
await asyncio.sleep(1)
else:
bpy.ops.screen.animation_play()

countdown_task.task = AsyncTask(countdown, delay=delay).exec()
5 changes: 5 additions & 0 deletions editor-blender/core/actions/state/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,13 @@ async def logout() -> None:
state.init_editor_task.cancel()
state.init_editor_task = None

if state.command_task is not None:
state.command_task.cancel()
state.command_task = None

await client.close_graphql()
await client.restart_http()
await client.close_command()

unmount_handlers()

Expand Down
80 changes: 80 additions & 0 deletions editor-blender/core/actions/state/command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from ....graphqls.command import (
FromControllerServerBoardInfoPayload,
FromControllerServerCommandResponse,
)
from ...models import InterfaceStatus, RPiStatusItem, ShellTransaction
from ...states import state
from ..property.command import set_RPi_props_from_state


def read_board_info_payload(payload: FromControllerServerBoardInfoPayload):
for item in payload.values():
rpi_status = state.rpi_status
if item.dancer not in rpi_status:
rpi_status[item.dancer] = RPiStatusItem(
ethernet=InterfaceStatus(
name=item.dancer,
IP=item.IP,
MAC=item.MAC,
connected=False,
statusCode=0,
message="",
),
wifi=InterfaceStatus(
name=item.dancer,
IP=item.IP,
MAC=item.MAC,
connected=False,
statusCode=0,
message="",
),
)
match item.interface:
case "ethernet":
rpi_status[item.dancer].ethernet = InterfaceStatus(
name=item.dancer,
IP=item.IP,
MAC=item.MAC,
connected=item.connected,
statusCode=0,
message="",
)
case "wifi":
rpi_status[item.dancer].wifi = InterfaceStatus(
name=item.dancer,
IP=item.IP,
MAC=item.MAC,
connected=item.connected,
statusCode=0,
message="",
)
set_RPi_props_from_state()


def read_command_response(data: FromControllerServerCommandResponse):
payload = data.payload
dancer = payload.dancer
command = payload.command
message = payload.message
rpi_status = state.rpi_status
if dancer not in state.rpi_status:
rpi_status[dancer] = RPiStatusItem(
ethernet=InterfaceStatus(
name=dancer, IP="", MAC="", connected=False, statusCode=0, message=""
),
wifi=InterfaceStatus(
name=dancer, IP="", MAC="", connected=False, statusCode=0, message=""
),
)
rpi_status_item = rpi_status[dancer]
if rpi_status_item.ethernet.connected:
rpi_status_item.ethernet.message = f"[{command}] {message}"
rpi_status_item.ethernet.statusCode = data.statusCode
if rpi_status_item.wifi.connected:
rpi_status_item.wifi.message = f"[{command}] {message}"
rpi_status_item.wifi.statusCode = data.statusCode
shell_history = state.shell_history
if dancer not in shell_history:
shell_history[dancer] = []
shell_history[dancer].append(ShellTransaction(command=command, output=message))
set_RPi_props_from_state()
2 changes: 1 addition & 1 deletion editor-blender/core/actions/state/initialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from ....client import client

# from ....client.cache import FieldPolicy, InMemoryCache, TypePolicy
from ....client.subscription import subscribe
from ....client.subscription import subscribe, subscribe_command
from ....core.actions.state.app_state import (
set_logged_in,
set_ready,
Expand Down
5 changes: 5 additions & 0 deletions editor-blender/core/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ def initialize(self):
raise Exception("FILE_SERVER_URL is not defined")
self.FILE_SERVER_URL = remove_wrapped_slash(FILE_SERVER_URL)

CONTROLLER_WS_URL = os.getenv("CONTROLLER_WS_URL")
if CONTROLLER_WS_URL is None:
raise Exception("CONTROLLER_WS_URL is not defined")
self.CONTROLLER_WS_URL = remove_wrapped_slash(CONTROLLER_WS_URL)

"""
Assets
"""
Expand Down
Loading

0 comments on commit 3f4298e

Please sign in to comment.