You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
286 lines
11 KiB
Python
286 lines
11 KiB
Python
7 months ago
|
from communication.World_Parser import World_Parser
|
||
|
from itertools import count
|
||
|
from select import select
|
||
|
from sys import exit
|
||
|
from world.World import World
|
||
|
import socket
|
||
|
import time
|
||
|
|
||
|
class Server_Comm():
|
||
|
monitor_socket = None
|
||
|
|
||
|
def __init__(self, host:str, agent_port:int, monitor_port:int, unum:int, robot_type:int, team_name:str,
|
||
|
world_parser:World_Parser, world:World, other_players, wait_for_server=True) -> None:
|
||
|
|
||
|
self.BUFFER_SIZE = 8192
|
||
|
self.rcv_buff = bytearray(self.BUFFER_SIZE)
|
||
|
self.send_buff = []
|
||
|
self.world_parser = world_parser
|
||
|
self.unum = unum
|
||
|
|
||
|
# During initialization, it's not clear whether we are on the left or right side
|
||
|
self._unofficial_beam_msg_left = "(agent (unum " + str(unum) + ") (team Left) (move "
|
||
|
self._unofficial_beam_msg_right = "(agent (unum " + str(unum) + ") (team Right) (move "
|
||
|
self.world = world
|
||
|
|
||
|
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM )
|
||
|
|
||
|
if wait_for_server: print("Waiting for server at ", host, ":", agent_port, sep="",end=".",flush=True)
|
||
|
while True:
|
||
|
try:
|
||
|
self.socket.connect((host, agent_port))
|
||
|
print(end=" ")
|
||
|
break
|
||
|
except ConnectionRefusedError:
|
||
|
if not wait_for_server:
|
||
|
print("Server is down. Closing...")
|
||
|
exit()
|
||
|
time.sleep(1)
|
||
|
print(".",end="",flush=True)
|
||
|
print("Connected agent", unum, self.socket.getsockname())
|
||
|
|
||
|
self.send_immediate(b'(scene rsg/agent/nao/nao_hetero.rsg ' + str(robot_type).encode() + b')')
|
||
|
self._receive_async(other_players, True)
|
||
|
|
||
|
self.send_immediate(b'(init (unum '+ str(unum).encode() + b') (teamname '+ team_name.encode() + b'))')
|
||
|
self._receive_async(other_players, False)
|
||
|
|
||
|
# Repeat to guarantee that team side information is received
|
||
|
for _ in range(3):
|
||
|
# Eliminate advanced step by changing syn order (rcssserver3d protocol bug, usually seen for player 11)
|
||
|
self.send_immediate(b'(syn)') #if this syn is not needed, it will be discarded by the server
|
||
|
for p in other_players:
|
||
|
p.scom.send_immediate(b'(syn)')
|
||
|
for p in other_players:
|
||
|
p.scom.receive()
|
||
|
self.receive()
|
||
|
|
||
|
if world.team_side_is_left == None:
|
||
|
print("\nError: server did not return a team side! Check server terminal!")
|
||
|
exit()
|
||
|
|
||
|
# Monitor socket is shared by all agents on the same thread
|
||
|
if Server_Comm.monitor_socket is None and monitor_port is not None:
|
||
|
print("Connecting to server's monitor port at ", host, ":", monitor_port, sep="",end=".",flush=True)
|
||
|
Server_Comm.monitor_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM )
|
||
|
Server_Comm.monitor_socket.connect((host, monitor_port))
|
||
|
print("Done!")
|
||
|
|
||
|
|
||
|
|
||
|
def _receive_async(self, other_players, first_pass) -> None:
|
||
|
'''Private function that receives asynchronous information during the initialization'''
|
||
|
|
||
|
if not other_players:
|
||
|
self.receive()
|
||
|
return
|
||
|
|
||
|
self.socket.setblocking(0)
|
||
|
if first_pass: print("Async agent",self.unum,"initialization", end="", flush=True)
|
||
|
|
||
|
while True:
|
||
|
try:
|
||
|
print(".",end="",flush=True)
|
||
|
self.receive()
|
||
|
break
|
||
|
except:
|
||
|
pass
|
||
|
for p in other_players:
|
||
|
p.scom.send_immediate(b'(syn)')
|
||
|
for p in other_players:
|
||
|
p.scom.receive()
|
||
|
|
||
|
self.socket.setblocking(1)
|
||
|
if not first_pass: print("Done!")
|
||
|
|
||
|
|
||
|
def receive(self, update=True):
|
||
|
|
||
|
for i in count(): # parse all messages and perform value updates, but heavy computation is only done once at the end
|
||
|
try:
|
||
|
if self.socket.recv_into(self.rcv_buff, nbytes=4) != 4: raise ConnectionResetError()
|
||
|
msg_size = int.from_bytes(self.rcv_buff[:4], byteorder='big', signed=False)
|
||
|
if self.socket.recv_into(self.rcv_buff, nbytes=msg_size, flags=socket.MSG_WAITALL) != msg_size: raise ConnectionResetError()
|
||
|
except ConnectionResetError:
|
||
|
print("\nError: socket was closed by rcssserver3d!")
|
||
|
exit()
|
||
|
|
||
|
self.world_parser.parse(self.rcv_buff[:msg_size])
|
||
|
if len(select([self.socket],[],[], 0.0)[0]) == 0: break
|
||
|
|
||
|
if update:
|
||
|
if i==1: self.world.log( "Server_Comm.py: The agent lost 1 packet! Is syncmode enabled?")
|
||
|
if i>1: self.world.log(f"Server_Comm.py: The agent lost {i} consecutive packets! Is syncmode disabled?")
|
||
|
self.world.update()
|
||
|
|
||
|
if len(select([self.socket],[],[], 0.0)[0]) != 0:
|
||
|
self.world.log("Server_Comm.py: Received a new packet while on world.update()!")
|
||
|
self.receive()
|
||
|
|
||
|
|
||
|
def send_immediate(self, msg:bytes) -> None:
|
||
|
''' Commit and send immediately '''
|
||
|
try:
|
||
|
self.socket.send( (len(msg)).to_bytes(4,byteorder='big') + msg ) #Add message length in the first 4 bytes
|
||
|
except BrokenPipeError:
|
||
|
print("\nError: socket was closed by rcssserver3d!")
|
||
|
exit()
|
||
|
|
||
|
|
||
|
def send(self) -> None:
|
||
|
''' Send all committed messages '''
|
||
|
if len(select([self.socket],[],[], 0.0)[0]) == 0:
|
||
|
self.send_buff.append(b'(syn)')
|
||
|
self.send_immediate( b''.join(self.send_buff) )
|
||
|
else:
|
||
|
self.world.log("Server_Comm.py: Received a new packet while thinking!")
|
||
|
self.send_buff = [] #clear buffer
|
||
|
|
||
|
def commit(self, msg:bytes) -> None:
|
||
|
assert type(msg) == bytes, "Message must be of type Bytes!"
|
||
|
self.send_buff.append(msg)
|
||
|
|
||
|
def commit_and_send(self, msg:bytes = b'') -> None:
|
||
|
self.commit(msg)
|
||
|
self.send()
|
||
|
|
||
|
def clear_buffer(self) -> None:
|
||
|
self.send_buff = []
|
||
|
|
||
|
def commit_announcement(self, msg:bytes) -> None:
|
||
|
'''
|
||
|
Say something to every player on the field.
|
||
|
Maximum 20 characters, ascii between 0x20, 0x7E except ' ', '(', ')'
|
||
|
Accepted: letters+numbers+symbols: !"#$%&'*+,-./:;<=>?@[\]^_`{|}~
|
||
|
Message range: 50m (the field is 36m diagonally, so ignore this limitation)
|
||
|
A player can only hear a teammate's message every 2 steps (0.04s)
|
||
|
This ability exists independetly for messages from both teams
|
||
|
(i.e. our team cannot spam the other team to block their messages)
|
||
|
Messages from oneself are always heard
|
||
|
'''
|
||
|
assert len(msg) <= 20 and type(msg) == bytes
|
||
|
self.commit(b'(say ' + msg + b')')
|
||
|
|
||
|
def commit_pass_command(self) -> None:
|
||
|
'''
|
||
|
Issue a pass command:
|
||
|
Conditions:
|
||
|
- The current playmode is PlayOn
|
||
|
- The agent is near the ball (default 0.5m)
|
||
|
- No opponents are near the ball (default 1m)
|
||
|
- The ball is stationary (default <0.05m/s)
|
||
|
- A certain amount of time has passed between pass commands
|
||
|
'''
|
||
|
self.commit(b'(pass)')
|
||
|
|
||
|
def commit_beam(self, pos2d, rot) -> None:
|
||
|
'''
|
||
|
Official beam command that can be used in-game
|
||
|
This beam is affected by noise (unless it is disabled in the server configuration)
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
pos2d : array_like
|
||
|
Absolute 2D position (negative X is always our half of the field, no matter our side)
|
||
|
rot : `int`/`float`
|
||
|
Player angle in degrees (0 points forward)
|
||
|
'''
|
||
|
assert len(pos2d)==2, "The official beam command accepts only 2D positions!"
|
||
|
self.commit( f"(beam {pos2d[0]} {pos2d[1]} {rot})".encode() )
|
||
|
|
||
|
|
||
|
def unofficial_beam(self, pos3d, rot) -> None:
|
||
|
'''
|
||
|
Unofficial beam - it cannot be used in official matches
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
pos3d : array_like
|
||
|
Absolute 3D position (negative X is always our half of the field, no matter our side)
|
||
|
rot : `int`/`float`
|
||
|
Player angle in degrees (0 points forward)
|
||
|
'''
|
||
|
assert len(pos3d)==3, "The unofficial beam command accepts only 3D positions!"
|
||
|
|
||
|
# there is no need to normalize the angle, the server accepts any angle
|
||
|
if self.world.team_side_is_left:
|
||
|
msg = f"{self._unofficial_beam_msg_left }{ pos3d[0]} { pos3d[1]} {pos3d[2]} {rot-90}))".encode()
|
||
|
else:
|
||
|
msg = f"{self._unofficial_beam_msg_right}{-pos3d[0]} {-pos3d[1]} {pos3d[2]} {rot+90}))".encode()
|
||
|
|
||
|
self.monitor_socket.send( (len(msg)).to_bytes(4,byteorder='big') + msg )
|
||
|
|
||
|
def unofficial_kill_sim(self) -> None:
|
||
|
''' Unofficial kill simulator command '''
|
||
|
msg = b'(killsim)'
|
||
|
self.monitor_socket.send( (len(msg)).to_bytes(4,byteorder='big') + msg )
|
||
|
|
||
|
def unofficial_move_ball(self, pos3d, vel3d=(0,0,0)) -> None:
|
||
|
'''
|
||
|
Unofficial command to move ball
|
||
|
info: ball radius = 0.042m
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
pos3d : array_like
|
||
|
Absolute 3D position (negative X is always our half of the field, no matter our side)
|
||
|
vel3d : array_like
|
||
|
Absolute 3D velocity (negative X is always our half of the field, no matter our side)
|
||
|
'''
|
||
|
assert len(pos3d)==3 and len(vel3d)==3, "To move the ball we need a 3D position and velocity"
|
||
|
|
||
|
if self.world.team_side_is_left:
|
||
|
msg = f"(ball (pos { pos3d[0]} { pos3d[1]} {pos3d[2]}) (vel { vel3d[0]} { vel3d[1]} {vel3d[2]}))".encode()
|
||
|
else:
|
||
|
msg = f"(ball (pos {-pos3d[0]} {-pos3d[1]} {pos3d[2]}) (vel {-vel3d[0]} {-vel3d[1]} {vel3d[2]}))".encode()
|
||
|
|
||
|
self.monitor_socket.send( (len(msg)).to_bytes(4,byteorder='big') + msg )
|
||
|
|
||
|
def unofficial_set_game_time(self, time_in_s : float) -> None:
|
||
|
'''
|
||
|
Unofficial command to set the game time
|
||
|
e.g. unofficial_set_game_time(68.78)
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
time_in_s : float
|
||
|
Game time in seconds
|
||
|
'''
|
||
|
msg = f"(time {time_in_s})".encode()
|
||
|
self.monitor_socket.send( (len(msg)).to_bytes(4,byteorder='big') + msg )
|
||
|
|
||
|
def unofficial_set_play_mode(self, play_mode : str) -> None:
|
||
|
'''
|
||
|
Unofficial command to set the play mode
|
||
|
e.g. unofficial_set_play_mode("PlayOn")
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
play_mode : str
|
||
|
Play mode
|
||
|
'''
|
||
|
msg = f"(playMode {play_mode})".encode()
|
||
|
self.monitor_socket.send( (len(msg)).to_bytes(4,byteorder='big') + msg )
|
||
|
|
||
|
def unofficial_kill_player(self, unum : int, team_side_is_left : bool) -> None:
|
||
|
'''
|
||
|
Unofficial command to kill specific player
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
unum : int
|
||
|
Uniform number
|
||
|
team_side_is_left : bool
|
||
|
True if player to kill belongs to left team
|
||
|
'''
|
||
|
msg = f"(kill (unum {unum}) (team {'Left' if team_side_is_left else 'Right'}))".encode()
|
||
|
self.monitor_socket.send( (len(msg)).to_bytes(4,byteorder='big') + msg )
|
||
|
|
||
|
def close(self, close_monitor_socket = False):
|
||
|
''' Close agent socket, and optionally the monitor socket (shared by players running on the same thread) '''
|
||
|
self.socket.close()
|
||
|
if close_monitor_socket and Server_Comm.monitor_socket is not None:
|
||
|
Server_Comm.monitor_socket.close()
|
||
|
Server_Comm.monitor_socket = None
|
||
|
|