2 # -*- coding: utf-8 -*-
4 # Copyright (C) 2008-2013 Team XBMC
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License along
17 # with this program; if not, write to the Free Software Foundation, Inc.,
18 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21 Implementation of XBMC's UDP based input system.
23 A set of classes that abstract the various packets that the event server
24 currently supports. In addition, there's also a class, XBMCClient, that
25 provides functions that sends the various packets. Use XBMCClient if you
26 don't need complete control over packet structure.
28 The basic workflow involves:
31 2. Send x number of valid packets
34 IMPORTANT NOTE ABOUT TIMEOUTS:
35 A client is considered to be timed out if XBMC doesn't received a packet
36 at least once every 60 seconds. To "ping" XBMC with an empty packet use
37 PacketPING or XBMCClient.ping(). See the documentation for details.
40 __author__ = "d4rk@xbmc.org"
43 from struct import pack
47 MAX_PACKET_SIZE = 1024
49 MAX_PAYLOAD_SIZE = MAX_PACKET_SIZE - HEADER_SIZE
50 UNIQUE_IDENTIFICATION = (int)(time.time())
58 PT_NOTIFICATION = 0x07
90 ACTION_EXECBUILTIN = 0x01
93 ######################################################################
95 ######################################################################
97 def format_string(msg):
101 def format_uint32(num):
103 return pack ("!I", num)
105 def format_uint16(num):
111 return pack ("!H", num)
114 ######################################################################
116 ######################################################################
119 """Base class that implements a single event packet.
121 - Generic packet structure (maximum 1024 bytes per packet)
122 - Header is 32 bytes long, so 992 bytes available for payload
123 - large payloads can be split into multiple packets using H4 and H5
124 H5 should contain total no. of packets in such a case
125 - H6 contains length of P1, which is limited to 992 bytes
126 - if H5 is 0 or 1, then H4 will be ignored (single packet msg)
127 - H7 must be set to zeros for now
129 -----------------------------
130 | -H1 Signature ("XBMC") | - 4 x CHAR 4B
131 | -H2 Version (eg. 2.0) | - 2 x UNSIGNED CHAR 2B
132 | -H3 PacketType | - 1 x UNSIGNED SHORT 2B
133 | -H4 Sequence number | - 1 x UNSIGNED LONG 4B
134 | -H5 No. of packets in msg | - 1 x UNSIGNED LONG 4B
135 | -H7 Client's unique token | - 1 x UNSIGNED LONG 4B
136 | -H8 Reserved | - 10 x UNSIGNED CHAR 10B
137 |---------------------------|
139 -----------------------------
148 self.uid = UNIQUE_IDENTIFICATION
149 self.reserved = "\0" * 10
154 def append_payload(self, blob):
155 """Append to existing payload
158 blob -- binary data to append to the current payload
160 self.set_payload(self.payload + blob)
163 def set_payload(self, payload):
164 """Set the payload for this packet
167 payload -- binary data that contains the payload
169 self.payload = payload
170 self.payloadsize = len(self.payload)
171 self.maxseq = int((self.payloadsize + (MAX_PAYLOAD_SIZE - 1)) / MAX_PAYLOAD_SIZE)
174 def num_packets(self):
175 """ Return the number of packets required for payload """
178 def get_header(self, packettype=-1, seq=1, maxseq=1, payload_size=0):
179 """Construct a header and return as string
182 packettype -- valid packet types are PT_HELO, PT_BYE, PT_BUTTON,
183 PT_MOUSE, PT_PING, PT_BORADCAST, PT_NOTIFICATION,
185 seq -- the sequence of this packet for a multi packet message
187 maxseq -- the total number of packets for a multi packet message
189 payload_size -- the size of the payload of this packet (default 0)
192 packettype = self.packettype
194 header += chr(self.majver)
195 header += chr(self.minver)
196 header += format_uint16(packettype)
197 header += format_uint32(seq)
198 header += format_uint32(maxseq)
199 header += format_uint16(payload_size)
200 header += format_uint32(self.uid)
201 header += self.reserved
204 def get_payload_size(self, seq):
205 """Returns the calculated payload size for the particular packet
208 seq -- the sequence number
211 return self.payloadsize
213 if seq < self.maxseq:
214 return MAX_PAYLOAD_SIZE
216 return self.payloadsize % MAX_PAYLOAD_SIZE
219 def get_udp_message(self, packetnum=1):
220 """Construct the UDP message for the specified packetnum and return
224 packetnum -- the packet no. for which to construct the message
227 if packetnum > self.num_packets() or packetnum < 1:
231 header = self.get_header(self.packettype, packetnum, self.maxseq,
232 self.get_payload_size(packetnum))
234 header = self.get_header(PT_BLOB, packetnum, self.maxseq,
235 self.get_payload_size(packetnum))
237 payload = self.payload[ (packetnum-1) * MAX_PAYLOAD_SIZE :
238 (packetnum-1) * MAX_PAYLOAD_SIZE+
239 self.get_payload_size(packetnum) ]
240 return header + payload
242 def send(self, sock, addr, uid=UNIQUE_IDENTIFICATION):
243 """Send the entire message to the specified socket and address.
246 sock -- datagram socket object (socket.socket)
247 addr -- address, port pair (eg: ("127.0.0.1", 9777) )
248 uid -- unique identification
251 for a in range ( 0, self.num_packets() ):
253 sock.sendto(self.get_udp_message(a+1), addr)
259 class PacketHELO (Packet):
262 A HELO packet establishes a valid connection to XBMC. It is the
263 first packet that should be sent.
265 def __init__(self, devicename=None, icon_type=ICON_NONE, icon_file=None):
268 devicename -- the string that identifies the client
269 icon_type -- one of ICON_NONE, ICON_JPEG, ICON_PNG, ICON_GIF
270 icon_file -- location of icon file with respect to current working
271 directory if icon_type is not ICON_NONE
273 Packet.__init__(self)
274 self.packettype = PT_HELO
275 self.icontype = icon_type
276 self.set_payload ( format_string(devicename)[0:128] )
277 self.append_payload( chr (icon_type) )
278 self.append_payload( format_uint16 (0) ) # port no
279 self.append_payload( format_uint32 (0) ) # reserved1
280 self.append_payload( format_uint32 (0) ) # reserved2
281 if icon_type != ICON_NONE and icon_file:
282 self.append_payload( file(icon_file).read() )
284 class PacketNOTIFICATION (Packet):
285 """A NOTIFICATION packet
287 This packet displays a notification window in XBMC. It can contain
288 a caption, a message and an icon.
290 def __init__(self, title, message, icon_type=ICON_NONE, icon_file=None):
293 title -- the notification caption / title
294 message -- the main text of the notification
295 icon_type -- one of ICON_NONE, ICON_JPEG, ICON_PNG, ICON_GIF
296 icon_file -- location of icon file with respect to current working
297 directory if icon_type is not ICON_NONE
299 Packet.__init__(self)
300 self.packettype = PT_NOTIFICATION
302 self.message = message
303 self.set_payload ( format_string(title) )
304 self.append_payload( format_string(message) )
305 self.append_payload( chr (icon_type) )
306 self.append_payload( format_uint32 (0) ) # reserved
307 if icon_type != ICON_NONE and icon_file:
308 self.append_payload( file(icon_file).read() )
310 class PacketBUTTON (Packet):
313 A button packet send a key press or release event to XBMC
315 def __init__(self, code=0, repeat=1, down=1, queue=0,
316 map_name="", button_name="", amount=0, axis=0):
319 code -- raw button code (default: 0)
320 repeat -- this key press should repeat until released (default: 1)
321 Note that queued pressed cannot repeat.
322 down -- if this is 1, it implies a press event, 0 implies a release
324 queue -- a queued key press means that the button event is
325 executed just once after which the next key press is
326 processed. It can be used for macros. Currently there
327 is no support for time delays between queued presses.
329 map_name -- a combination of map_name and button_name refers to a
330 mapping in the user's Keymap.xml or Lircmap.xml.
331 map_name can be one of the following:
332 "KB" => standard keyboard map ( <keyboard> section )
333 "XG" => xbox gamepad map ( <gamepad> section )
334 "R1" => xbox remote map ( <remote> section )
335 "R2" => xbox universal remote map ( <universalremote>
337 "LI:devicename" => LIRC remote map where 'devicename' is the
339 button_name -- a button name defined in the map specified in map_name.
340 For example, if map_name is "KB" refering to the
341 <keyboard> section in Keymap.xml then, valid
342 button_names include "printscreen", "minus", "x", etc.
343 amount -- unimplemented for now; in the future it will be used for
344 specifying magnitude of analog key press events
346 Packet.__init__(self)
348 self.packettype = PT_BUTTON
349 if type (code ) == str:
352 # assign code only if we don't have a map and button name
353 if not (map_name and button_name):
356 self.flags |= BT_USE_NAME
359 self.flags |= BT_USE_AMOUNT
360 self.amount = int(amount)
365 self.flags |= BT_DOWN
369 self.flags |= BT_NO_REPEAT
371 self.flags |= BT_QUEUE
373 self.flags |= BT_AXISSINGLE
375 self.flags |= BT_AXIS
377 self.set_payload ( format_uint16(self.code) )
378 self.append_payload( format_uint16(self.flags) )
379 self.append_payload( format_uint16(self.amount) )
380 self.append_payload( format_string (map_name) )
381 self.append_payload( format_string (button_name) )
383 class PacketMOUSE (Packet):
386 A MOUSE packets sets the mouse position in XBMC
388 def __init__(self, x, y):
391 x -- horitontal position ranging from 0 to 65535
392 y -- vertical position ranging from 0 to 65535
394 The range will be mapped to the screen width and height in XBMC
396 Packet.__init__(self)
397 self.packettype = PT_MOUSE
398 self.flags = MS_ABSOLUTE
399 self.append_payload( chr (self.flags) )
400 self.append_payload( format_uint16(x) )
401 self.append_payload( format_uint16(y) )
403 class PacketBYE (Packet):
406 A BYE packet terminates the connection to XBMC.
409 Packet.__init__(self)
410 self.packettype = PT_BYE
413 class PacketPING (Packet):
416 A PING packet tells XBMC that the client is still alive. All valid
417 packets act as ping (not just this one). A client needs to ping
418 XBMC at least once in 60 seconds or it will time out.
421 Packet.__init__(self)
422 self.packettype = PT_PING
424 class PacketLOG (Packet):
427 A LOG packet tells XBMC to log the message to xbmc.log with the loglevel as specified.
429 def __init__(self, loglevel=0, logmessage="", autoprint=True):
432 loglevel -- the loglevel, follows XBMC standard.
433 logmessage -- the message to log
434 autoprint -- if the logmessage should automaticly be printed to stdout
436 Packet.__init__(self)
437 self.packettype = PT_LOG
438 self.append_payload( chr (loglevel) )
439 self.append_payload( format_string(logmessage) )
443 class PacketACTION (Packet):
446 An ACTION packet tells XBMC to do the action specified, based on the type it knows were it needs to be sent.
447 The idea is that this will be as in scripting/skining and keymapping, just triggered from afar.
449 def __init__(self, actionmessage="", actiontype=ACTION_EXECBUILTIN):
452 loglevel -- the loglevel, follows XBMC standard.
453 logmessage -- the message to log
454 autoprint -- if the logmessage should automaticly be printed to stdout
456 Packet.__init__(self)
457 self.packettype = PT_ACTION
458 self.append_payload( chr (actiontype) )
459 self.append_payload( format_string(actionmessage) )
461 ######################################################################
463 ######################################################################
466 """An XBMC event client"""
468 def __init__(self, name ="", icon_file=None, broadcast=False, uid=UNIQUE_IDENTIFICATION,
472 name -- Name of the client
473 icon_file -- location of an icon file, if any (png, jpg or gif)
474 uid -- unique identification
476 self.name = str(name)
477 self.icon_file = icon_file
478 self.icon_type = self._get_icon_type(icon_file)
481 self.sock = socket(AF_INET,SOCK_DGRAM)
483 self.sock.setsockopt(SOL_SOCKET, SO_BROADCAST, 1)
487 def connect(self, ip=None, port=None):
488 """Initialize connection to XBMC
489 ip -- IP Address of XBMC
490 port -- port that the event server on XBMC is listening on
495 self.port = int(port)
496 self.addr = (self.ip, self.port)
497 packet = PacketHELO(self.name, self.icon_type, self.icon_file)
498 return packet.send(self.sock, self.addr, self.uid)
502 """Close the current connection"""
504 return packet.send(self.sock, self.addr, self.uid)
508 """Send a PING packet"""
509 packet = PacketPING()
510 return packet.send(self.sock, self.addr, self.uid)
513 def send_notification(self, title="", message="", icon_file=None):
514 """Send a notification to the connected XBMC
516 title -- The title/heading for the notifcation
517 message -- The message to be displayed
518 icon_file -- location of an icon file, if any (png, jpg, gif)
521 packet = PacketNOTIFICATION(title, message,
522 self._get_icon_type(icon_file),
524 return packet.send(self.sock, self.addr, self.uid)
527 def send_keyboard_button(self, button=None):
528 """Send a keyboard event to XBMC
530 button -- name of the keyboard button to send (same as in Keymap.xml)
534 return self.send_button(map="KB", button=button)
537 def send_remote_button(self, button=None):
538 """Send a remote control event to XBMC
540 button -- name of the remote control button to send (same as in Keymap.xml)
544 return self.send_button(map="R1", button=button)
547 def release_button(self):
548 """Release all buttons"""
549 packet = PacketBUTTON(code=0x01, down=0)
550 return packet.send(self.sock, self.addr, self.uid)
553 def send_button(self, map="", button="", amount=0):
554 """Send a button event to XBMC
556 map -- a combination of map_name and button_name refers to a
557 mapping in the user's Keymap.xml or Lircmap.xml.
558 map_name can be one of the following:
559 "KB" => standard keyboard map ( <keyboard> section )
560 "XG" => xbox gamepad map ( <gamepad> section )
561 "R1" => xbox remote map ( <remote> section )
562 "R2" => xbox universal remote map ( <universalremote>
564 "LI:devicename" => LIRC remote map where 'devicename' is the
566 button -- a button name defined in the map specified in map, above.
567 For example, if map is "KB" refering to the <keyboard>
568 section in Keymap.xml then, valid buttons include
569 "printscreen", "minus", "x", etc.
571 packet = PacketBUTTON(map_name=str(map), button_name=str(button), amount=amount)
572 return packet.send(self.sock, self.addr, self.uid)
574 def send_button_state(self, map="", button="", amount=0, down=0, axis=0):
575 """Send a button event to XBMC
577 map -- a combination of map_name and button_name refers to a
578 mapping in the user's Keymap.xml or Lircmap.xml.
579 map_name can be one of the following:
580 "KB" => standard keyboard map ( <keyboard> section )
581 "XG" => xbox gamepad map ( <gamepad> section )
582 "R1" => xbox remote map ( <remote> section )
583 "R2" => xbox universal remote map ( <universalremote>
585 "LI:devicename" => LIRC remote map where 'devicename' is the
587 button -- a button name defined in the map specified in map, above.
588 For example, if map is "KB" refering to the <keyboard>
589 section in Keymap.xml then, valid buttons include
590 "printscreen", "minus", "x", etc.
598 packet = PacketBUTTON(map_name=str(map), button_name=str(button), amount=amount, down=down, queue=1, axis=axis)
599 return packet.send(self.sock, self.addr, self.uid)
601 def send_mouse_position(self, x=0, y=0):
602 """Send a mouse event to XBMC
604 x -- absolute x position of mouse ranging from 0 to 65535
605 which maps to the entire screen width
606 y -- same a 'x' but relates to the screen height
608 packet = PacketMOUSE(int(x), int(y))
609 return packet.send(self.sock, self.addr, self.uid)
611 def send_log(self, loglevel=0, logmessage="", autoprint=True):
614 loglevel -- the loglevel, follows XBMC standard.
615 logmessage -- the message to log
616 autoprint -- if the logmessage should automaticly be printed to stdout
618 packet = PacketLOG(loglevel, logmessage)
619 return packet.send(self.sock, self.addr, self.uid)
621 def send_action(self, actionmessage="", actiontype=ACTION_EXECBUILTIN):
624 actionmessage -- the ActionString
625 actiontype -- The ActionType the ActionString should be sent to.
627 packet = PacketACTION(actionmessage, actiontype)
628 return packet.send(self.sock, self.addr, self.uid)
630 def _get_icon_type(self, icon_file):
632 if icon_file.lower()[-3:] == "png":
634 elif icon_file.lower()[-3:] == "gif":
636 elif icon_file.lower()[-3:] == "jpg":