Compare commits

..

No commits in common. "a6b2992cc29b5bd5cd94c6885a28d56dd9f393ca" and "e55ae9f033928d31acd7b5f4c6b40624e4e27116" have entirely different histories.

2 changed files with 70 additions and 270 deletions

View file

@ -10,31 +10,23 @@ from argparse import ArgumentParser
from websockets.client import connect, WebSocketClientProtocol
from pydantic import BaseModel
URL = "ws://localhost:8124"
URL_PARAMS = {
"protocol-version": "1.0.0",
"manufacturer": "TeamsCtrl",
"device": "TeamsCtrl",
"app": "TeamsCtrlApp",
"app-version": "1.4",
}
class Button(BaseModel):
num: int
color_on: tuple[int, int, int]
color_off: tuple[int, int, int]
BUTTONS = {"cam": 1, "mic": 2}
class BUTTONS:
cam = Button(num=0, color_off=(0, 0, 10), color_on=(0, 0, 255))
mic = Button(num=1, color_off=(10, 0, 0), color_on=(255, 0, 0))
hand = Button(num=2, color_off=(10, 10, 0), color_on=(255, 192, 0))
call = Button(num=3, color_off=(0, 10, 0), color_on=(255, 64, 0))
all = [cam, mic, hand, call]
# class consts:
# BUTTONS = {"cam": 0, "mic": 1, "call": 3, "hand": 2}
# COLORS = {
# "mic":,
# "hand":,
# "call":,
# }
COLORS = (
((0, 10, 0), (0, 255, 0)),
((10, 0, 0), (255, 0, 0)),
((0, 0, 10), (0, 0, 255)),
((10, 10, 10), (255, 255, 255)),
)
class MeetingPermissions(BaseModel):
@ -75,109 +67,61 @@ class State:
def __init__(self, ctrl: "TeamsCtrl") -> None:
self.ctrl = ctrl
async def process(self) -> str | None:
await asyncio.sleep(0.1)
def process(self) -> str | None:
time.sleep(0.1)
return None
async def handle_key(self, key_num: int, state: bool) -> str | None:
def handle_key(self, key_num: int, state: bool) -> str | None:
print(f"key:{key_num}, state:{state}")
if state is True:
msg = {
"apiVersion": "1.0.0",
"manufacturer": "Elgato",
"device": "StreamDeck",
"timestamp": time.time_ns() // 1000,
}
match key_num:
case BUTTONS.mic.num:
msg.update(
{
"service": "toggle-mute",
"action": "toggle-mute",
}
)
case BUTTONS.cam.num:
msg.update(
{
"service": "toggle-video",
"action": "toggle-video",
}
)
case BUTTONS.call.num:
msg.update(
{
"service": "call",
"action": "leave-call",
}
)
case BUTTONS.hand.num:
msg.update(
{
"service": "raise-hand",
"action": "toggle-hand",
}
)
case _:
return None
await self.ctrl.send_ws_cmd(msg)
if key_num == BUTTONS["mic"]:
self.ctrl.send_ws_cmd(
{
"apiVersion": "1.0.0",
"service": "toggle-mute",
"action": "toggle-mute",
"manufacturer": "Elgato",
"device": "StreamDeck",
"timestamp": 1675341775725,
}
)
if key_num == BUTTONS["cam"]:
self.ctrl.send_ws_cmd(
{
"apiVersion": "1.0.0",
"service": "toggle-video",
"action": "toggle-video",
"manufacturer": "Elgato",
"device": "StreamDeck",
"timestamp": 1675341791249,
}
)
return None
def handle_state_change(self, changes: dict[str, bool]) -> str | None:
if "isInMeeting" in changes:
if changes["isInMeeting"]:
for btn in BUTTONS.all:
self.ctrl.set_color(
key_num=btn.num,
color=btn.color_off,
)
for key_num in range(4):
self.ctrl.set_color(key_num=key_num, color=COLORS[key_num][1])
else:
for btn in BUTTONS.all:
self.ctrl.set_color(key_num=btn.num, color=(0, 0, 0))
for key_num in range(4):
self.ctrl.set_color(key_num=key_num, color=(0, 0, 0))
if "isMuted" in changes:
key_num = BUTTONS.mic.num
key_num = BUTTONS["mic"]
if changes["isMuted"]:
self.ctrl.set_color(key_num=key_num, color=BUTTONS.mic.color_on)
self.ctrl.set_color(key_num=key_num, color=COLORS[key_num][1])
else:
self.ctrl.set_color(key_num=key_num, color=BUTTONS.mic.color_off)
self.ctrl.set_color(key_num=key_num, color=(0, 0, 0))
if "isCameraOn" in changes:
key_num = BUTTONS.cam.num
key_num = BUTTONS["cam"]
if changes["isCameraOn"]:
self.ctrl.set_color(key_num=key_num, color=BUTTONS.cam.color_on)
self.ctrl.set_color(key_num=key_num, color=COLORS[key_num][1])
else:
self.ctrl.set_color(key_num=key_num, color=BUTTONS.cam.color_off)
if "isHandRaised" in changes:
key_num = BUTTONS.hand.num
if changes["isHandRaised"]:
self.ctrl.set_color(key_num=key_num, color=BUTTONS.hand.color_on)
else:
self.ctrl.set_color(key_num=key_num, color=BUTTONS.hand.color_off)
if "hasUnreadMessages" in changes:
if changes["hasUnreadMessages"]:
return "notify"
self.ctrl.set_color(key_num=key_num, color=(0, 0, 0))
return None
class Notify(State):
async def process(self) -> str | None:
self.ctrl.set_color(key_num=BUTTONS.call.num, color=BUTTONS.call.color_on)
await asyncio.sleep(0.1)
self.ctrl.set_color(key_num=BUTTONS.call.num, color=BUTTONS.call.color_off)
await asyncio.sleep(0.2)
self.ctrl.set_color(key_num=BUTTONS.call.num, color=BUTTONS.call.color_on)
await asyncio.sleep(0.1)
self.ctrl.set_color(key_num=BUTTONS.call.num, color=BUTTONS.call.color_off)
await asyncio.sleep(0.6)
return None
def handle_state_change(self, changes: dict[str, bool]) -> str | None:
super_state = super().handle_state_change(changes)
if "hasUnreadMessages" in changes:
if changes["hasUnreadMessages"] is False and "isInMeeting" not in changes:
self.ctrl.set_color(
key_num=BUTTONS.call.num, color=BUTTONS.call.color_off
)
return "idle"
return super_state
class InMeeting(State):
pass
class TeamsCtrl:
@ -193,7 +137,7 @@ class TeamsCtrl:
self.serial_reader, self.serial_writer = serial
self.ws = ws
self.state_machine = {"idle": State(self), "notify": Notify(self)}
self.state_machine = {"idle": State(self), "inmeeting": InMeeting(self)}
self.current_state_name = "idle"
self.meeting_state: MeetingState | None = None
@ -212,8 +156,8 @@ class TeamsCtrl:
print(cmd)
self.serial_writer.write(cmd.encode("utf-8"))
async def process_serial(self) -> None:
while not self.loop.is_closed():
async def process_serial(self):
while True:
raw_msg = await self.serial_reader.readline()
msg = raw_msg.decode("utf-8").strip()
action = msg[0]
@ -228,15 +172,13 @@ class TeamsCtrl:
key_state = False
case _:
raise ValueError(f"unknown action: {action}")
next_state = await self.current_state.handle_key(
key_num=key_num, state=key_state
)
next_state = self.current_state.handle_key(key_num=key_num, state=key_state)
if next_state is not None:
self.set_next_state(next_state=next_state)
async def send_ws_cmd(self, cmd: dict[str, Any]) -> None:
def send_ws_cmd(self, cmd: dict[str, Any]) -> None:
msg = json.dumps(cmd)
await self.ws.send(msg)
self.loop.run_until_complete(self.ws.send(msg))
async def process_ws(self) -> None:
async for raw_msg in self.ws:
@ -260,33 +202,6 @@ class TeamsCtrl:
if next_state is not None:
self.set_next_state(next_state=next_state)
async def process_statemachine(self) -> None:
while not self.loop.is_closed():
await self.current_state.process()
def gather_all(self):
return asyncio.gather(
self.process_serial(), self.process_ws(), self.process_statemachine()
)
async def amain():
loop = asyncio.get_event_loop()
serial = await serial_asyncio.open_serial_connection(
loop=loop, url=args.port, baudrate=115200
)
url = f"ws://localhost:8124?token={token}&protocol-version=1.0.0&manufacturer=TeamsCtrl&device=TeamsCtrl&app=TeamsCtrlApp&app-version=1.4"
ws = await connect(url)
ctrl = TeamsCtrl(loop=loop, serial=serial, ws=ws)
try:
tasks = ctrl.gather_all()
await tasks
finally:
for btn in BUTTONS.all:
ctrl.set_color(key_num=btn.num, color=(0, 0, 0))
if __name__ == "__main__":
parser = ArgumentParser(description=__doc__)
@ -299,7 +214,16 @@ if __name__ == "__main__":
with open(args.token) as tf:
token = tf.read().strip()
asyncio.run(amain())
loop = asyncio.get_event_loop()
# loop = asyncio.new_event_loop()
# asyncio.set_event_loop(loop)
serial = loop.run_until_complete(
serial_asyncio.open_serial_connection(loop=loop, url=args.port, baudrate=115200)
)
url = f"ws://localhost:8124?token={token}&protocol-version=1.0.0&manufacturer=TeamsCtrl&device=TeamsCtrl&app=TeamsCtrlApp&app-version=1.4"
ws = loop.run_until_complete(connect(url))
ctrl = TeamsCtrl(loop=loop, serial=serial, ws=ws)
loop.run_forever()
loop.close()

