[cosmetics] update date in GPL header
[vuplus_xbmc] / tools / EventClients / lib / python / xbmcclient.py
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3
4 #   Copyright (C) 2008-2013 Team XBMC
5 #
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.
10 #
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.
15 #
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.
19
20 """
21 Implementation of XBMC's UDP based input system.
22
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.
27
28 The basic workflow involves:
29
30 1. Send a HELO packet
31 2. Send x number of valid packets
32 3. Send a BYE packet
33
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.
38 """
39
40 __author__  = "d4rk@xbmc.org"
41 __version__ = "0.0.3"
42
43 from struct import pack
44 from socket import *
45 import time
46
47 MAX_PACKET_SIZE  = 1024
48 HEADER_SIZE      = 32
49 MAX_PAYLOAD_SIZE = MAX_PACKET_SIZE - HEADER_SIZE
50 UNIQUE_IDENTIFICATION = (int)(time.time())
51
52 PT_HELO          = 0x01
53 PT_BYE           = 0x02
54 PT_BUTTON        = 0x03
55 PT_MOUSE         = 0x04
56 PT_PING          = 0x05
57 PT_BROADCAST     = 0x06
58 PT_NOTIFICATION  = 0x07
59 PT_BLOB          = 0x08
60 PT_LOG           = 0x09
61 PT_ACTION        = 0x0A
62 PT_DEBUG         = 0xFF
63
64 ICON_NONE = 0x00
65 ICON_JPEG = 0x01
66 ICON_PNG  = 0x02
67 ICON_GIF  = 0x03
68
69 BT_USE_NAME   = 0x01
70 BT_DOWN       = 0x02
71 BT_UP         = 0x04
72 BT_USE_AMOUNT = 0x08
73 BT_QUEUE      = 0x10
74 BT_NO_REPEAT  = 0x20
75 BT_VKEY       = 0x40
76 BT_AXIS       = 0x80
77 BT_AXISSINGLE = 0x100
78
79 MS_ABSOLUTE = 0x01
80
81 LOGDEBUG   = 0x00
82 LOGINFO    = 0x01
83 LOGNOTICE  = 0x02
84 LOGWARNING = 0x03
85 LOGERROR   = 0x04
86 LOGSEVERE  = 0x05
87 LOGFATAL   = 0x06
88 LOGNONE    = 0x07
89
90 ACTION_EXECBUILTIN = 0x01
91 ACTION_BUTTON      = 0x02
92
93 ######################################################################
94 # Helper Functions
95 ######################################################################
96
97 def format_string(msg):
98     """ """
99     return msg + "\0"
100
101 def format_uint32(num):
102     """ """
103     return pack ("!I", num)
104
105 def format_uint16(num):
106     """ """
107     if num<0:
108         num = 0
109     elif num>65535:
110         num = 65535
111     return pack ("!H", num)
112
113
114 ######################################################################
115 #  Packet Classes
116 ######################################################################
117
118 class Packet:
119     """Base class that implements a single event packet.
120
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
128
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          |---------------------------|
138          | -P1 payload               | -
139          -----------------------------
140     """
141     def __init__(self):
142         self.sig = "XBMC"
143         self.minver = 0
144         self.majver = 2
145         self.seq = 1
146         self.maxseq = 1
147         self.payloadsize = 0
148         self.uid = UNIQUE_IDENTIFICATION
149         self.reserved = "\0" * 10
150         self.payload = ""
151         return
152
153
154     def append_payload(self, blob):
155         """Append to existing payload
156
157         Arguments:
158         blob -- binary data to append to the current payload
159         """
160         self.set_payload(self.payload + blob)
161
162
163     def set_payload(self, payload):
164         """Set the payload for this packet
165
166         Arguments:
167         payload -- binary data that contains the payload
168         """
169         self.payload = payload
170         self.payloadsize = len(self.payload)
171         self.maxseq = int((self.payloadsize + (MAX_PAYLOAD_SIZE - 1)) / MAX_PAYLOAD_SIZE)
172
173
174     def num_packets(self):
175         """ Return the number of packets required for payload """
176         return self.maxseq
177
178     def get_header(self, packettype=-1, seq=1, maxseq=1, payload_size=0):
179         """Construct a header and return as string
180
181         Keyword arguments:
182         packettype -- valid packet types are PT_HELO, PT_BYE, PT_BUTTON,
183                       PT_MOUSE, PT_PING, PT_BORADCAST, PT_NOTIFICATION,
184                       PT_BLOB, PT_DEBUG
185         seq -- the sequence of this packet for a multi packet message
186                (default 1)
187         maxseq -- the total number of packets for a multi packet message
188                   (default 1)
189         payload_size -- the size of the payload of this packet (default 0)
190         """
191         if packettype < 0:
192             packettype = self.packettype
193         header = self.sig
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
202         return header
203
204     def get_payload_size(self, seq):
205         """Returns the calculated payload size for the particular packet
206
207         Arguments:
208         seq -- the sequence number
209         """
210         if self.maxseq == 1:
211             return self.payloadsize
212
213         if seq < self.maxseq:
214             return MAX_PAYLOAD_SIZE
215
216         return self.payloadsize % MAX_PAYLOAD_SIZE
217
218
219     def get_udp_message(self, packetnum=1):
220         """Construct the UDP message for the specified packetnum and return
221         as string
222
223         Keyword arguments:
224         packetnum -- the packet no. for which to construct the message
225                      (default 1)
226         """
227         if packetnum > self.num_packets() or packetnum < 1:
228             return ""
229         header = ""
230         if packetnum==1:
231             header = self.get_header(self.packettype, packetnum, self.maxseq,
232                                      self.get_payload_size(packetnum))
233         else:
234             header = self.get_header(PT_BLOB, packetnum, self.maxseq,
235                                      self.get_payload_size(packetnum))
236
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
241
242     def send(self, sock, addr, uid=UNIQUE_IDENTIFICATION):
243         """Send the entire message to the specified socket and address.
244
245         Arguments:
246         sock -- datagram socket object (socket.socket)
247         addr -- address, port pair (eg: ("127.0.0.1", 9777) )
248         uid  -- unique identification
249         """
250         self.uid = uid
251         for a in range ( 0, self.num_packets() ):
252             try:
253                 sock.sendto(self.get_udp_message(a+1), addr)
254             except:
255                 return False
256         return True
257
258
259 class PacketHELO (Packet):
260     """A HELO packet
261
262     A HELO packet establishes a valid connection to XBMC. It is the
263     first packet that should be sent.
264     """
265     def __init__(self, devicename=None, icon_type=ICON_NONE, icon_file=None):
266         """
267         Keyword arguments:
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
272         """
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() )
283
284 class PacketNOTIFICATION (Packet):
285     """A NOTIFICATION packet
286
287     This packet displays a notification window in XBMC. It can contain
288     a caption, a message and an icon.
289     """
290     def __init__(self, title, message, icon_type=ICON_NONE, icon_file=None):
291         """
292         Keyword arguments:
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
298         """
299         Packet.__init__(self)
300         self.packettype = PT_NOTIFICATION
301         self.title = title
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() )
309
310 class PacketBUTTON (Packet):
311     """A BUTTON packet
312
313     A button packet send a key press or release event to XBMC
314     """
315     def __init__(self, code=0, repeat=1, down=1, queue=0,
316                  map_name="", button_name="", amount=0, axis=0):
317         """
318         Keyword arguments:
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
323                 event. (default: 1)
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.
328                  (default: 0)
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>
336                             section )
337                     "LI:devicename" => LIRC remote map where 'devicename' is the
338                     actual device's name
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
345         """
346         Packet.__init__(self)
347         self.flags = 0
348         self.packettype = PT_BUTTON
349         if type (code ) == str:
350             code = ord(code)
351
352         # assign code only if we don't have a map and button name
353         if not (map_name and button_name):
354             self.code = code
355         else:
356             self.flags |= BT_USE_NAME
357             self.code = 0
358         if (amount != None):
359             self.flags |= BT_USE_AMOUNT
360             self.amount = int(amount)
361         else:
362             self.amount = 0
363
364         if down:
365             self.flags |= BT_DOWN
366         else:
367             self.flags |= BT_UP
368         if not repeat:
369             self.flags |= BT_NO_REPEAT
370         if queue:
371             self.flags |= BT_QUEUE
372         if axis == 1:
373             self.flags |= BT_AXISSINGLE
374         elif axis == 2:
375             self.flags |= BT_AXIS
376
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) )
382
383 class PacketMOUSE (Packet):
384     """A MOUSE packet
385
386     A MOUSE packets sets the mouse position in XBMC
387     """
388     def __init__(self, x, y):
389         """
390         Arguments:
391         x -- horitontal position ranging from 0 to 65535
392         y -- vertical position ranging from 0 to 65535
393
394         The range will be mapped to the screen width and height in XBMC
395         """
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) )
402
403 class PacketBYE (Packet):
404     """A BYE packet
405
406     A BYE packet terminates the connection to XBMC.
407     """
408     def __init__(self):
409         Packet.__init__(self)
410         self.packettype = PT_BYE
411
412
413 class PacketPING (Packet):
414     """A PING packet
415
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.
419     """
420     def __init__(self):
421         Packet.__init__(self)
422         self.packettype = PT_PING
423
424 class PacketLOG (Packet):
425     """A LOG packet
426
427     A LOG packet tells XBMC to log the message to xbmc.log with the loglevel as specified.
428     """
429     def __init__(self, loglevel=0, logmessage="", autoprint=True):
430         """
431         Keyword arguments:
432         loglevel -- the loglevel, follows XBMC standard.
433         logmessage -- the message to log
434         autoprint -- if the logmessage should automaticly be printed to stdout
435         """
436         Packet.__init__(self)
437         self.packettype = PT_LOG
438         self.append_payload( chr (loglevel) )
439         self.append_payload( format_string(logmessage) )
440         if (autoprint):
441           print logmessage
442
443 class PacketACTION (Packet):
444     """An ACTION packet
445
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.
448     """
449     def __init__(self, actionmessage="", actiontype=ACTION_EXECBUILTIN):
450         """
451         Keyword arguments:
452         loglevel -- the loglevel, follows XBMC standard.
453         logmessage -- the message to log
454         autoprint -- if the logmessage should automaticly be printed to stdout
455         """
456         Packet.__init__(self)
457         self.packettype = PT_ACTION
458         self.append_payload( chr (actiontype) )
459         self.append_payload( format_string(actionmessage) )
460
461 ######################################################################
462 # XBMC Client Class
463 ######################################################################
464
465 class XBMCClient:
466     """An XBMC event client"""
467
468     def __init__(self, name ="", icon_file=None, broadcast=False, uid=UNIQUE_IDENTIFICATION,
469                  ip="127.0.0.1"):
470         """
471         Keyword arguments:
472         name -- Name of the client
473         icon_file -- location of an icon file, if any (png, jpg or gif)
474         uid  -- unique identification
475         """
476         self.name = str(name)
477         self.icon_file = icon_file
478         self.icon_type = self._get_icon_type(icon_file)
479         self.ip = ip
480         self.port = 9777
481         self.sock = socket(AF_INET,SOCK_DGRAM)
482         if broadcast:
483             self.sock.setsockopt(SOL_SOCKET, SO_BROADCAST, 1)
484         self.uid = uid
485
486
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
491         """
492         if ip:
493             self.ip = ip
494         if port:
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)
499
500
501     def close(self):
502         """Close the current connection"""
503         packet = PacketBYE()
504         return packet.send(self.sock, self.addr, self.uid)
505
506
507     def ping(self):
508         """Send a PING packet"""
509         packet = PacketPING()
510         return packet.send(self.sock, self.addr, self.uid)
511
512
513     def send_notification(self, title="", message="", icon_file=None):
514         """Send a notification to the connected XBMC
515         Keyword Arguments:
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)
519         """
520         self.connect()
521         packet = PacketNOTIFICATION(title, message,
522                                     self._get_icon_type(icon_file),
523                                     icon_file)
524         return packet.send(self.sock, self.addr, self.uid)
525
526
527     def send_keyboard_button(self, button=None):
528         """Send a keyboard event to XBMC
529         Keyword Arguments:
530         button -- name of the keyboard button to send (same as in Keymap.xml)
531         """
532         if not button:
533             return
534         return self.send_button(map="KB", button=button)
535
536
537     def send_remote_button(self, button=None):
538         """Send a remote control event to XBMC
539         Keyword Arguments:
540         button -- name of the remote control button to send (same as in Keymap.xml)
541         """
542         if not button:
543             return
544         return self.send_button(map="R1", button=button)
545
546
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)
551
552
553     def send_button(self, map="", button="", amount=0):
554         """Send a button event to XBMC
555         Keyword arguments:
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>
563                            section )
564                    "LI:devicename" => LIRC remote map where 'devicename' is the
565                                       actual device's name
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.
570         """
571         packet = PacketBUTTON(map_name=str(map), button_name=str(button), amount=amount)
572         return packet.send(self.sock, self.addr, self.uid)
573
574     def send_button_state(self, map="", button="", amount=0, down=0, axis=0):
575         """Send a button event to XBMC
576         Keyword arguments:
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>
584                            section )
585                    "LI:devicename" => LIRC remote map where 'devicename' is the
586                                       actual device's name
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.
591         """
592         if axis:
593           if amount == 0:
594             down = 0
595           else:
596             down = 1
597
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)
600
601     def send_mouse_position(self, x=0, y=0):
602         """Send a mouse event to XBMC
603         Keywords Arguments:
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
607         """
608         packet = PacketMOUSE(int(x), int(y))
609         return packet.send(self.sock, self.addr, self.uid)
610
611     def send_log(self, loglevel=0, logmessage="", autoprint=True):
612         """
613         Keyword arguments:
614         loglevel -- the loglevel, follows XBMC standard.
615         logmessage -- the message to log
616         autoprint -- if the logmessage should automaticly be printed to stdout
617         """
618         packet = PacketLOG(loglevel, logmessage)
619         return packet.send(self.sock, self.addr, self.uid)
620
621     def send_action(self, actionmessage="", actiontype=ACTION_EXECBUILTIN):
622         """
623         Keyword arguments:
624         actionmessage -- the ActionString
625         actiontype -- The ActionType the ActionString should be sent to.
626         """
627         packet = PacketACTION(actionmessage, actiontype)
628         return packet.send(self.sock, self.addr, self.uid)
629
630     def _get_icon_type(self, icon_file):
631         if icon_file:
632             if icon_file.lower()[-3:] == "png":
633                 return ICON_PNG
634             elif icon_file.lower()[-3:] == "gif":
635                 return ICON_GIF
636             elif icon_file.lower()[-3:] == "jpg":
637                 return ICON_JPEG
638         return ICON_NONE