├── LICENSE ├── MQTT.py └── README /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012 by Adam Rudd 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /MQTT.py: -------------------------------------------------------------------------------- 1 | from twisted.internet.protocol import Protocol 2 | import random 3 | 4 | 5 | class MQTTProtocol(Protocol): 6 | 7 | _packetTypes = {0x00: "null", 0x01: "connect", 0x02: "connack", 8 | 0x03: "publish", 0x04: "puback", 0x05: "pubrec", 9 | 0x06: "pubrel", 0x07: "pubcomp", 0x08: "subscribe", 10 | 0x09: "suback", 0x0A: "unsubscribe", 0x0B: "unsuback", 11 | 0x0C: "pingreq", 0x0D: "pingresp", 0x0E: "disconnect"} 12 | 13 | buffer = bytearray() 14 | 15 | def dataReceived(self, data): 16 | self._accumulatePacket(data) 17 | 18 | def _accumulatePacket(self, data): 19 | self.buffer.extend(data) 20 | 21 | length = None 22 | 23 | while len(self.buffer): 24 | if length is None: 25 | # Start on a new packet 26 | 27 | # Haven't got enough data to start a new packet, 28 | # wait for some more 29 | if len(self.buffer) < 2: 30 | break 31 | 32 | lenLen = 1 33 | # Calculate the length of the length field 34 | while lenLen < len(self.buffer): 35 | if not self.buffer[lenLen] & 0x80: 36 | break 37 | lenLen += 1 38 | 39 | # We still haven't got all of the remaining length field 40 | if lenLen < len(self.buffer) and self.buffer[lenLen] & 0x80: 41 | return 42 | 43 | length = self._decodeLength(self.buffer[1:]) 44 | 45 | if len(self.buffer) >= length + lenLen + 1: 46 | chunk = self.buffer[:length + lenLen + 1] 47 | self._processPacket(chunk) 48 | self.buffer = self.buffer[length + lenLen + 1:] 49 | length = None 50 | 51 | else: 52 | break 53 | 54 | def _processPacket(self, packet): 55 | try: 56 | packet_type = (packet[0] & 0xF0) >> 4 57 | packet_type_name = self._packetTypes[packet_type] 58 | dup = (packet[0] & 0x08) == 0x08 59 | qos = (packet[0] & 0x06) >> 1 60 | retain = (packet[0] & 0x01) == 0x01 61 | except: 62 | # Invalid packet type, throw away this packet 63 | print "Invalid packet type %x" % packet_type 64 | return 65 | 66 | # Strip the fixed header 67 | lenLen = 1 68 | while packet[lenLen] & 0x80: 69 | lenLen += 1 70 | 71 | packet = packet[lenLen+1:] 72 | 73 | # Get the appropriate handler function 74 | packetHandler = getattr(self, "_event_%s" % packet_type_name, None) 75 | 76 | if packetHandler: 77 | packetHandler(packet, qos, dup, retain) 78 | else: 79 | # Rocks fall, everyone dies 80 | print "Invalid packet handler for %s" % packet_type_name 81 | return 82 | 83 | def _event_connect(self, packet, qos, dup, retain): 84 | # Strip the protocol name and version number 85 | packet = packet[len("06MQisdp3"):] 86 | 87 | # Extract the connect flags 88 | willRetain = packet[0] & 0x20 == 0x20 89 | willQos = packet[0] & 0x18 >> 3 90 | willFlag = packet[0] & 0x04 == 0x04 91 | cleanStart = packet[0] & 0x02 == 0x02 92 | 93 | packet = packet[1:] 94 | # Extract the keepalive period 95 | keepalive = self._decodeValue(packet[:2]) 96 | packet = packet[2:] 97 | 98 | # Extract the client id 99 | clientID = self._decodeString(packet) 100 | packet = packet[len(clientID) + 2:] 101 | 102 | # Extract the will topic and message, if applicable 103 | willTopic = None 104 | willMessage = None 105 | 106 | if willFlag: 107 | # Extract the will topic 108 | willTopic = self._decodeString(packet) 109 | packet = packet[len(willTopic) + 2:] 110 | 111 | # Extract the will message 112 | # Whatever remains is the will message 113 | willMessage = packet 114 | 115 | self.connectReceived(clientID, keepalive, willTopic, 116 | willMessage, willQos, willRetain, 117 | cleanStart) 118 | 119 | def _event_connack(self, packet, qos, dup, retain): 120 | # Return the status field 121 | self.connackReceived(packet[0]) 122 | 123 | def _event_publish(self, packet, qos, dup, retain): 124 | # Extract the topic name 125 | topic = self._decodeString(packet) 126 | packet = packet[len(topic) + 2:] 127 | 128 | # Extract the message ID if appropriate 129 | messageId = None 130 | if qos > 0: 131 | messageId = self._decodeValue(packet[:2]) 132 | packet = packet[2:] 133 | 134 | # Extract the message 135 | # Whatever remains is the message 136 | message = str(packet) 137 | 138 | self.publishReceived(topic, message, qos, dup, retain, messageId) 139 | 140 | def _event_puback(self, packet, qos, dup, retain): 141 | # Extract the message ID 142 | messageId = self._decodeValue(packet[:2]) 143 | self.pubackReceived(messageId) 144 | 145 | def _event_pubrec(self, packet, qos, dup, retain): 146 | # Extract the message ID 147 | messageId = self._decodeValue(packet[:2]) 148 | self.pubrecReceived(messageId) 149 | 150 | def _event_pubrel(self, packet, qos, dup, retain): 151 | # Extract the message ID 152 | messageId = self._decodeValue(packet[:2]) 153 | self.pubrelReceived(messageId) 154 | 155 | def _event_pubcomp(self, packet, qos, dup, retain): 156 | # Extract the message ID 157 | messageId = self._decodeValue(packet[:2]) 158 | self.pubcompReceived(messageId) 159 | 160 | def _event_subscribe(self, packet, qos, dup, retain): 161 | # Extract the message ID 162 | messageId = self._decodeValue(packet[:2]) 163 | packet = packet[2:] 164 | 165 | # Extract the requested topics and their QoS levels 166 | topics = [] 167 | 168 | while len(packet): 169 | # Get the topic name 170 | topic = self._decodeString(packet) 171 | packet = packet[len(topic) + 2:] 172 | 173 | # Get the QoS level 174 | qos = packet[0] 175 | packet = packet[1:] 176 | 177 | # Add them to the list of (topic, qos)s 178 | topics.append((topic, qos)) 179 | 180 | self.subscribeReceived(topics, messageId) 181 | 182 | def _event_suback(self, packet, qos, dup, retain): 183 | # Extract the message ID 184 | messageId = self._decodeValue(packet[:2]) 185 | packet = packet[2:] 186 | 187 | # Extract the granted QoS levels 188 | grantedQos = [] 189 | 190 | while len(packet): 191 | grantedQos.append(packet[0]) 192 | packet = packet[1:] 193 | 194 | self.subackReceived(grantedQos, messageId) 195 | 196 | def _event_unsubscribe(self, packet, qos, dup, retain): 197 | # Extract the message ID 198 | messageId = self._decodeValue(packet[:2]) 199 | packet = packet[2:] 200 | 201 | # Extract the unsubscribing topics 202 | topics = [] 203 | 204 | while len(packet): 205 | # Get the topic name 206 | topic = self._decodeString(packet) 207 | packet = packet[len(topic) + 2:] 208 | 209 | # Add it to the list of topics 210 | topics.append(topic) 211 | 212 | self.unsubscribeReceived(topics, messageId) 213 | 214 | def _event_unsuback(self, packet, qos, dup, retain): 215 | # Extract the message ID 216 | messageId = self._decodeValue(packet[:2]) 217 | self.unsubackReceived(messageId) 218 | 219 | def _event_pingreq(self, packet, qos, dup, retain): 220 | self.pingreqReceived() 221 | 222 | def _event_pingresp(self, packet, qos, dup, retain): 223 | self.pingrespReceived() 224 | 225 | def _event_disconnect(self, packet, qos, dup, retain): 226 | self.disconnectReceived() 227 | 228 | def connectionMade(self): 229 | pass 230 | 231 | def connectionLost(self, reason): 232 | pass 233 | 234 | def connectReceived(self, clientID, keepalive, willTopic, 235 | willMessage, willQoS, willRetain, cleanStart): 236 | pass 237 | 238 | def connackReceived(self, status): 239 | pass 240 | 241 | def publishReceived(self, topic, message, qos=0, 242 | dup=False, retain=False, messageId=None): 243 | pass 244 | 245 | def pubackReceived(self, messageId): 246 | pass 247 | 248 | def pubrecReceived(self, messageId): 249 | pass 250 | 251 | def pubrelReceived(self, messageId): 252 | pass 253 | 254 | def pubcompReceived(self, messageId): 255 | pass 256 | 257 | def subscribeReceived(self, topics, messageId): 258 | pass 259 | 260 | def subackReceived(self, grantedQos, messageId): 261 | pass 262 | 263 | def unsubscribeReceived(self, topics, messageId): 264 | pass 265 | 266 | def unsubackReceived(self, messageId): 267 | pass 268 | 269 | def pingreqReceived(self): 270 | pass 271 | 272 | def pingrespReceived(self): 273 | pass 274 | 275 | def disconnectReceived(self): 276 | pass 277 | 278 | def connect(self, clientID, keepalive=3000, willTopic=None, 279 | willMessage=None, willQoS=0, willRetain=False, 280 | cleanStart=True): 281 | header = bytearray() 282 | varHeader = bytearray() 283 | payload = bytearray() 284 | 285 | varHeader.extend(self._encodeString("MQIsdp")) 286 | varHeader.append(3) 287 | 288 | if willMessage is None or willTopic is None: 289 | # Clean start, no will message 290 | varHeader.append(0 << 2 | cleanStart << 1) 291 | else: 292 | varHeader.append(willRetain << 5 | willQoS << 3 293 | | 1 << 2 | cleanStart << 1) 294 | 295 | varHeader.extend(self._encodeValue(keepalive/1000)) 296 | 297 | payload.extend(self._encodeString(clientID)) 298 | if willMessage is not None and willTopic is not None: 299 | payload.extend(self._encodeString(willTopic)) 300 | payload.extend(self._encodeString(willMessage)) 301 | 302 | header.append(0x01 << 4) 303 | header.extend(self._encodeLength(len(varHeader) + len(payload))) 304 | 305 | self.transport.write(str(header)) 306 | self.transport.write(str(varHeader)) 307 | self.transport.write(str(payload)) 308 | 309 | def connack(self, status): 310 | header = bytearray() 311 | payload = bytearray() 312 | 313 | header.append(0x02 << 4) 314 | payload.append(status) 315 | 316 | header.extend(self._encodeLength(len(payload))) 317 | self.transport.write(str(header)) 318 | self.transport.write(str(payload)) 319 | 320 | def publish(self, topic, message, qosLevel=0, retain=False, dup=False, 321 | messageId=None): 322 | 323 | header = bytearray() 324 | varHeader = bytearray() 325 | payload = bytearray() 326 | 327 | # Type = publish 328 | header.append(0x03 << 4 | dup << 3 | qosLevel << 1 | retain) 329 | 330 | varHeader.extend(self._encodeString(topic)) 331 | 332 | if qosLevel > 0: 333 | if messageId is not None: 334 | varHeader.extend(self._encodeValue(messageId)) 335 | else: 336 | varHeader.extend(self._encodeValue(random.randint(1, 0xFFFF))) 337 | 338 | payload.extend(message) 339 | 340 | header.extend(self._encodeLength(len(varHeader) + len(payload))) 341 | 342 | self.transport.write(str(header)) 343 | self.transport.write(str(varHeader)) 344 | self.transport.write(str(payload)) 345 | 346 | def puback(self, messageId): 347 | header = bytearray() 348 | varHeader = bytearray() 349 | 350 | header.append(0x04 << 4) 351 | varHeader.extend(self._encodeValue(messageId)) 352 | 353 | header.extend(self._encodeLength(len(varHeader))) 354 | 355 | self.transport.write(str(header)) 356 | self.transport.write(str(varHeader)) 357 | 358 | def pubrec(self, messageId): 359 | header = bytearray() 360 | varHeader = bytearray() 361 | 362 | header.append(0x05 << 4) 363 | varHeader.extend(self._encodeValue(messageId)) 364 | 365 | header.extend(self._encodeLength(len(varHeader))) 366 | 367 | self.transport.write(str(header)) 368 | self.transport.write(str(varHeader)) 369 | 370 | def pubrel(self, messageId): 371 | header = bytearray() 372 | varHeader = bytearray() 373 | 374 | header.append(0x06 << 4) 375 | varHeader.extend(self._encodeValue(messageId)) 376 | 377 | header.extend(self._encodeLength(len(varHeader))) 378 | 379 | self.transport.write(str(header)) 380 | self.transport.write(str(varHeader)) 381 | 382 | def pubcomp(self, messageId): 383 | header = bytearray() 384 | varHeader = bytearray() 385 | 386 | header.append(0x07 << 4) 387 | varHeader.extend(self._encodeValue(messageId)) 388 | 389 | header.extend(self._encodeLength(len(varHeader))) 390 | 391 | self.transport.write(str(header)) 392 | self.transport.write(str(varHeader)) 393 | 394 | def subscribe(self, topic, requestedQoS=0, messageId=None): 395 | """ 396 | Only supports QoS = 0 subscribes 397 | Only supports one subscription per message 398 | """ 399 | header = bytearray() 400 | varHeader = bytearray() 401 | payload = bytearray() 402 | 403 | # Type = subscribe, QoS = 1 404 | header.append(0x08 << 4 | 0x01 << 1) 405 | 406 | if messageId is None: 407 | varHeader.extend(self._encodeValue(random.randint(1, 0xFFFF))) 408 | else: 409 | varHeader.extend(self._encodeValue(messageId)) 410 | 411 | payload.extend(self._encodeString(topic)) 412 | payload.append(requestedQoS) 413 | 414 | header.extend(self._encodeLength(len(varHeader) + len(payload))) 415 | 416 | self.transport.write(str(header)) 417 | self.transport.write(str(varHeader)) 418 | self.transport.write(str(payload)) 419 | 420 | def suback(self, grantedQos, messageId): 421 | header = bytearray() 422 | varHeader = bytearray() 423 | payload = bytearray() 424 | 425 | header.append(0x09 << 4) 426 | varHeader.extend(self._encodeValue(messageId)) 427 | 428 | for i in grantedQos: 429 | payload.append(i) 430 | 431 | header.extend(self._encodeLength(len(varHeader) + len(payload))) 432 | 433 | self.transport.write(str(header)) 434 | self.transport.write(str(varHeader)) 435 | self.transport.write(str(payload)) 436 | 437 | def unsubscribe(self, topic, messageId=None): 438 | header = bytearray() 439 | varHeader = bytearray() 440 | payload = bytearray() 441 | 442 | header.append(0x0A << 4 | 0x01 << 1) 443 | 444 | if messageId is not None: 445 | varHeader.extend(self._encodeValue(self.messageID)) 446 | else: 447 | varHeader.extend(self._encodeValue(random.randint(1, 0xFFFF))) 448 | 449 | payload.extend(self._encodeString(topic)) 450 | 451 | header.extend(self._encodeLength(len(payload) + len(varHeader))) 452 | 453 | self.transport.write(str(header)) 454 | self.transport.write(str(varHeader)) 455 | self.transport.write(str(payload)) 456 | 457 | def unsuback(self, messageId): 458 | header = bytearray() 459 | varHeader = bytearray() 460 | 461 | header.append(0x0B << 4) 462 | varHeader.extend(self._encodeValue(messageId)) 463 | 464 | header.extend(self._encodeLength(len(varHeader))) 465 | 466 | self.transport.write(str(header)) 467 | self.transport.write(str(varHeader)) 468 | 469 | def pingreq(self): 470 | header = bytearray() 471 | header.append(0x0C << 4) 472 | header.extend(self._encodeLength(0)) 473 | 474 | self.transport.write(str(header)) 475 | 476 | def pingresp(self): 477 | header = bytearray() 478 | header.append(0x0D << 4) 479 | header.extend(self._encodeLength(0)) 480 | 481 | self.transport.write(str(header)) 482 | 483 | def disconnect(self): 484 | header = bytearray() 485 | header.append(0x0E << 4) 486 | header.extend(self._encodeLength(0)) 487 | 488 | self.transport.write(str(header)) 489 | 490 | def _encodeString(self, string): 491 | encoded = bytearray() 492 | encoded.append(len(string) >> 8) 493 | encoded.append(len(string) & 0xFF) 494 | for i in string: 495 | encoded.append(i) 496 | 497 | return encoded 498 | 499 | def _decodeString(self, encodedString): 500 | length = 256 * encodedString[0] + encodedString[1] 501 | return str(encodedString[2:2+length]) 502 | 503 | def _encodeLength(self, length): 504 | encoded = bytearray() 505 | while True: 506 | digit = length % 128 507 | length //= 128 508 | if length > 0: 509 | digit |= 128 510 | 511 | encoded.append(digit) 512 | if length <= 0: 513 | break 514 | 515 | return encoded 516 | 517 | def _encodeValue(self, value): 518 | encoded = bytearray() 519 | encoded.append(value >> 8) 520 | encoded.append(value & 0xFF) 521 | 522 | return encoded 523 | 524 | def _decodeLength(self, lengthArray): 525 | length = 0 526 | multiplier = 1 527 | for i in lengthArray: 528 | length += (i & 0x7F) * multiplier 529 | multiplier *= 0x80 530 | 531 | if (i & 0x80) != 0x80: 532 | break 533 | 534 | return length 535 | 536 | def _decodeValue(self, valueArray): 537 | value = 0 538 | multiplier = 1 539 | for i in valueArray[::-1]: 540 | value += i * multiplier 541 | multiplier = multiplier << 8 542 | 543 | return value 544 | 545 | 546 | class MQTTClient(MQTTProtocol): 547 | 548 | def __init__(self, clientId=None, keepalive=None, willQos=0, 549 | willTopic=None, willMessage=None, willRetain=False): 550 | 551 | if clientId is not None: 552 | self.clientId = clientId 553 | else: 554 | self.clientId = "Twisted%i" % random.randint(1, 0xFFFF) 555 | 556 | if keepalive is not None: 557 | self.keepalive = keepalive 558 | else: 559 | self.keepalive = 3000 560 | 561 | self.willQos = willQos 562 | self.willTopic = willTopic 563 | self.willMessage = willMessage 564 | self.willRetain = willRetain 565 | 566 | def connectionMade(self): 567 | self.connect(self.clientId, self.keepalive, self.willTopic, 568 | self.willMessage, self.willQos, self.willRetain, True) 569 | 570 | def connackReceived(self, status): 571 | if status == 0: 572 | self.mqttConnected() 573 | else: 574 | # Error 575 | pass 576 | 577 | def mqttConnected(self): 578 | pass 579 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | MQTT for Twisted Python 2 | 3 | Usage: 4 | Subclass MQTTProtocol and implement *received methods 5 | 6 | 7 | --------------------------------------------------------------------------------