View file

@ -1,124 +0,0 @@
import time
import json
from typing import Literal
from websockets.sync.server import serve, ServerConnection
from websockets.exceptions import ConnectionClosedError
from threading import Thread
from pydantic import BaseModel, Field
class MeetingPermissions(BaseModel):
canToggleMute: bool = False
canToggleVideo: bool = False
canToggleHand: bool = False
canToggleBlur: bool = False
canToggleRecord: bool = False
canLeave: bool = False
canReact: bool = False
canToggleShareTray: bool = False
canToggleChat: bool = False
canStopSharing: bool = False
canPair: bool = False
class MeetingState(BaseModel):
isMuted: bool = False
isCameraOn: bool = False
isHandRaised: bool = False
isInMeeting: bool = False
isRecordingOn: bool = False
isBackgroundBlurred: bool = False
hasUnreadMessages: bool = False
class MeetingUpdate(BaseModel):
meetingState: MeetingState = Field(default_factory=MeetingState)
meetingPermissions: MeetingPermissions = Field(default_factory=MeetingPermissions)
class TeamsMsg(BaseModel):
apiVersion: Literal["1.0.0"] = "1.0.0"
meetingUpdate: MeetingUpdate = Field(default_factory=MeetingUpdate)
class TeamsSim:
def __init__(self) -> None:
self.meeting = TeamsMsg()
self.rx_thread = Thread(target=self.rx_thread_run)
self.ws: ServerConnection | None = None
self.active: bool = True
# {
# "apiVersion": "1.0.0",
# "service": "toggle-mute",
# "action": "toggle-mute",
# "manufacturer": "Elgato",
# "device": "NkTeamsCtrl",
# "timestamp": time.time_ns() // 1000,
# }
def rx_thread_run(self):
assert self.ws is not None
while self.active:
try:
raw_msg = self.ws.recv()
except ConnectionClosedError:
self.ws = None
self.active = False
break
msg = json.loads(raw_msg)
print(f">{msg}")
match msg.get("action", ""):
case "toggle-mute":
self.meeting.meetingUpdate.meetingState.isMuted ^= True
case "toggle-video":
self.meeting.meetingUpdate.meetingState.isCameraOn ^= True
case "toggle-hand":
self.meeting.meetingUpdate.meetingState.isHandRaised ^= True
case "leave-call":
self.meeting.meetingUpdate.meetingState.isInMeeting = False
case _:
continue
self.send_status()
def send_status(self) -> None:
if self.ws is None:
return
msg = self.meeting.json()
print(f"<{msg}")
self.ws.send(msg)
def handler(self, websocket: ServerConnection):
if self.rx_thread.is_alive():
self.active = False
if self.ws is not None:
self.ws.close_socket()
self.rx_thread.join()
self.rx_thread = Thread(target=self.rx_thread_run)
self.ws = websocket
self.active = True
self.rx_thread.start()
while True:
time.sleep(0.5)
if self.meeting.meetingUpdate.meetingState.isInMeeting is False:
print('### not in meeting, "reconnecting" in 5')
time.sleep(5)
self.meeting.meetingUpdate.meetingState.isInMeeting = True
self.send_status()
time.sleep(5)
self.meeting.meetingUpdate.meetingState.hasUnreadMessages = True
self.send_status()
time.sleep(5)
self.meeting.meetingUpdate.meetingState.hasUnreadMessages = False
self.send_status()
def run(self):
with serve(self.handler, host="localhost", port=8124) as server:
server.serve_forever()
if __name__ == "__main__":
srv = TeamsSim()
srv.run()