├── .project ├── Documents └── MQTTWhatIs.pdf ├── LICENSE ├── README.md └── src ├── .properties └── MQTT ├── Boolean.extension.st ├── MQTTBadHeaderException.class.st ├── MQTTBadPacketTestConnect.class.st ├── MQTTBadPacketTestTestBadMsgIDConnect.class.st ├── MQTTBadPacketTypeException.class.st ├── MQTTBadTopicException.class.st ├── MQTTBrokerLookupFailure.class.st ├── MQTTCONNECTException.class.st ├── MQTTClientIDHolder.class.st ├── MQTTClientInterface.class.st ├── MQTTConnectionException.class.st ├── MQTTEmptyStreamErrorException.class.st ├── MQTTPacket.class.st ├── MQTTPacketAndPendingJobPair.class.st ├── MQTTPacketConnAck.class.st ├── MQTTPacketConnect.class.st ├── MQTTPacketDisconnect.class.st ├── MQTTPacketPingReq.class.st ├── MQTTPacketPingResp.class.st ├── MQTTPacketPubAck.class.st ├── MQTTPacketPubComp.class.st ├── MQTTPacketPubRec.class.st ├── MQTTPacketPubRel.class.st ├── MQTTPacketPublish.class.st ├── MQTTPacketSubAck.class.st ├── MQTTPacketSubscribe.class.st ├── MQTTPacketUnsubAck.class.st ├── MQTTPacketUnsubscribe.class.st ├── MQTTPacketVariableHeaded.class.st ├── MQTTPacketVariableHeadedWithPayload.class.st ├── MQTTPendingJob.class.st ├── MQTTPendingPingJob.class.st ├── MQTTPendingPubAckJob.class.st ├── MQTTPendingPubCompJob.class.st ├── MQTTPendingPubRecJob.class.st ├── MQTTPendingPubRelJob.class.st ├── MQTTPendingSubAckJob.class.st ├── MQTTPendingUnsubAckJob.class.st ├── MQTTServerInterface.class.st ├── MQTTSocketClient.class.st ├── MQTTSocketDaemon.class.st ├── MQTTSocketServer.class.st ├── MQTTStatistics.class.st ├── MQTTSubscription.class.st ├── MQTTTransportLayer.class.st ├── MQTTTransportLayerClient.class.st ├── MQTTTransportLayerServer.class.st ├── MQTTWriteStream.class.st ├── Object.extension.st ├── Socket.extension.st ├── UTF8Encoder.class.st └── package.st /.project: -------------------------------------------------------------------------------- 1 | { 2 | 'srcDirectory' : 'src' 3 | } 4 | -------------------------------------------------------------------------------- /Documents/MQTTWhatIs.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabWare/MQTT-broker/efdfabca40e6fd0c3157958ab2e005375743aebc/Documents/MQTTWhatIs.pdf -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | This class is based on work by 4 | Tim Rowledge for the MQTT Client he wrote for the Pi 5 | 6 | http://www.squeaksource.com/MQTTClient.html 7 | 8 | Modified by John M McIntosh, Corporate Smalltalk Consulting Ltd for LabWare Inc. 9 | 10 | Copyright 2017-2019 Tim Rowledge 11 | Copyright 2018, 2019. Corporate Smalltalk Consulting Ltd. 12 | Copyright 2018, 2019. LabWare Inc. 13 | 14 | Permission is hereby granted, free of charge, to any person obtaining a copy 15 | of this software and associated documentation files (the "Software"), to deal 16 | in the Software without restriction, including without limitation the rights 17 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 18 | copies of the Software, and to permit persons to whom the Software is 19 | furnished to do so, subject to the following conditions: 20 | 21 | The above copyright notice and this permission notice shall be included in all 22 | copies or substantial portions of the Software. 23 | 24 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 25 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 26 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 27 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 28 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 29 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 30 | SOFTWARE. 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MQTT-broker 2 | An implementation of a MQTT Client and a Broker in pure Smalltalk for Pharo 3 | 4 | This work is based on the work that Tim Rowledge wrote for the Pi 5 | The original code base is found at: 6 | 7 | http://www.squeaksource.com/MQTTClient.html 8 | 9 | What we did was refactor the logic into a client and server interface to support a full MQTT V311 data broker in pure Smalltalk. 10 | This product (in VSE) did pass various V311 data broker test units, after converting to Pharo we hope it still works. 11 | 12 | To start the server. 13 | (MQTTServerInterface openOnPort: 1883) start inspect. 14 | 15 | ``` 16 | To start a client. 17 | [(MQTTClientInterface openOnHostName: ‘192.168.1.139’ port: 1883 keepAlive: 300) start inspect] fork. 18 | or 19 | [(MQTTClientInterface openOnHostName: ‘test.mosquitto.org’ port: 1883 keepAlive: 300) start inspect] fork. 20 | ``` 21 | Stopping the server or the client. 22 | ``` 23 | MQTTServerInterface allInstances do: [ :e | e stop ]. 24 | MQTTClientInterface allInstances do: [ :e | e stop ]. 25 | ``` 26 | 27 | Helpful for figuring out what is running, or not, ensure you do a GC before using. 28 | ``` 29 | MQTTSocketClient allInstances inspect. 30 | MQTTClientInterface allInstances inspect. 31 | MQTTTransportLayerClient allInstances inspect. 32 | MQTTSocketServer allInstances inspect. 33 | MQTTServerInterface allInstances inspect. 34 | MQTTSocketDaemon allInstances inspect. 35 | MQTTTransportLayerServer allInstances inspect. 36 | ``` 37 | If you examine the class MQTTStatistics we do collect statistics on each server and client session. 38 | 39 | LOGGING is on 40 | To turn it off change the code found in MQTTCLientInterface class>>debugLog:tag:str2: 41 | (or: [true]) 42 | 43 | In MQTTCLientInterface 44 | there are methods testBlock and testSetupForTopic 45 | These can be alter to test for example a connection to test.mosquitto.org. 46 | At the moment they send QOS > 0 message info to the Transcript 47 | 48 | 49 | Things that the community could do? 50 | ``` 51 | Persistent store of the data broker queued data 52 | Ensure it works with Pharo 53 | Port to VA Smalltalk 54 | Port to other variations. 55 | Respecting userid & password in the Connect packet. 56 | If you need a VSE version please contact us. 57 | ``` 58 | 59 | -------------------------------------------------------------------------------- /src/.properties: -------------------------------------------------------------------------------- 1 | { 2 | #format : #tonel 3 | } -------------------------------------------------------------------------------- /src/MQTT/Boolean.extension.st: -------------------------------------------------------------------------------- 1 | Extension { #name : #Boolean } 2 | 3 | { #category : #'*MQTT' } 4 | Boolean >> isBoolean [ 5 | ^true 6 | ] 7 | -------------------------------------------------------------------------------- /src/MQTT/MQTTBadHeaderException.class.st: -------------------------------------------------------------------------------- 1 | " 2 | Exception for bad header data 3 | " 4 | Class { 5 | #name : #MQTTBadHeaderException, 6 | #superclass : #Error, 7 | #category : #MQTT 8 | } 9 | -------------------------------------------------------------------------------- /src/MQTT/MQTTBadPacketTestConnect.class.st: -------------------------------------------------------------------------------- 1 | " 2 | This was part of Tim Rowledge's testing, might not work now 3 | " 4 | Class { 5 | #name : #MQTTBadPacketTestConnect, 6 | #superclass : #MQTTPacketConnect, 7 | #category : #MQTT 8 | } 9 | 10 | { #category : #mqtt } 11 | MQTTBadPacketTestConnect >> encodeVariableHeaderOn: aWriteStream [ 12 | 13 | "write the CONNECT variable header (see 3.1.2) to the stream" 14 | 15 | self encodeString: 'MQTT' on: aWriteStream. 16 | aWriteStream 17 | nextPut: 255; 18 | nextPut: self connectFlags. 19 | self encode16BitInteger: keepAliveTime on: aWriteStream 20 | ] 21 | -------------------------------------------------------------------------------- /src/MQTT/MQTTBadPacketTestTestBadMsgIDConnect.class.st: -------------------------------------------------------------------------------- 1 | " 2 | This was part of Tim Rowledge's testing, might not work now 3 | " 4 | Class { 5 | #name : #MQTTBadPacketTestTestBadMsgIDConnect, 6 | #superclass : #MQTTPacketConnect, 7 | #category : #MQTT 8 | } 9 | 10 | { #category : #mqtt } 11 | MQTTBadPacketTestTestBadMsgIDConnect >> encodePayloadOn: aWriteStream [ 12 | 13 | "write my payload onto the stream- 14 | the client ID 15 | iff it exists, the will topic 16 | the will message 17 | iff it exists, the username 18 | iff it exists, the password" 19 | | bytes | 20 | 21 | bytes := ByteArray new: 2. 22 | bytes at: 1 put: 16rA0. 23 | bytes at: 2 put: 16rA1. 24 | self encode16BitInteger: bytes size on: aWriteStream. 25 | aWriteStream nextPutAll: bytes. 26 | self encodeWillDataOn: aWriteStream. 27 | self encodeUsernameOn: aWriteStream. 28 | self encodePasswordOn: aWriteStream 29 | ] 30 | -------------------------------------------------------------------------------- /src/MQTT/MQTTBadPacketTypeException.class.st: -------------------------------------------------------------------------------- 1 | " 2 | Exception for bad packet type 3 | " 4 | Class { 5 | #name : #MQTTBadPacketTypeException, 6 | #superclass : #Error, 7 | #category : #MQTT 8 | } 9 | -------------------------------------------------------------------------------- /src/MQTT/MQTTBadTopicException.class.st: -------------------------------------------------------------------------------- 1 | " 2 | Exception for bad topic data 3 | " 4 | Class { 5 | #name : #MQTTBadTopicException, 6 | #superclass : #Error, 7 | #category : #MQTT 8 | } 9 | -------------------------------------------------------------------------------- /src/MQTT/MQTTBrokerLookupFailure.class.st: -------------------------------------------------------------------------------- 1 | " 2 | Exception for broker lookup failure, current not used 3 | " 4 | Class { 5 | #name : #MQTTBrokerLookupFailure, 6 | #superclass : #Error, 7 | #instVars : [ 8 | 'brokerName' 9 | ], 10 | #category : #MQTT 11 | } 12 | 13 | { #category : #mqtt } 14 | MQTTBrokerLookupFailure class >> brokerName: aString [ 15 | 16 | ^super new brokerName: aString 17 | ] 18 | 19 | { #category : #mqtt } 20 | MQTTBrokerLookupFailure >> brokerName [ 21 | 22 | ^brokerName 23 | ] 24 | 25 | { #category : #mqtt } 26 | MQTTBrokerLookupFailure >> brokerName: aString [ 27 | 28 | brokerName := aString 29 | ] 30 | -------------------------------------------------------------------------------- /src/MQTT/MQTTCONNECTException.class.st: -------------------------------------------------------------------------------- 1 | " 2 | exception for connection failure 3 | " 4 | Class { 5 | #name : #MQTTCONNECTException, 6 | #superclass : #Error, 7 | #instVars : [ 8 | 'connectReturnCode' 9 | ], 10 | #category : #MQTT 11 | } 12 | 13 | { #category : #mqtt } 14 | MQTTCONNECTException class >> connectReturnCode: aByte [ 15 | 16 | ^(super new connectReturnCode: aByte) signal. 17 | ] 18 | 19 | { #category : #mqtt } 20 | MQTTCONNECTException >> connectReturnCode [ 21 | 22 | "Answer my 'connectReturnCode' instance variable." 23 | 24 | ^connectReturnCode 25 | ] 26 | 27 | { #category : #mqtt } 28 | MQTTCONNECTException >> connectReturnCode: aValue [ 29 | 30 | "Set my 'connectReturnCode' instance variable to aValue." 31 | 32 | connectReturnCode := aValue 33 | ] 34 | 35 | { #category : #mqtt } 36 | MQTTCONNECTException >> printOn: aStream [ 37 | 38 | "debug print" 39 | 40 | super printOn: aStream. 41 | aStream nextPutAll: ' returnCode: '. 42 | connectReturnCode printString printOn: aStream. 43 | ] 44 | -------------------------------------------------------------------------------- /src/MQTT/MQTTClientIDHolder.class.st: -------------------------------------------------------------------------------- 1 | " 2 | This class holds onto the client information for the data broker. It points to the current transport, the subscriptions, packets incoming and outgoing. 3 | " 4 | Class { 5 | #name : #MQTTClientIDHolder, 6 | #superclass : #Object, 7 | #instVars : [ 8 | 'clientID', 9 | 'transport', 10 | 'connectPacket', 11 | 'subscriptionsMutex', 12 | 'subscriptions', 13 | 'packetInFlightQueue', 14 | 'pendingJobs', 15 | 'outgoingPacketQueue', 16 | 'currentSubscriptions', 17 | 'lastMID' 18 | ], 19 | #category : #MQTT 20 | } 21 | 22 | { #category : #mqtt } 23 | MQTTClientIDHolder class >> with: aClientID transport: aTransport connectPacket: aConnectPacket [ 24 | 25 | "new" 26 | 27 | ^super new initializeWith: aClientID transport: aTransport connectPacket: aConnectPacket 28 | ] 29 | 30 | { #category : #mqtt } 31 | MQTTClientIDHolder >> = aClientID [ 32 | 33 | "equal" 34 | 35 | (aClientID class = self class) 36 | ifFalse:[^false]. 37 | ^clientID = aClientID clientID 38 | ] 39 | 40 | { #category : #mqtt } 41 | MQTTClientIDHolder >> addSubscription: aSubscriptionPacket [ 42 | 43 | "add the subscription" 44 | 45 | aSubscriptionPacket payloadDict associationsDo: [:assoc | 46 | | subscription dict|subscription := MQTTSubscription for: assoc key qos: assoc value do: [:x :y :z | ]. 47 | subscription ifNil: [^aSubscriptionPacket badTopicError]. 48 | subscriptionsMutex critical: [subscriptions at: assoc key put: subscription]. 49 | dict := Dictionary new. 50 | dict at: subscription topic put: subscription. 51 | self transport serverInterface forwardRetainMessagesTo: self usingPossibleSubscription: dict "[MQTT-3.8.4-3]" 52 | ]. 53 | ] 54 | 55 | { #category : #mqtt } 56 | MQTTClientIDHolder >> cleanSession: aBoolean withNewTransport: newTransport passingConnAck: aConnectAckPacket [ 57 | 58 | "clean session or not" 59 | 60 | aBoolean 61 | ifTrue:[ "[MQTT-3.1.2-6]" 62 | lastMID := nil. 63 | pendingJobs := nil. 64 | packetInFlightQueue := nil. 65 | outgoingPacketQueue := nil. 66 | currentSubscriptions := nil. 67 | connectPacket := nil. 68 | subscriptions := Dictionary new. 69 | newTransport sendPacket: aConnectAckPacket. 70 | ^self]. 71 | aBoolean 72 | ifFalse:[ "renew session [MQTT-3.1.2-4]. [MQTT-4.4.0-1]" 73 | | packet|outgoingPacketQueue ifNil: [^self]. 74 | newTransport currentSubscriptions: currentSubscriptions. 75 | newTransport lastMID: lastMID. 76 | pendingJobs ifNotNil: [pendingJobs do: [:p | p resetSendTime: -60]]. 77 | newTransport pendingJobs: pendingJobs. 78 | newTransport sendPacket: aConnectAckPacket. 79 | Processor yield. 80 | [packet := packetInFlightQueue nextOrNil. 81 | packet isNil not] 82 | whileTrue:[self sendPacket: packet onTransport: newTransport]. 83 | outgoingPacketQueue size > 0 84 | ifTrue:[ 85 | [packet := outgoingPacketQueue nextOrNil. 86 | packet isNil not] 87 | whileTrue:[self sendPacket: packet onTransport: newTransport]]. 88 | lastMID := nil. 89 | pendingJobs := nil. 90 | packetInFlightQueue := nil. 91 | outgoingPacketQueue := nil. 92 | currentSubscriptions := nil] 93 | ] 94 | 95 | { #category : #mqtt } 96 | MQTTClientIDHolder >> clientID [ 97 | 98 | "Answer my 'clientID' instance variable." 99 | 100 | ^clientID 101 | ] 102 | 103 | { #category : #mqtt } 104 | MQTTClientIDHolder >> clientID: aValue [ 105 | 106 | "Set my 'clientID' instance variable to aValue." 107 | 108 | clientID := aValue 109 | ] 110 | 111 | { #category : #mqtt } 112 | MQTTClientIDHolder >> connectPacket [ 113 | 114 | "Answer my 'connectPacket' instance variable." 115 | 116 | ^connectPacket 117 | ] 118 | 119 | { #category : #mqtt } 120 | MQTTClientIDHolder >> connectPacket: aValue [ 121 | 122 | "Set my 'connectPacket' instance variable to aValue." 123 | 124 | connectPacket := aValue 125 | ] 126 | 127 | { #category : #mqtt } 128 | MQTTClientIDHolder >> hash [ 129 | 130 | ^clientID hash 131 | ] 132 | 133 | { #category : #mqtt } 134 | MQTTClientIDHolder >> initialize [ 135 | 136 | "init" 137 | 138 | subscriptions := Dictionary new. 139 | subscriptionsMutex := Semaphore forMutualExclusion. 140 | ] 141 | 142 | { #category : #mqtt } 143 | MQTTClientIDHolder >> initializeWith: aClientID transport: aTransport connectPacket: aConnectPacket [ 144 | 145 | "init" 146 | 147 | subscriptions := Dictionary new. 148 | clientID := aClientID. 149 | transport := aTransport. 150 | connectPacket := aConnectPacket. 151 | subscriptionsMutex := Semaphore forMutualExclusion. 152 | ] 153 | 154 | { #category : #mqtt } 155 | MQTTClientIDHolder >> outgoingPacketQueue [ 156 | 157 | "Answer my 'outgoingPacketQueue' instance variable." 158 | 159 | ^outgoingPacketQueue 160 | ] 161 | 162 | { #category : #mqtt } 163 | MQTTClientIDHolder >> printOn: aStream [ 164 | 165 | "print useful data" 166 | | state | 167 | 168 | super printOn: aStream. 169 | aStream nextPutAll: ' '. 170 | clientID asString printOn: aStream. 171 | state := transport ifNotNil: [ 172 | transport abort 173 | ifTrue:['Aborting'] 174 | ifFalse:['Connected']] ifNil: ['Disconnected']. 175 | aStream nextPutAll: ' '. 176 | state asString printOn: aStream. 177 | ] 178 | 179 | { #category : #mqtt } 180 | MQTTClientIDHolder >> release [ 181 | 182 | "release cycles" 183 | 184 | super release. 185 | transport ifNil: [^self]. 186 | self transport release. 187 | packetInFlightQueue := transport packetInFlightQueue copy. 188 | outgoingPacketQueue := transport outgoingPacketQueue copy. 189 | lastMID := transport lastMID. 190 | pendingJobs := transport pendingJobs. 191 | currentSubscriptions := transport currentSubscriptions. 192 | transport := nil 193 | ] 194 | 195 | { #category : #mqtt } 196 | MQTTClientIDHolder >> removeSubscription: aUnsubscriptionPacket [ 197 | 198 | "remove the subscription" 199 | 200 | "[MQTT-3.10.4-1] [MQTT-3.10.4-2] [MQTT-3.10.4-3]" 201 | aUnsubscriptionPacket topics do: [:topic | "[MQTT-3.10.4-6]" 202 | | subscription|subscriptionsMutex critical: [subscriptions removeKey: topic ifAbsent: []]]. 203 | ] 204 | 205 | { #category : #mqtt } 206 | MQTTClientIDHolder >> sendPacket: packet onTransport: newTransport [ 207 | 208 | "ensure packet goes to correct location" 209 | 210 | packet class = MQTTPacketPublish 211 | ifTrue:[newTransport handlePublishResponse: packet] 212 | ifFalse:[newTransport sendPacket: packet] 213 | ] 214 | 215 | { #category : #mqtt } 216 | MQTTClientIDHolder >> subscriptions [ 217 | 218 | "Answer my 'subscriptions' instance variable." 219 | 220 | ^subscriptions 221 | ] 222 | 223 | { #category : #mqtt } 224 | MQTTClientIDHolder >> subscriptions: aValue [ 225 | 226 | "Set my 'subscriptions' instance variable to aValue." 227 | 228 | subscriptions := aValue 229 | ] 230 | 231 | { #category : #mqtt } 232 | MQTTClientIDHolder >> subscriptionsMutex [ 233 | 234 | "Answer my 'subscriptionsMutex' instance variable." 235 | 236 | ^subscriptionsMutex 237 | ] 238 | 239 | { #category : #mqtt } 240 | MQTTClientIDHolder >> transport [ 241 | 242 | "Answer my 'transport' instance variable." 243 | 244 | ^transport 245 | ] 246 | 247 | { #category : #mqtt } 248 | MQTTClientIDHolder >> transport: aValue [ 249 | 250 | "Set my 'transport' instance variable to aValue." 251 | 252 | transport := aValue 253 | ] 254 | -------------------------------------------------------------------------------- /src/MQTT/MQTTClientInterface.class.st: -------------------------------------------------------------------------------- 1 | " 2 | [(MQTTClientInterface openOnHostName: 'test.mosquitto.org' port: 1883 keepAlive: 300) start inspect] fork 3 | 4 | This class is based on work by 5 | Tim Rowledge for the MQTT Client he wrote for the Pi 6 | 7 | http://www.squeaksource.com/MQTTClient.html 8 | 9 | Modified by John M McIntosh, Corporate Smalltalk Consulting Ltd for LabWare Inc. 10 | Copyright 2017-2019 Tim Rowledge 11 | Copyright 2018, 2019. Corporate Smalltalk Consulting Ltd. 12 | Copyright 2018, 2019. LabWare Inc. 13 | 14 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ""Software""), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | " 20 | Class { 21 | #name : #MQTTClientInterface, 22 | #superclass : #Object, 23 | #instVars : [ 24 | 'socketClient', 25 | 'hostName', 26 | 'keepAliveTime' 27 | ], 28 | #category : #MQTT 29 | } 30 | 31 | { #category : #mqtt } 32 | MQTTClientInterface class >> debugLog: aString [ 33 | 34 | "debug" 35 | 36 | self debugLog: aString tag: ' CI ' str2: '' 37 | ] 38 | 39 | { #category : #mqtt } 40 | MQTTClientInterface class >> debugLog: aString tag: aTag str2: str2 [ 41 | 42 | "debug" 43 | | stream | 44 | 45 | (aTag = ' CI ' or: [ true ]) 46 | ifFalse:[^self]. 47 | stream := WriteStream on: (String new: 128). 48 | Time nowLocal print24: true showSeconds: true on: stream. 49 | stream 50 | nextPutAll: aTag; 51 | nextPutAll: aString; 52 | space; 53 | nextPutAll: str2. 54 | Transcript show: stream contents;cr. 55 | ] 56 | 57 | { #category : #mqtt } 58 | MQTTClientInterface class >> openOnHostName: hostname [ 59 | 60 | "open a client on host called hostname" 61 | 62 | ^self openOnHostName: hostname keepAlive: 0 63 | ] 64 | 65 | { #category : #mqtt } 66 | MQTTClientInterface class >> openOnHostName: hostname keepAlive: aSeconds [ 67 | 68 | "open a client on host called hostname" 69 | | instance minSeconds | 70 | 71 | instance := self new. 72 | minSeconds := aSeconds > 0 73 | ifTrue:[aSeconds max: 60] 74 | ifFalse:[0]. 75 | ^instance openOnHostName: hostname keepAlive: minSeconds. 76 | ] 77 | 78 | { #category : #mqtt } 79 | MQTTClientInterface class >> openOnHostName: hostname port: portNumber keepAlive: aSeconds [ 80 | 81 | "open a client on host called hostname and port" 82 | | instance minSeconds | 83 | 84 | instance := self new. 85 | minSeconds := aSeconds > 0 86 | ifTrue:[aSeconds max: 60] 87 | ifFalse:[0]. 88 | ^instance openOnHostName: hostname port: portNumber keepAlive: minSeconds 89 | ] 90 | 91 | { #category : #mqtt } 92 | MQTTClientInterface >> debugLog: aString [ 93 | 94 | "debug" 95 | 96 | self class debugLog: aString 97 | ] 98 | 99 | { #category : #mqtt } 100 | MQTTClientInterface >> disconnect [ 101 | 102 | "disconnect" 103 | 104 | self socketClient disconnect. "[MQTT-3.14.4-1]" 105 | ] 106 | 107 | { #category : #mqtt } 108 | MQTTClientInterface >> hostName [ 109 | 110 | "get my 'hostName' instance variable " 111 | 112 | ^hostName 113 | ] 114 | 115 | { #category : #mqtt } 116 | MQTTClientInterface >> hostName: aValue [ 117 | 118 | "Set my 'hostName' instance variable to aValue." 119 | 120 | hostName := aValue 121 | ] 122 | 123 | { #category : #mqtt } 124 | MQTTClientInterface >> isValid [ 125 | 126 | ^(socketClient notNil and: [socketClient transport notNil]) and: [socketClient transport abort not] 127 | ] 128 | 129 | { #category : #mqtt } 130 | MQTTClientInterface >> keepAliveTime [ 131 | 132 | "Answer my 'keepAliveTime' instance variable." 133 | 134 | ^keepAliveTime ifNil: [keepAliveTime := 0] 135 | ] 136 | 137 | { #category : #mqtt } 138 | MQTTClientInterface >> keepAliveTime: aValue [ 139 | 140 | "Set my 'keepAliveTime' instance variable to aValue." 141 | 142 | keepAliveTime := aValue 143 | ] 144 | 145 | { #category : #mqtt } 146 | MQTTClientInterface >> openOnHostName: aHostName [ 147 | 148 | "open on the hostName" 149 | 150 | self hostName: aHostName. 151 | self socketClient: ( 152 | self socketClientClass openOnHostName: aHostName keepAlive: keepAliveTime interface: self). 153 | ^self 154 | ] 155 | 156 | { #category : #mqtt } 157 | MQTTClientInterface >> openOnHostName: aHostName keepAlive: aSeconds [ 158 | 159 | "open on the hostName" 160 | 161 | self hostName: aHostName. 162 | self keepAliveTime: aSeconds. 163 | self socketClient: ( 164 | self socketClientClass openOnHostName: aHostName keepAlive: aSeconds interface: self). 165 | ^self 166 | ] 167 | 168 | { #category : #mqtt } 169 | MQTTClientInterface >> openOnHostName: aHostName port: portNumber keepAlive: aSeconds [ 170 | 171 | "open on the hostName and port" 172 | 173 | self hostName: aHostName. 174 | self keepAliveTime: aSeconds. 175 | self socketClient: ( 176 | self socketClientClass openOnHostName: aHostName port: portNumber keepAlive: aSeconds interface: self). 177 | ^self 178 | ] 179 | 180 | { #category : #mqtt } 181 | MQTTClientInterface >> printOn: aStream [ 182 | 183 | "print useful data" 184 | | state | 185 | 186 | super printOn: aStream. 187 | aStream nextPutAll: ' '. 188 | hostName asString printOn: aStream. 189 | state := (socketClient notNil and: [socketClient transport notNil]) 190 | ifTrue:[ 191 | socketClient transport abort 192 | ifTrue:['Aborting'] 193 | ifFalse:['Connected']] 194 | ifFalse:['Disconnected?']. 195 | aStream nextPutAll: ' '. 196 | state asString printOn: aStream. 197 | ] 198 | 199 | { #category : #mqtt } 200 | MQTTClientInterface >> publish: aByteArray onTopic: topicString qos: qos [ 201 | 202 | "publish the aByteArray to the connected broker." 203 | 204 | self publish: aByteArray onTopic: topicString qos: qos retain: false 205 | ] 206 | 207 | { #category : #mqtt } 208 | MQTTClientInterface >> publish: aByteArray onTopic: topicString qos: qos retain: retainFlag [ 209 | 210 | "publish the aByteArray to the connected broker. If qos > 0 we'll need to schedule a pending job for the ack sequence(s)" 211 | 212 | self socketClient publishTopic: topicString message: aByteArray qos: qos retain: retainFlag 213 | ] 214 | 215 | { #category : #mqtt } 216 | MQTTClientInterface >> readWaitTime: aSeconds [ 217 | 218 | "set read time out" 219 | 220 | self socketClient readWaitTime: aSeconds 221 | ] 222 | 223 | { #category : #mqtt } 224 | MQTTClientInterface >> restart [ 225 | 226 | "restart logic" 227 | 228 | (Delay forSeconds: 15) wait. 229 | self debugLog: 'MQTTClientInterface restarting possible existing session'. 230 | ^(self openOnHostName: self hostName) start. 231 | ] 232 | 233 | { #category : #mqtt } 234 | MQTTClientInterface >> socketClient [ 235 | 236 | "Answer my 'socketClient' instance variable." 237 | 238 | ^socketClient 239 | ] 240 | 241 | { #category : #mqtt } 242 | MQTTClientInterface >> socketClient: aValue [ 243 | 244 | "Set my 'socketClient' instance variable to aValue." 245 | 246 | socketClient := aValue 247 | ] 248 | 249 | { #category : #mqtt } 250 | MQTTClientInterface >> socketClientClass [ 251 | 252 | "SocketClient to use" 253 | 254 | ^MQTTSocketClient 255 | ] 256 | 257 | { #category : #mqtt } 258 | MQTTClientInterface >> start [ 259 | 260 | "Start actual socket connection" 261 | ] 262 | 263 | { #category : #mqtt } 264 | MQTTClientInterface >> stop [ 265 | 266 | "disconnect" 267 | 268 | self disconnect 269 | ] 270 | 271 | { #category : #mqtt } 272 | MQTTClientInterface >> subscribeTo: topicString qos: qos do: aBlock [ 273 | 274 | "a basic subscribe and do something message. We must check the topicString's acceptability and fail if there are issues" 275 | 276 | self socketClient onTopic: topicString qos: qos do: aBlock 277 | ] 278 | 279 | { #category : #mqtt } 280 | MQTTClientInterface >> testBlock [ 281 | 282 | "test block" 283 | 284 | ^[:t :m :q | 285 | q > 0 ifTrue: 286 | [Transcript 287 | show: 'topic: ' , t; 288 | space; 289 | show: 'qos: ' , q printString; 290 | space; 291 | show: 'payload: ' , m size printString; 292 | cr]] 293 | ] 294 | 295 | { #category : #mqtt } 296 | MQTTClientInterface >> testSetupForTopic [ 297 | 298 | "test procedure" 299 | 300 | self subscribeTo: '#' qos: 2 do: self testBlock. 301 | ] 302 | 303 | { #category : #mqtt } 304 | MQTTClientInterface >> unsubscribeFrom: aTopic [ 305 | 306 | "unsubscribe from aTopic - remove the subscription from currentSubscription" 307 | 308 | self socketClient unsubscribeFrom: aTopic 309 | ] 310 | 311 | { #category : #mqtt } 312 | MQTTClientInterface >> username: uName password: pwd [ 313 | 314 | "set username and password" 315 | 316 | self socketClient username: uName password: pwd 317 | ] 318 | 319 | { #category : #mqtt } 320 | MQTTClientInterface >> willTopic: topicString message: messageString retain: retainBoolean qos: qosValue [ 321 | 322 | "Set will Information" 323 | 324 | self socketClient willTopic: topicString message: messageString retain: retainBoolean qos: qosValue 325 | ] 326 | -------------------------------------------------------------------------------- /src/MQTT/MQTTConnectionException.class.st: -------------------------------------------------------------------------------- 1 | " 2 | exception for connection failure 3 | " 4 | Class { 5 | #name : #MQTTConnectionException, 6 | #superclass : #Error, 7 | #category : #MQTT 8 | } 9 | -------------------------------------------------------------------------------- /src/MQTT/MQTTEmptyStreamErrorException.class.st: -------------------------------------------------------------------------------- 1 | " 2 | exception for emptry stream failure. This might not be a fatal error, the reader will try again. 3 | " 4 | Class { 5 | #name : #MQTTEmptyStreamErrorException, 6 | #superclass : #Error, 7 | #category : #MQTT 8 | } 9 | -------------------------------------------------------------------------------- /src/MQTT/MQTTPacket.class.st: -------------------------------------------------------------------------------- 1 | " 2 | Abstract class for different MQTT packets. 3 | " 4 | Class { 5 | #name : #MQTTPacket, 6 | #superclass : #Object, 7 | #classVars : [ 8 | 'MQTTProtocolLevel311', 9 | 'PacketTypes' 10 | ], 11 | #category : #MQTT 12 | } 13 | 14 | { #category : #mqtt } 15 | MQTTPacket class >> badPacketTypeError [ 16 | 17 | "raise an exception at some point; for now just halt" 18 | 19 | self debugLog: 'badPacketTypeError'. 20 | MQTTBadPacketTypeException signal. 21 | ] 22 | 23 | { #category : #mqtt } 24 | MQTTPacket class >> commentOriginal [ 25 | 26 | "MQTTPacket is the root class for specific varieties of MQTT packet. It only provides the outline of the fixed header etc. Subclasses provide the packets that have only the fixed header - 27 | DISCONNECT 28 | PINGREQ 29 | PINGRESP 30 | 31 | Others provide the sub-hierarchies for packets with variable headers and payloads. 32 | 33 | See the comment for MQTTClient for usage details" 34 | 35 | ] 36 | 37 | { #category : #mqtt } 38 | MQTTPacket class >> debugLog: aString [ 39 | 40 | "debug" 41 | 42 | MQTTClientInterface debugLog: aString tag: ' PP ' str2: '' 43 | ] 44 | 45 | { #category : #mqtt } 46 | MQTTPacket class >> emptyStreamError [ 47 | 48 | "raise an exception at some point; for now just halt" 49 | 50 | ^MQTTEmptyStreamErrorException signal: 'MQTT no bytes available to be read' 51 | ] 52 | 53 | { #category : #mqtt } 54 | MQTTPacket class >> initialize [ 55 | 56 | "set up the packet type list etc MQTTPacket initialize" 57 | 58 | "self initialize" 59 | PacketTypes := OrderedCollection new add: MQTTPacketConnect; add: MQTTPacketConnAck; add: MQTTPacketPublish; add: MQTTPacketPubAck; add: MQTTPacketPubRec; add: MQTTPacketPubRel; add: MQTTPacketPubComp; add: MQTTPacketSubscribe; add: MQTTPacketSubAck; add: MQTTPacketUnsubscribe; add: MQTTPacketUnsubAck; add: MQTTPacketPingReq; add: MQTTPacketPingResp; add: MQTTPacketDisconnect; yourself. 60 | MQTTProtocolLevel311 := 4 "encoding for CONNECT etc" 61 | ] 62 | 63 | { #category : #mqtt } 64 | MQTTPacket class >> readFrom: aReadStream [ 65 | 66 | "read the stream to work out which kind of packet it contains, create a suitable instance and get that to read the rest of the data" 67 | | byte | 68 | 69 | byte := [aReadStream peek] on: ConnectionTimedOut do: [:ex | self emptyStreamError]. 70 | byte ifNil: [^self emptyStreamError]. 71 | ^(PacketTypes at: byte >> 4 ifAbsent: [self badPacketTypeError]) new readFrom: aReadStream 72 | ] 73 | 74 | { #category : #mqtt } 75 | MQTTPacket >> badConnectAcknowledgeError [ 76 | 77 | "raise an exception at some point; for now just halt" 78 | 79 | self debugLog: 'badConnectAcknowledgeError'. 80 | MQTTConnectionException signal: 'badConnectAcknowledgeError'. 81 | ] 82 | 83 | { #category : #mqtt } 84 | MQTTPacket >> badFixedHeaderError [ 85 | 86 | "raise an exception at some point; for now just halt" 87 | 88 | self debugLog: 'badFixedHeaderError'. 89 | ^MQTTBadHeaderException signal 90 | ] 91 | 92 | { #category : #mqtt } 93 | MQTTPacket >> badQosError [ 94 | 95 | "raise an exception at some point; for now just halt" 96 | 97 | self debugLog: 'badQosError'. 98 | MQTTBadHeaderException signal: 'badQosError'. 99 | ] 100 | 101 | { #category : #mqtt } 102 | MQTTPacket >> badTopicError [ 103 | 104 | "raise an exception at some point; for now just halt" 105 | 106 | self debugLog: 'badTopicError'. 107 | MQTTBadTopicException signal: 'badTopicError'. 108 | ] 109 | 110 | { #category : #mqtt } 111 | MQTTPacket >> badTopicListError [ 112 | 113 | "raise an exception at some point; for now just halt" 114 | 115 | self debugLog: 'badTopicListError'. 116 | MQTTBadTopicException signal: 'badTopicListError'. 117 | ] 118 | 119 | { #category : #mqtt } 120 | MQTTPacket >> connectionRefusedError: refusalCode [ 121 | 122 | "raise an exception at some point; for now just halt" 123 | 124 | self debugLog: 'connectionRefusedError:'. 125 | ^MQTTCONNECTException connectReturnCode: refusalCode. 126 | ] 127 | 128 | { #category : #mqtt } 129 | MQTTPacket >> debugLog: aString [ 130 | 131 | "debug" 132 | 133 | self class debugLog: aString 134 | ] 135 | 136 | { #category : #mqtt } 137 | MQTTPacket >> decode16BitIntegerFrom: aReadStream [ 138 | 139 | "read a 16bit number MSB format from the stream" 140 | 141 | ^aReadStream next << 8 + aReadStream next 142 | ] 143 | 144 | { #category : #mqtt } 145 | MQTTPacket >> decodeFixedHeaderFrom: aReadStream [ 146 | 147 | "read the fixed header from the stream and check it for decent state; the only class needing to do anything clever is MQTTPacketPublish" 148 | | token | 149 | 150 | token := aReadStream next. 151 | token = self fixedHeader 152 | ifFalse:[self badFixedHeaderError]. 153 | self decodeLengthFrom: aReadStream 154 | ] 155 | 156 | { #category : #mqtt } 157 | MQTTPacket >> decodeFrom: aReadStream [ 158 | 159 | "decode me from the stream. I only have a fixed header" 160 | 161 | self decodeFixedHeaderFrom: aReadStream 162 | ] 163 | 164 | { #category : #mqtt } 165 | MQTTPacket >> decodeLengthFrom: aReadStream [ 166 | 167 | "the default for fixed header only packets is 0 but we have to pull the next byte from the stream to keep the input buffer balanced" 168 | | byte | 169 | 170 | byte := aReadStream next. 171 | byte > 0 172 | ifTrue:[self encodedLengthError] 173 | ] 174 | 175 | { #category : #mqtt } 176 | MQTTPacket >> encode16BitInteger: smallNumber on: aWriteStream [ 177 | 178 | "write a 16bit number MSB format on the stream" 179 | 180 | aWriteStream 181 | nextPut: (smallNumber >> 8); 182 | nextPut: (smallNumber bitAnd: 16rFF) 183 | ] 184 | 185 | { #category : #mqtt } 186 | MQTTPacket >> encodeOn: aWriteStream [ 187 | 188 | "basic encoding of my data onto a stream (usually a socket) to transmit. I only have a fixed header. 189 | The fixed header only classes are all 2 bytes in size, giving a remaining length of 0. 190 | See MQTTPacketVariableHeaded>>#encodeOn: for the more complex arrangements for variable size packets" 191 | 192 | aWriteStream 193 | nextPut: self fixedHeader; 194 | nextPut: 0; 195 | flush. 196 | ^aWriteStream 197 | ] 198 | 199 | { #category : #mqtt } 200 | MQTTPacket >> encodeString: aString on: aWriteStream [ 201 | 202 | "generic write of string to the strea,; convert to UTF8, prepend with the 16 bit MSB length of said utf8 string" 203 | | bytes | 204 | 205 | bytes := UTF8Encoder encode: aString asString. 206 | self encode16BitInteger: bytes size on: aWriteStream. 207 | aWriteStream nextPutAll: bytes 208 | ] 209 | 210 | { #category : #mqtt } 211 | MQTTPacket >> encodedLengthError [ 212 | 213 | "raise an exception at some point; for now just halt" 214 | 215 | self debugLog: 'encodedLengthError'. 216 | MQTTBadHeaderException signal: 'encodedLengthError'. 217 | ] 218 | 219 | { #category : #mqtt } 220 | MQTTPacket >> evaluateFor: anMQTTCLient [ 221 | 222 | "I've been received by the client so now is the time to come to the aid of the party" 223 | 224 | ^self subclassResponsibility 225 | ] 226 | 227 | { #category : #mqtt } 228 | MQTTPacket >> fixedHeader [ 229 | 230 | "return the byte for the fixed header; 231 | this is the packet type 4 bit value << 4 | the flags particular to the type. 232 | For almost all kinds of packet this is a fixed number. " 233 | 234 | ^self packetType << 4 bitOr: self fixedHeaderFlags 235 | ] 236 | 237 | { #category : #mqtt } 238 | MQTTPacket >> fixedHeaderFlags [ 239 | 240 | "default for most classes is 0" 241 | 242 | ^0 243 | ] 244 | 245 | { #category : #mqtt } 246 | MQTTPacket >> packetType [ 247 | ^nil 248 | ] 249 | 250 | { #category : #mqtt } 251 | MQTTPacket >> readFrom: aReadStream [ 252 | 253 | self decodeFrom: aReadStream 254 | ] 255 | -------------------------------------------------------------------------------- /src/MQTT/MQTTPacketAndPendingJobPair.class.st: -------------------------------------------------------------------------------- 1 | " 2 | The pending work after a packet is sent. 3 | 4 | When a packet is sent, and there is oh say a SubAck to be expected in the future we queue up a pending job pair for a future time. The time first is set to the far future, then once the packet is sent on the wire, we look for a response N seconds out. If that response fails we do the recovery logic which for a SubAck would be to resend the Subscribe Packet. 5 | 6 | The reason to wait for the transmission on the wire is that a Publish with 100MB of data might take minutes/hours to transmit, so obviously we don't want to wait 90 seconds for a response to our QOS 1 or 2 logic 7 | " 8 | Class { 9 | #name : #MQTTPacketAndPendingJobPair, 10 | #superclass : #Object, 11 | #instVars : [ 12 | 'packet', 13 | 'pendingJob' 14 | ], 15 | #category : #MQTT 16 | } 17 | 18 | { #category : #mqtt } 19 | MQTTPacketAndPendingJobPair class >> packet: aPacket pendingJob: aPendingJob [ 20 | 21 | "make pair" 22 | | instance | 23 | 24 | instance := self new. 25 | instance packet: aPacket. 26 | instance pendingJob: aPendingJob. 27 | ^instance 28 | ] 29 | 30 | { #category : #mqtt } 31 | MQTTPacketAndPendingJobPair >> encodeOn: sockStrm [ 32 | 33 | "encode and reset time" 34 | | ret | 35 | 36 | ret := self packet encodeOn: sockStrm. 37 | pendingJob ifNotNil: [pendingJob resetSendTime: 20]. 38 | ^ret 39 | ] 40 | 41 | { #category : #mqtt } 42 | MQTTPacketAndPendingJobPair >> packet [ 43 | 44 | "Answer my 'packet' instance variable." 45 | 46 | ^packet 47 | ] 48 | 49 | { #category : #mqtt } 50 | MQTTPacketAndPendingJobPair >> packet: aValue [ 51 | 52 | "Set my 'packet' instance variable to aValue." 53 | 54 | packet := aValue 55 | ] 56 | 57 | { #category : #mqtt } 58 | MQTTPacketAndPendingJobPair >> pendingJob [ 59 | 60 | "Answer my 'pendingJob' instance variable." 61 | 62 | ^pendingJob 63 | ] 64 | 65 | { #category : #mqtt } 66 | MQTTPacketAndPendingJobPair >> pendingJob: aValue [ 67 | 68 | "Set my 'pendingJob' instance variable to aValue." 69 | 70 | pendingJob := aValue 71 | ] 72 | 73 | { #category : #mqtt } 74 | MQTTPacketAndPendingJobPair >> printOn: aStream [ 75 | 76 | "print useful data" 77 | 78 | super printOn: aStream. 79 | aStream nextPutAll: ' '. 80 | packet asString printOn: aStream. 81 | aStream nextPutAll: ' '. 82 | pendingJob asString printOn: aStream. 83 | ] 84 | -------------------------------------------------------------------------------- /src/MQTT/MQTTPacketConnAck.class.st: -------------------------------------------------------------------------------- 1 | " 2 | The Connection ACK packet 3 | " 4 | Class { 5 | #name : #MQTTPacketConnAck, 6 | #superclass : #MQTTPacketVariableHeaded, 7 | #instVars : [ 8 | 'sessionPresent', 9 | 'byte1', 10 | 'byte2' 11 | ], 12 | #category : #MQTT 13 | } 14 | 15 | { #category : #mqtt } 16 | MQTTPacketConnAck class >> commentOriginal [ 17 | 18 | "An MQTTPacketConnAck is returned by the server after we send a CONNECT, per MQTT3.1.1 doc. 19 | We only receive these and a CONNACK is supposed to be the first packet sent from the serer after a CONNECT. (3.2). 20 | 21 | The variable header has no msgID field but instead 2 bytes containing 22 | a) connect acknowledge flags - (3.2.2.1) - currently just the lowest bit of the byte representing the Session Present flag 23 | b) connect return code (3.2.2.3)- an accept (\0) or failure code (\1-\5 see table 3.1) 24 | 25 | There is no payload 26 | " 27 | 28 | ] 29 | 30 | { #category : #mqtt } 31 | MQTTPacketConnAck >> byte1 [ 32 | 33 | "Answer my 'byte1' instance variable." 34 | 35 | ^byte1 36 | ] 37 | 38 | { #category : #mqtt } 39 | MQTTPacketConnAck >> byte2 [ 40 | 41 | "Answer my 'byte2' instance variable." 42 | 43 | ^byte2 44 | ] 45 | 46 | { #category : #mqtt } 47 | MQTTPacketConnAck >> byte2: aValue [ 48 | 49 | "Set my 'byte2' instance variable to aValue." 50 | 51 | byte2 := aValue 52 | ] 53 | 54 | { #category : #mqtt } 55 | MQTTPacketConnAck >> decodeVariableHeaderFrom: aReadStream [ 56 | 57 | "I don't use the msgID stuff; my variable header contains a connect flag and the acknowledge flags per section 3.2.2 of the spec" 58 | 59 | byte1 := aReadStream next. 60 | (byte1 anyMask: 16rFE) 61 | ifTrue:[^self badConnectAcknowledgeError]. 62 | sessionPresent := byte1 allMask: 1. 63 | byte2 := aReadStream next. 64 | ] 65 | 66 | { #category : #mqtt } 67 | MQTTPacketConnAck >> encodeVariableHeaderOn: aWriteStream [ 68 | 69 | "write the connect acknowledge flags and return code to the stream" 70 | 71 | byte1 := sessionPresent asBit. 72 | aWriteStream 73 | nextPut: sessionPresent asBit; 74 | nextPut: byte2 75 | ] 76 | 77 | { #category : #mqtt } 78 | MQTTPacketConnAck >> evaluateFor: anMQTTClient [ 79 | 80 | "The broker has responded to a CONNECT from me" 81 | 82 | anMQTTClient handleConnAckPacket: self 83 | ] 84 | 85 | { #category : #mqtt } 86 | MQTTPacketConnAck >> packetType [ 87 | 88 | ^2 89 | ] 90 | 91 | { #category : #mqtt } 92 | MQTTPacketConnAck >> printOn: aStream [ 93 | 94 | "debug print" 95 | | byteToPrint | 96 | 97 | super printOn: aStream. 98 | aStream nextPutAll: ' session present: '. 99 | sessionPresent printOn: aStream. 100 | byteToPrint := byte1 ifNil: ['nil'] ifNotNil: [ 101 | byte1 asInteger printPaddedWith: ($0) to: 2 base: 16]. 102 | aStream nextPutAll: ' byte1: '. 103 | byteToPrint printOn: aStream. 104 | byteToPrint := byte2 ifNil: ['nil'] ifNotNil: [ 105 | byte2 asInteger printPaddedWith: ($0) to: 2 base: 16]. 106 | aStream nextPutAll: ' byte2: '. 107 | byteToPrint printOn: aStream. 108 | ] 109 | 110 | { #category : #mqtt } 111 | MQTTPacketConnAck >> sessionPresent [ 112 | 113 | "after connecting the client should check that this is ok; a false value implies the server doesn't have any retained state from a prior connection. This may be important to the client" 114 | 115 | ^sessionPresent 116 | ] 117 | 118 | { #category : #mqtt } 119 | MQTTPacketConnAck >> sessionPresent: aValue [ 120 | 121 | "Set my 'sessionPresent' instance variable to aValue." 122 | 123 | sessionPresent := aValue 124 | ] 125 | 126 | { #category : #mqtt } 127 | MQTTPacketConnAck >> testSessionPresent: boolean [ 128 | 129 | sessionPresent := boolean 130 | ] 131 | -------------------------------------------------------------------------------- /src/MQTT/MQTTPacketConnect.class.st: -------------------------------------------------------------------------------- 1 | " 2 | The Connect packet 3 | " 4 | Class { 5 | #name : #MQTTPacketConnect, 6 | #superclass : #MQTTPacketVariableHeadedWithPayload, 7 | #instVars : [ 8 | 'user', 9 | 'password', 10 | 'keepAliveTime', 11 | 'willMessage', 12 | 'willTopic', 13 | 'willQos', 14 | 'willFlag', 15 | 'willRetainFlag', 16 | 'cleanSessionFlag', 17 | 'clientID', 18 | 'returnCode' 19 | ], 20 | #category : #MQTT 21 | } 22 | 23 | { #category : #mqtt } 24 | MQTTPacketConnect class >> commentOriginal [ 25 | 26 | "An MQTTPacketConnect contains all the data to make a CONNECT request per section 3.1 of the spec. 27 | We only send these; the server responds with a CONNACK. 28 | 29 | The variable header contains 4 fields to describe 30 | a) protocol name (3.1.2.1) - a fixed 6 bytes of \0\4MQTT 31 | b) protocol level (3.1.2.2) - currently a fixed value of \4 to signify MQTT 3.1.1 with an older version 3.1 using \3. If the returned CONNACK packer has a code of \1 it means the server does not understand the requested protocol version 32 | c)connect flags (3.1.2.3) - a single byte encoding 33 | userFlag | passwordFlag | willRetainedFlag | willQos (2 bits) | willFlag | cleanSessionFlag | reserverved to \0 34 | d) keep alive time (3.1.2.10) - 2 bytes msb-first time in secondsipr\ 35 | 36 | Following the headers there is a payload containing 37 | a) a client ID (3.1.3.1) 38 | b) an optional Will topic (3.1.3.2) and WIll message (3.1.3.3) 39 | c) an optional user name (3.1.3.4) 40 | d) an optional password (3.1.3.5) 41 | 42 | We expect to get a MQTTPacketConnAck back; only one will be sent and we need no matching id to test. Sending a second connect request is supposed to make the server close the connection. (3.1) 43 | " 44 | 45 | ] 46 | 47 | { #category : #mqtt } 48 | MQTTPacketConnect >> cleanSession: sessionBoolean [ 49 | 50 | "set the clean session flag; default is true" 51 | 52 | cleanSessionFlag := sessionBoolean 53 | ] 54 | 55 | { #category : #mqtt } 56 | MQTTPacketConnect >> cleanSessionFlag [ 57 | 58 | "Answer my 'cleanSessionFlag' instance variable." 59 | 60 | ^cleanSessionFlag 61 | ] 62 | 63 | { #category : #mqtt } 64 | MQTTPacketConnect >> clientID: clientIDString [ 65 | 66 | "set the id that the server will use to identify the session being established by this connect packet. It ought to be unique within the scope of this image, ideally totally unique. That's up to the user of this class though" 67 | 68 | clientID := clientIDString 69 | ] 70 | 71 | { #category : #mqtt } 72 | MQTTPacketConnect >> clientIDString [ 73 | 74 | "ought to come from the client but if it hasn't been set use a vaguely useful default" 75 | 76 | ^clientID ifNil: ['SqueakMQTT'] 77 | ] 78 | 79 | { #category : #mqtt } 80 | MQTTPacketConnect >> connectFlags [ 81 | 82 | | byte | 83 | 84 | byte := willQos << 3. 85 | byte := byte bitAt: 2 put: cleanSessionFlag asBit. 86 | byte := byte bitAt: 3 put: willTopic notNil asBit. 87 | byte := byte bitAt: 6 put: willRetainFlag asBit. 88 | byte := byte bitAt: 7 put: password notNil asBit. 89 | byte := byte bitAt: 8 put: user notNil asBit. 90 | ^byte 91 | ] 92 | 93 | { #category : #mqtt } 94 | MQTTPacketConnect >> decodePayloadFrom: aReadStream [ 95 | 96 | "first read the CONNECT variable header (see 3.1.2) from the stream and then the actualy payload - purely for comparison and test purposes since we only send CONNECT packets. We do this odd order because the variable header includes flags that directly affect how the payload is interpreted" 97 | 98 | "first the 16 bit length of the protocol name" 99 | | cFlags passwordFlag userFlag protocol version | 100 | 101 | "[MQTT-3.1.4-1]" 102 | protocol := self decodeStringFrom: aReadStream. 103 | protocol = 'MQTT' 104 | ifFalse:[ 105 | self debugLog: 'Protocol found: ' , protocol. 106 | (self connectionRefusedError: 16r01) signal]. " [MQTT-3.1.2-1]. ignore" 107 | version := aReadStream next. 108 | (version = 3 or: [version = 4]) 109 | ifFalse:[self debugLog: 'version found: ' , version]. " [MQTT-3.1.2-2]. ignore" 110 | cFlags := aReadStream next. 111 | (cFlags allMask: 1) 112 | ifTrue:[(self connectionRefusedError: 16r01) signal "[MQTT-3.1.2-3]." 113 | ]. 114 | cleanSessionFlag := cFlags allMask: 2. 115 | 116 | "We don't support the will logic" 117 | willFlag := cFlags allMask: 4. 118 | willRetainFlag := cFlags allMask: 1 << 5. 119 | willQos := cFlags >> 3 bitAnd: 3. 120 | passwordFlag := cFlags allMask: 1 << 6. 121 | userFlag := cFlags allMask: 1 << 7. 122 | keepAliveTime := self decode16BitIntegerFrom: aReadStream. " [MQTT-3.1.2-24]." 123 | clientID := self decodeStringFrom: aReadStream. "[MQTT-3.1.3-1]" 124 | clientID size = 0 125 | ifTrue:[(self connectionRefusedError: 16r02) signal " [MQTT-3.1.3-9]." 126 | ]. 127 | 128 | "[MQTT-3.1.3-3/4/5/6/7/8] are handled by failure of size 0" 129 | willFlag 130 | ifTrue:[ 131 | willTopic := self decodeStringFrom: aReadStream. 132 | willMessage := self decodeByteArrayFrom: aReadStream]. 133 | userFlag 134 | ifTrue:[user := self decodeStringFrom: aReadStream]. 135 | passwordFlag 136 | ifTrue:[password := self decodeByteArrayFrom: aReadStream]. "[MQTT-3.1.2-21]" 137 | returnCode := 16r00. 138 | ] 139 | 140 | { #category : #mqtt } 141 | MQTTPacketConnect >> decodeVariableHeaderFrom: aReadStream [ 142 | 143 | "do nothing because the actual work is deferred to the decodePayloadFrom: method" 144 | 145 | ] 146 | 147 | { #category : #mqtt } 148 | MQTTPacketConnect >> encodePasswordOn: aWriteStream [ 149 | 150 | " if we have a username set, write it to the stream" 151 | 152 | password ifNotNil: [self encodeString: password on: aWriteStream] 153 | ] 154 | 155 | { #category : #mqtt } 156 | MQTTPacketConnect >> encodePayloadOn: aWriteStream [ 157 | 158 | "write my payload onto the stream- 159 | the client ID 160 | iff it exists, the will topic 161 | the will message 162 | iff it exists, the username 163 | iff it exists, the password" 164 | 165 | self encodeString: self clientIDString on: aWriteStream. 166 | self encodeWillDataOn: aWriteStream. 167 | self encodeUsernameOn: aWriteStream. 168 | self encodePasswordOn: aWriteStream 169 | ] 170 | 171 | { #category : #mqtt } 172 | MQTTPacketConnect >> encodeUsernameOn: aWriteStream [ 173 | 174 | " if we have a username set, write it to the stream" 175 | 176 | user ifNotNil: [self encodeString: user on: aWriteStream] 177 | ] 178 | 179 | { #category : #mqtt } 180 | MQTTPacketConnect >> encodeVariableHeaderOn: aWriteStream [ 181 | 182 | "write the CONNECT variable header (see 3.1.2) to the stream" 183 | 184 | self encodeString: 'MQTT' on: aWriteStream. 185 | aWriteStream 186 | nextPut: MQTTProtocolLevel311; 187 | nextPut: self connectFlags. 188 | self encode16BitInteger: keepAliveTime on: aWriteStream 189 | ] 190 | 191 | { #category : #mqtt } 192 | MQTTPacketConnect >> encodeWillDataOn: aWriteStream [ 193 | 194 | "If I have a will set up, write the topic and then the message to the stream" 195 | 196 | willTopic ifNotNil: [ 197 | self encodeString: willTopic on: aWriteStream. 198 | willMessage ifNil: [self encodeString: '' on: aWriteStream] ifNotNil: [ 199 | self encodeString: willMessage on: aWriteStream]] 200 | ] 201 | 202 | { #category : #mqtt } 203 | MQTTPacketConnect >> evaluateFor: anMQTTServer [ 204 | 205 | "I've been received by the client so now is the time to come to the aid of the party" 206 | 207 | ^anMQTTServer handleConnectPacket: self 208 | ] 209 | 210 | { #category : #mqtt } 211 | MQTTPacketConnect >> initialize [ 212 | 213 | "set the basic state to no user/password/will stuff/clean, just for safety. Nil is fine for most of them but a couple of flags and values need more specific initial values." 214 | 215 | willRetainFlag := false. 216 | cleanSessionFlag := true. "normally we are establishing a new connection and don't want old data back" 217 | willQos := 0. 218 | keepAliveTime := 60 219 | ] 220 | 221 | { #category : #mqtt } 222 | MQTTPacketConnect >> keepAliveTime [ 223 | 224 | "Answer my 'keepAliveTime' instance variable." 225 | 226 | ^keepAliveTime 227 | ] 228 | 229 | { #category : #mqtt } 230 | MQTTPacketConnect >> keepAliveTime: timeInSecs [ 231 | 232 | "set the keep alive time interval; default is 60" 233 | 234 | keepAliveTime := timeInSecs 235 | ] 236 | 237 | { #category : #mqtt } 238 | MQTTPacketConnect >> packetType [ 239 | 240 | ^1 241 | ] 242 | 243 | { #category : #mqtt } 244 | MQTTPacketConnect >> printOn: aStream [ 245 | 246 | "print useful data" 247 | 248 | super printOn: aStream. 249 | aStream nextPutAll: ' ID: '. 250 | clientID asString printOn: aStream. 251 | aStream nextPutAll: ' user: '. 252 | user asString printOn: aStream. 253 | aStream nextPutAll: ' kat: '. 254 | keepAliveTime asString printOn: aStream. 255 | aStream nextPutAll: ' c: '. 256 | cleanSessionFlag asString printOn: aStream. 257 | aStream nextPutAll: ' will: '. 258 | willFlag asString printOn: aStream. 259 | ] 260 | 261 | { #category : #mqtt } 262 | MQTTPacketConnect >> returnCode [ 263 | 264 | "Answer my 'returnCode' instance variable." 265 | 266 | ^returnCode 267 | ] 268 | 269 | { #category : #mqtt } 270 | MQTTPacketConnect >> returnCode: aValue [ 271 | 272 | "Set my 'returnCode' instance variable to aValue." 273 | 274 | returnCode := aValue 275 | ] 276 | 277 | { #category : #mqtt } 278 | MQTTPacketConnect >> testCleanSessionFlag [ 279 | 280 | ^cleanSessionFlag 281 | ] 282 | 283 | { #category : #mqtt } 284 | MQTTPacketConnect >> testKeepAlive [ 285 | 286 | ^keepAliveTime 287 | ] 288 | 289 | { #category : #mqtt } 290 | MQTTPacketConnect >> testPassword [ 291 | 292 | ^password 293 | ] 294 | 295 | { #category : #mqtt } 296 | MQTTPacketConnect >> testUser [ 297 | 298 | ^user 299 | ] 300 | 301 | { #category : #mqtt } 302 | MQTTPacketConnect >> testWillMessage [ 303 | 304 | ^willMessage 305 | ] 306 | 307 | { #category : #mqtt } 308 | MQTTPacketConnect >> testWillQos [ 309 | 310 | ^willQos 311 | ] 312 | 313 | { #category : #mqtt } 314 | MQTTPacketConnect >> testWillRetain [ 315 | 316 | ^willRetainFlag 317 | ] 318 | 319 | { #category : #mqtt } 320 | MQTTPacketConnect >> testWillTopic [ 321 | 322 | ^willTopic 323 | ] 324 | 325 | { #category : #mqtt } 326 | MQTTPacketConnect >> user: userName password: passwd [ 327 | 328 | "setup the user name and password; it is possible to have either nil, apparently" 329 | 330 | user := userName. 331 | password := passwd 332 | ] 333 | 334 | { #category : #mqtt } 335 | MQTTPacketConnect >> willFlag [ 336 | 337 | "Answer my 'willFlag' instance variable." 338 | 339 | ^willFlag 340 | ] 341 | 342 | { #category : #mqtt } 343 | MQTTPacketConnect >> willFlag: aValue [ 344 | 345 | "Set my 'willFlag' instance variable to aValue." 346 | 347 | willFlag := aValue 348 | ] 349 | 350 | { #category : #mqtt } 351 | MQTTPacketConnect >> willMessage [ 352 | 353 | "Answer my 'willMessage' instance variable." 354 | 355 | ^willMessage 356 | ] 357 | 358 | { #category : #mqtt } 359 | MQTTPacketConnect >> willQos [ 360 | 361 | "Answer my 'willQos' instance variable." 362 | 363 | ^willQos 364 | ] 365 | 366 | { #category : #mqtt } 367 | MQTTPacketConnect >> willRetainFlag [ 368 | 369 | "Answer my 'willRetainFlag' instance variable." 370 | 371 | ^willRetainFlag 372 | ] 373 | 374 | { #category : #mqtt } 375 | MQTTPacketConnect >> willTopic [ 376 | 377 | "Answer my 'willTopic' instance variable." 378 | 379 | ^willTopic 380 | ] 381 | 382 | { #category : #mqtt } 383 | MQTTPacketConnect >> willTopic: topicString message: messageString retain: retainBoolean qos: qosValue [ 384 | 385 | "setup a will message. We must have a topic, the actual message, a retain flag and requested QOS" 386 | 387 | willTopic := topicString. 388 | willMessage := messageString. 389 | willRetainFlag := retainBoolean. 390 | willQos := qosValue min: 2 max: 0 391 | ] 392 | -------------------------------------------------------------------------------- /src/MQTT/MQTTPacketDisconnect.class.st: -------------------------------------------------------------------------------- 1 | " 2 | The Disconnect Packet. 3 | " 4 | Class { 5 | #name : #MQTTPacketDisconnect, 6 | #superclass : #MQTTPacket, 7 | #category : #MQTT 8 | } 9 | 10 | { #category : #mqtt } 11 | MQTTPacketDisconnect class >> commentOriginal [ 12 | 13 | "A MQTTPacketDisconnect is how we close down the server connection and is the final action before closing sockets etc. See section 3.14 14 | 15 | There is no variable header nor payload. We must not send any more packets to the server after sending the DISCONNECT and must close the network connection to the server. This does NOT cause any associated Will Message to be published. (3.14.4)" 16 | 17 | ] 18 | 19 | { #category : #mqtt } 20 | MQTTPacketDisconnect >> evaluateFor: anMQTTClient [ 21 | 22 | "The broker has to handle discoonect" 23 | 24 | anMQTTClient handleDisconnectPacket: self 25 | ] 26 | 27 | { #category : #mqtt } 28 | MQTTPacketDisconnect >> packetType [ 29 | 30 | ^14 31 | ] 32 | -------------------------------------------------------------------------------- /src/MQTT/MQTTPacketPingReq.class.st: -------------------------------------------------------------------------------- 1 | " 2 | The Ping Packet. 3 | " 4 | Class { 5 | #name : #MQTTPacketPingReq, 6 | #superclass : #MQTTPacket, 7 | #category : #MQTT 8 | } 9 | 10 | { #category : #mqtt } 11 | MQTTPacketPingReq class >> commentOriginal [ 12 | 13 | "A MQTTPacketPingReq is a way for us to ping the server to make sure that the network is still awake, that the server is still talking to us and that we are still here. See section 3.12 14 | 15 | There is no variable header nor payload. We expect a PINGRESP in return (3.12.4) 16 | " 17 | 18 | ] 19 | 20 | { #category : #mqtt } 21 | MQTTPacketPingReq >> evaluateFor: anMQTTServer [ 22 | 23 | "Got PINGREQ from client" 24 | 25 | anMQTTServer handlePingReqPacket: self 26 | ] 27 | 28 | { #category : #mqtt } 29 | MQTTPacketPingReq >> packetType [ 30 | 31 | ^12 32 | ] 33 | -------------------------------------------------------------------------------- /src/MQTT/MQTTPacketPingResp.class.st: -------------------------------------------------------------------------------- 1 | " 2 | The Ping response Packet. 3 | " 4 | Class { 5 | #name : #MQTTPacketPingResp, 6 | #superclass : #MQTTPacket, 7 | #category : #MQTT 8 | } 9 | 10 | { #category : #mqtt } 11 | MQTTPacketPingResp class >> commentOriginal [ 12 | 13 | "A MQTTPacketPingResp gets sent back from the server in response to a PINGREQ. See section 3.13 14 | 15 | There is no variable header nor payload." 16 | 17 | ] 18 | 19 | { #category : #mqtt } 20 | MQTTPacketPingResp >> evaluateFor: anMQTTClient [ 21 | 22 | "The broker has responded to a PINGREQ from me; we should check that it is reasonably timely and if not, close the connection and do whatever retry stuff seems proper" 23 | 24 | anMQTTClient handlePingRespPacket: self 25 | ] 26 | 27 | { #category : #mqtt } 28 | MQTTPacketPingResp >> packetType [ 29 | 30 | ^13 31 | ] 32 | -------------------------------------------------------------------------------- /src/MQTT/MQTTPacketPubAck.class.st: -------------------------------------------------------------------------------- 1 | " 2 | The Publish ACK packet 3 | " 4 | Class { 5 | #name : #MQTTPacketPubAck, 6 | #superclass : #MQTTPacketVariableHeaded, 7 | #category : #MQTT 8 | } 9 | 10 | { #category : #mqtt } 11 | MQTTPacketPubAck class >> commentOriginal [ 12 | 13 | "An MQTTPacketPubAck is the response to a PUBLISH packet with a qos = 1. See section 3.4 We have to both send and receive them. 14 | 15 | The variable header contains the msgID that was included in the relevant PUBLISH packet. There is no payload. 16 | " 17 | 18 | ] 19 | 20 | { #category : #mqtt } 21 | MQTTPacketPubAck >> evaluateFor: anMQTTClient [ 22 | 23 | "The broker has responded to a PUBLISH from me with qos=1" 24 | 25 | anMQTTClient handlePubAckPacket: self 26 | ] 27 | 28 | { #category : #mqtt } 29 | MQTTPacketPubAck >> packetType [ 30 | 31 | ^4 32 | ] 33 | -------------------------------------------------------------------------------- /src/MQTT/MQTTPacketPubComp.class.st: -------------------------------------------------------------------------------- 1 | " 2 | The Publish COMP packet 3 | " 4 | Class { 5 | #name : #MQTTPacketPubComp, 6 | #superclass : #MQTTPacketVariableHeaded, 7 | #category : #MQTT 8 | } 9 | 10 | { #category : #mqtt } 11 | MQTTPacketPubComp class >> commentOriginal [ 12 | 13 | "A MQTTPacketPubComp is the final response to a PUBLISH with a qos = 2. We need to both send and receive these. See section 3.6 14 | 15 | The variable header contains the msgID that was included in the relevant PUBLISH packet. There is no payload. 16 | " 17 | 18 | ] 19 | 20 | { #category : #mqtt } 21 | MQTTPacketPubComp >> evaluateFor: anMQTTClient [ 22 | 23 | "The broker has responded to a PUBLISH from me with qos=2" 24 | 25 | anMQTTClient handlePubCompPacket: self 26 | ] 27 | 28 | { #category : #mqtt } 29 | MQTTPacketPubComp >> packetType [ 30 | 31 | ^7 32 | ] 33 | -------------------------------------------------------------------------------- /src/MQTT/MQTTPacketPubRec.class.st: -------------------------------------------------------------------------------- 1 | " 2 | The Publish Rec packet 3 | " 4 | Class { 5 | #name : #MQTTPacketPubRec, 6 | #superclass : #MQTTPacketVariableHeaded, 7 | #instVars : [ 8 | 'originalPacket' 9 | ], 10 | #category : #MQTT 11 | } 12 | 13 | { #category : #mqtt } 14 | MQTTPacketPubRec class >> commentOriginal [ 15 | 16 | "A MQTTPacketPubRec is the response to a PUBLISH packet with qos =2; it is the second part of the exchange. We have to bothe send and receive them. See section 3.5 17 | 18 | The variable header contains the msgID from the relevant PUBLISH packet. There is no payload." 19 | 20 | ] 21 | 22 | { #category : #mqtt } 23 | MQTTPacketPubRec >> acknowledgement [ 24 | 25 | "return a PUBREL packet to acknowledge receiveing me" 26 | 27 | ^MQTTPacketPubRel new packetID: msgID 28 | ] 29 | 30 | { #category : #mqtt } 31 | MQTTPacketPubRec >> evaluateFor: anMQTTClient [ 32 | 33 | "The broker has responded to a PUBLISH from me with qos=2" 34 | 35 | anMQTTClient handlePubRecPacket: self 36 | ] 37 | 38 | { #category : #mqtt } 39 | MQTTPacketPubRec >> originalPacket [ 40 | 41 | "Answer my 'originalPacket' instance variable." 42 | 43 | ^originalPacket 44 | ] 45 | 46 | { #category : #mqtt } 47 | MQTTPacketPubRec >> originalPacket: aValue [ 48 | 49 | "Set my 'originalPacket' instance variable to aValue." 50 | 51 | originalPacket := aValue 52 | ] 53 | 54 | { #category : #mqtt } 55 | MQTTPacketPubRec >> packetType [ 56 | 57 | ^5 58 | ] 59 | -------------------------------------------------------------------------------- /src/MQTT/MQTTPacketPubRel.class.st: -------------------------------------------------------------------------------- 1 | " 2 | The Publish Rel packet 3 | " 4 | Class { 5 | #name : #MQTTPacketPubRel, 6 | #superclass : #MQTTPacketVariableHeaded, 7 | #category : #MQTT 8 | } 9 | 10 | { #category : #mqtt } 11 | MQTTPacketPubRel class >> commentOriginal [ 12 | 13 | "A MQTTPacketPubRel is the third response to a PUBLISH with a qos = 2. We need to both send and receive these. See section 3.6 14 | 15 | The variable header contains the msgID that was included in the relevant PUBLISH packet. There is no payload." 16 | 17 | ] 18 | 19 | { #category : #mqtt } 20 | MQTTPacketPubRel >> acknowledgement [ 21 | 22 | "ack the packet" 23 | 24 | ^MQTTPacketPubComp new packetID: msgID "[MQTT-2.3.1-3]" 25 | ] 26 | 27 | { #category : #mqtt } 28 | MQTTPacketPubRel >> evaluateFor: anMQTTClient [ 29 | 30 | "The broker has responded to a PUBLISH from me with qos=2" 31 | 32 | anMQTTClient handlePubRelPacket: self 33 | ] 34 | 35 | { #category : #mqtt } 36 | MQTTPacketPubRel >> fixedHeaderFlags [ 37 | 38 | ^2 39 | ] 40 | 41 | { #category : #mqtt } 42 | MQTTPacketPubRel >> packetType [ 43 | 44 | ^6 45 | ] 46 | 47 | { #category : #mqtt } 48 | MQTTPacketPubRel >> pendingJob [ 49 | 50 | "Return a pending PUBCOMP to complete later" 51 | 52 | ^MQTTPendingPubCompJob for: self 53 | ] 54 | -------------------------------------------------------------------------------- /src/MQTT/MQTTPacketPublish.class.st: -------------------------------------------------------------------------------- 1 | " 2 | The publish packet 3 | " 4 | Class { 5 | #name : #MQTTPacketPublish, 6 | #superclass : #MQTTPacketVariableHeadedWithPayload, 7 | #instVars : [ 8 | 'topic', 9 | 'message', 10 | 'duplicate', 11 | 'qos', 12 | 'retain' 13 | ], 14 | #category : #MQTT 15 | } 16 | 17 | { #category : #mqtt } 18 | MQTTPacketPublish class >> commentOriginal [ 19 | 20 | "An MQTTPacketPublish is used in both directions; we send one to publish information to a server and receive them to get information that we have subscribed to. See section 3.3 of the spec. 21 | 22 | The fixed header contains 4 bits of data affecting how the packet is interpreted. These are encodings of the duplicate flag, the qos value and the duplicate flag; all instance variables. See 3.3.1 and #fixedHeaderFlags. 23 | duplicate (3.3.1.1) - this indicates whether this is the first attempt to send this packet. True/1 indicates this may be a re-send. 24 | qos (3.3.1.2 & table 3.2)- a value 0/1/2 (3 is reserved and must not be used) indicating the level of service required. 25 | retain (3.3.1.3) - this indicates whether the message being published should be retained by the server and sent out to any new subscriber as a sort of initial state message 26 | 27 | The variable header includes - 28 | a) topic name (3.3.2.1) - (see section 1.5.3) a string of stuff 29 | b) packet ID iff qos > 0 30 | 31 | The payload is simply the data being published in an 'application specific format' which presumably includes text or binary, plain or encoded etc. A 0 length payload is acceptable, which may be a way to simply flag status in a minimal manner. 32 | 33 | The recipient of a PUBLISH must respond according to the qos required from that packet (3.3.4)" 34 | 35 | ] 36 | 37 | { #category : #mqtt } 38 | MQTTPacketPublish >> acknowledgement [ 39 | 40 | "Depending upon the qos we need to return a PUBACK or a PUBREC" 41 | | pubRec | 42 | 43 | qos = 0 44 | ifTrue:[^self badQosError]. 45 | qos = 1 46 | ifTrue:[^MQTTPacketPubAck new packetID: msgID]. "[MQTT-2.3.1-3] [MQTT-4.6.0-3]" 47 | pubRec := MQTTPacketPubRec new. 48 | pubRec packetID: msgID. 49 | pubRec originalPacket: self. 50 | ^pubRec 51 | ] 52 | 53 | { #category : #mqtt } 54 | MQTTPacketPublish >> decodeFixedHeaderFrom: aReadStream [ 55 | 56 | "read the fixed header from the stream and check it for decent state and extract the special flags" 57 | | hdrByte | 58 | 59 | (hdrByte := aReadStream next) >> 4 = self packetType 60 | ifFalse:[self badFixedHeaderError]. 61 | duplicate := hdrByte allMask: 8. 62 | retain := hdrByte allMask: 1. 63 | qos := (hdrByte >> 1) bitAnd: 3. "[MQTT-3.3.1-4]. we do not respect" 64 | self decodeLengthFrom: aReadStream 65 | ] 66 | 67 | { #category : #mqtt } 68 | MQTTPacketPublish >> decodeFrom: aReadStream [ 69 | 70 | "We need to work out the length of the variable header, which may have an arbitrary length utf8 string and packetID, in order to be able to work out how long the payload is" 71 | | ln | 72 | 73 | self decodeFixedHeaderFrom: aReadStream. 74 | ln := self decodeVariableHeaderFrom: aReadStream. 75 | message := aReadStream next: (remainingLength - ln) 76 | ] 77 | 78 | { #category : #mqtt } 79 | MQTTPacketPublish >> decodeTopicStringFrom: aReadStream [ 80 | 81 | "read a string from the stream, remembering that it will be prepended with a 16 bit integer indicating the length of the string. Convert from UTF8 to vse character mapping. 82 | Set the string as my topic and return the (encoded) string length to help in decoding the payload. 83 | This replace a simple #decodeStringFrom: normally used" 84 | | ln string | 85 | 86 | " [MQTT-3.3.2-1" 87 | 88 | "[MQTT-3.3.2-2] has no check" 89 | ln := self decode16BitIntegerFrom: aReadStream. 90 | string := aReadStream next: ln. 91 | topic := [UTF8Encoder decode: string asString] on: Error do: [:ex | 'Failed Topic Decode']. 92 | ^ln + 2 93 | ] 94 | 95 | { #category : #mqtt } 96 | MQTTPacketPublish >> decodeVariableHeaderFrom: aReadStream [ 97 | 98 | "extract the topic string and then iff qos >0, the msgID. Return the (encoded) length of the variable header to help in decoding the payload" 99 | | ln | 100 | 101 | ln := self decodeTopicStringFrom: aReadStream. "[MQTT-3.3.2-1]" 102 | qos > 0 103 | ifTrue:[ 104 | super decodeVariableHeaderFrom: aReadStream. 105 | ln := ln + 2]. 106 | ^ln 107 | ] 108 | 109 | { #category : #mqtt } 110 | MQTTPacketPublish >> duplicateFlag: aBoolean [ 111 | 112 | "set my DUP flag; this needs to be done by the client after the first send attempt for a qos>0 packet" 113 | 114 | duplicate := aBoolean 115 | ] 116 | 117 | { #category : #mqtt } 118 | MQTTPacketPublish >> encodePayloadOn: aWriteStream [ 119 | 120 | "PUBLISH packets put the message data out as is with no prepended size field. We work out the data size from the remainingLength value etc" 121 | 122 | aWriteStream nextPutAll: message 123 | ] 124 | 125 | { #category : #mqtt } 126 | MQTTPacketPublish >> encodeVariableHeaderOn: aWriteStream [ 127 | 128 | "PUBLISH packets put the topic into the variable header followed by a msgID iff qos >0" 129 | 130 | topic ifNil: [^self badTopicError]. 131 | self encodeString: topic on: aWriteStream. 132 | qos > 0 133 | ifTrue:[super encodeVariableHeaderOn: aWriteStream] 134 | ] 135 | 136 | { #category : #mqtt } 137 | MQTTPacketPublish >> evaluateFor: anMQTTClient [ 138 | 139 | "The broker has sent a PUBLISH packet to me" 140 | 141 | anMQTTClient handlePublishPacket: self qos: qos 142 | ] 143 | 144 | { #category : #mqtt } 145 | MQTTPacketPublish >> fixedHeaderFlags [ 146 | 147 | "a publish packet is the only one with flags dependent upon variable state 148 | DUP - duplicate delivery 149 | QoS - Quality of Service, 2 bits 150 | RETAIN - Retained message flag" 151 | 152 | ^((duplicate 153 | ifTrue:[8] 154 | ifFalse:[0]) bitOr: (qos << 1)) bitOr: ( 155 | retain 156 | ifTrue:[1] 157 | ifFalse:[0]) 158 | ] 159 | 160 | { #category : #mqtt } 161 | MQTTPacketPublish >> guessEncodedLength [ 162 | 163 | "get the size" 164 | 165 | ^self message size + 100 166 | ] 167 | 168 | { #category : #mqtt } 169 | MQTTPacketPublish >> initialize [ 170 | 171 | "set default flags & qos" 172 | 173 | qos := 0. 174 | duplicate := retain := false 175 | ] 176 | 177 | { #category : #mqtt } 178 | MQTTPacketPublish >> matchesSubscription: subscription ifTrue: aBlock [ 179 | 180 | "does my topic match the pattern? If so, evaluate the block" 181 | 182 | (subscription topicMatches: topic) 183 | ifTrue:[ 184 | aBlock value: topic value: message value: qos] 185 | ] 186 | 187 | { #category : #mqtt } 188 | MQTTPacketPublish >> message [ 189 | 190 | "Answer my 'message' instance variable." 191 | 192 | ^message 193 | ] 194 | 195 | { #category : #mqtt } 196 | MQTTPacketPublish >> messageString [ 197 | 198 | "return the message data asa String" 199 | 200 | ^UTF8Encoder decode: message asString 201 | ] 202 | 203 | { #category : #mqtt } 204 | MQTTPacketPublish >> packetType [ 205 | 206 | ^3 207 | ] 208 | 209 | { #category : #mqtt } 210 | MQTTPacketPublish >> pendingAckJob [ 211 | 212 | "We are handling a PUBLISH with qos=1 and need to schedule apending handler" 213 | 214 | ^MQTTPendingPubAckJob forDoNotTouchDupFlag: self 215 | ] 216 | 217 | { #category : #mqtt } 218 | MQTTPacketPublish >> pendingJob [ 219 | 220 | "Depending upon the qos we need to shedule a pending job for a PUBACK or a PUBREC" 221 | 222 | qos = 0 223 | ifTrue:[^self badQosError]. 224 | qos = 1 225 | ifTrue:[^MQTTPendingPubAckJob for: self]. 226 | ^MQTTPendingPubRecJob for: self 227 | ] 228 | 229 | { #category : #mqtt } 230 | MQTTPacketPublish >> pendingReceiveJob [ 231 | 232 | "We are handling a PUBLISH with qos=2 and need to schedule apending handler" 233 | 234 | ^MQTTPendingPubRecJob forDoNotTouchDupFlag: self 235 | ] 236 | 237 | { #category : #mqtt } 238 | MQTTPacketPublish >> prepareForResend [ 239 | 240 | "set my DUP flag; this needs to be done by the client after the first send attempt for a qos>0 packet" 241 | 242 | "[MQTT-3.3.1.-1]" 243 | duplicate := true 244 | ] 245 | 246 | { #category : #mqtt } 247 | MQTTPacketPublish >> printOn: aStream [ 248 | 249 | "print useful data" 250 | 251 | super printOn: aStream. 252 | aStream nextPutAll: ' t: '. 253 | topic asString printOn: aStream. 254 | aStream nextPutAll: ' sz: '. 255 | message size asString printOn: aStream. 256 | aStream nextPutAll: ' qos: '. 257 | qos asString printOn: aStream. 258 | aStream nextPutAll: ' d: '. 259 | duplicate asString printOn: aStream. 260 | aStream nextPutAll: ' r: '. 261 | retain asString printOn: aStream. 262 | ] 263 | 264 | { #category : #mqtt } 265 | MQTTPacketPublish >> qos [ 266 | 267 | "Answer my 'qos' instance variable." 268 | 269 | ^qos 270 | ] 271 | 272 | { #category : #mqtt } 273 | MQTTPacketPublish >> qos: aValue [ 274 | 275 | "Set my 'qos' instance variable to aValue." 276 | 277 | qos := aValue 278 | ] 279 | 280 | { #category : #mqtt } 281 | MQTTPacketPublish >> retain [ 282 | 283 | "Answer my 'retain' instance variable." 284 | 285 | ^retain 286 | ] 287 | 288 | { #category : #mqtt } 289 | MQTTPacketPublish >> retainFlag: aBoolean [ 290 | 291 | "set my RETAIN flag; this is supposed to be set if you want the message to be held by the broker for broadcasting in response to new subscribes." 292 | 293 | retain := aBoolean 294 | ] 295 | 296 | { #category : #mqtt } 297 | MQTTPacketPublish >> topic [ 298 | 299 | "Answer my 'topic' instance variable." 300 | 301 | ^topic 302 | ] 303 | 304 | { #category : #mqtt } 305 | MQTTPacketPublish >> topic: topicString message: messageBytes [ 306 | 307 | "set my topic string and message data. 308 | 309 | 310 | 311 | The topic must be a plain String which we will now check for any invalid characters and make sure when converted to UTF8 is still under 64kb long. The messageBytes must be a ByteArray" 312 | 313 | ((UTF8Encoder encode: topicString asString) size > 65535 or: [topicString includesAnyOf: #( 314 | $# $+ )]) "MQTT-4.7.1-1 MQTT-4.7.3-3" 315 | ifTrue:[^self badTopicError]. 316 | topic := topicString. 317 | message := messageBytes 318 | ] 319 | -------------------------------------------------------------------------------- /src/MQTT/MQTTPacketSubAck.class.st: -------------------------------------------------------------------------------- 1 | " 2 | The Subscribe ACK packet 3 | " 4 | Class { 5 | #name : #MQTTPacketSubAck, 6 | #superclass : #MQTTPacketVariableHeadedWithPayload, 7 | #instVars : [ 8 | 'returnCodes' 9 | ], 10 | #category : #MQTT 11 | } 12 | 13 | { #category : #mqtt } 14 | MQTTPacketSubAck class >> commentOriginal [ 15 | 16 | "A MQTTPacketSubAck is the response from the server to a SUBSCRIBE packet. We only receive these. See section 3.9 17 | 18 | The variable header consists of the same msgID as the initiating SUBSCRIBE. 19 | 20 | The payload is a list of return codes in the same order as the originallly SUBSCRIBEd topics. Each code provides the maximum qos the server will allow for each topic, or \128 for a failure (3.9.3)" 21 | 22 | ] 23 | 24 | { #category : #mqtt } 25 | MQTTPacketSubAck >> decodePayloadFrom: aReadStream [ 26 | "read my payload from the stream" 27 | 28 | returnCodes := aReadStream next: (remainingLength -2) 29 | ] 30 | 31 | { #category : #mqtt } 32 | MQTTPacketSubAck >> encodePayloadOn: aWriteStream [ 33 | 34 | "write my payload onto the stream" 35 | 36 | aWriteStream nextPutAll: self returnCodes 37 | ] 38 | 39 | { #category : #mqtt } 40 | MQTTPacketSubAck >> evaluateFor: anMQTTClient [ 41 | 42 | "The broker has responded to a SUBSCRIBE from me" 43 | 44 | anMQTTClient handleSubAckPacket: self 45 | ] 46 | 47 | { #category : #mqtt } 48 | MQTTPacketSubAck >> fixedHeaderFlags [ 49 | 50 | "fixed header" 51 | 52 | ^16r90 53 | ] 54 | 55 | { #category : #mqtt } 56 | MQTTPacketSubAck >> packetType [ 57 | 58 | ^9 59 | ] 60 | 61 | { #category : #mqtt } 62 | MQTTPacketSubAck >> returnCodes [ 63 | 64 | "Answer my 'returnCodes' instance variable." 65 | 66 | ^returnCodes 67 | ] 68 | 69 | { #category : #mqtt } 70 | MQTTPacketSubAck >> returnCodes: aValue [ 71 | 72 | "Set my 'returnCodes' instance variable to aValue." 73 | 74 | returnCodes := aValue 75 | ] 76 | -------------------------------------------------------------------------------- /src/MQTT/MQTTPacketSubscribe.class.st: -------------------------------------------------------------------------------- 1 | " 2 | The Subscribe packet 3 | " 4 | Class { 5 | #name : #MQTTPacketSubscribe, 6 | #superclass : #MQTTPacketVariableHeadedWithPayload, 7 | #instVars : [ 8 | 'payloadDict' 9 | ], 10 | #category : #MQTT 11 | } 12 | 13 | { #category : #mqtt } 14 | MQTTPacketSubscribe class >> commentOriginal [ 15 | 16 | "A MQTTPacketSubscribe contains a request to the server for future information on a topic or topics. We only send them. See section 3.8 17 | 18 | The variable header consists of a msgID set by us (3.8.2). It will be used by the server iff qos > 0 and we are supposed to make sure msgIDs are not used by multiple active packets. This would seem to require some complex checking within lists of active packets, or other tag collections. The Python implementation doesn't seem to even pretend to do this right. 19 | 20 | The payload is a list of topic filter strings (there must be at least one topic requested) accompanied by a requested qos (3.8.3). 21 | 22 | The server must respond with a SUBACK having the same msgID." 23 | 24 | ] 25 | 26 | { #category : #mqtt } 27 | MQTTPacketSubscribe >> addTopic: aString qos: aSmallNumber [ 28 | 29 | "add this topic to the payload along with its qos request. Some checking for validity wouldn't be a bad idea" 30 | 31 | payloadDict at: aString put: (aSmallNumber min: 2 max: 0) 32 | ] 33 | 34 | { #category : #mqtt } 35 | MQTTPacketSubscribe >> decodePayloadFrom: aReadStream [ 36 | 37 | "read my payload from the stream" 38 | | topic qos estimatedEnd | 39 | 40 | estimatedEnd := remainingLength - 2. 41 | [ 42 | topic := self decodeStringFrom: aReadStream. "[MQTT-3.8.3-1]" 43 | (topic isNil or: [topic size = 0]) 44 | ifTrue:[self badTopicError]. 45 | qos := aReadStream next asInteger. 46 | payloadDict at: topic put: qos. 47 | estimatedEnd := estimatedEnd - (topic size) - 2 - 1. 48 | estimatedEnd = 0] whileFalse. 49 | ] 50 | 51 | { #category : #mqtt } 52 | MQTTPacketSubscribe >> encodeOn: aWriteStream [ 53 | 54 | "check for having at least one topic and qos-request pair - fail if not" 55 | 56 | payloadDict ifEmpty: [^self badTopicListError]. 57 | ^super encodeOn: aWriteStream 58 | ] 59 | 60 | { #category : #mqtt } 61 | MQTTPacketSubscribe >> encodePayloadOn: aWriteStream [ 62 | 63 | "write my payload onto the stream" 64 | 65 | payloadDict keysAndValuesDo: [:key :val | 66 | self encodeString: key on: aWriteStream. 67 | aWriteStream nextPut: (val bitAnd: 3)] 68 | ] 69 | 70 | { #category : #mqtt } 71 | MQTTPacketSubscribe >> evaluateFor: anMQTTServer [ 72 | 73 | "I've been received by the client so now is the time to come to the aid of the party" 74 | 75 | ^anMQTTServer handleSubscribePacket: self 76 | ] 77 | 78 | { #category : #mqtt } 79 | MQTTPacketSubscribe >> fixedHeaderFlags [ 80 | 81 | ^2 82 | ] 83 | 84 | { #category : #mqtt } 85 | MQTTPacketSubscribe >> initialize [ 86 | 87 | "I need a dictionary prepared for the payload(s)" 88 | 89 | payloadDict := OrderedDictionary new: 2 "keep initial guess small" 90 | ] 91 | 92 | { #category : #mqtt } 93 | MQTTPacketSubscribe >> packetType [ 94 | 95 | ^8 96 | ] 97 | 98 | { #category : #mqtt } 99 | MQTTPacketSubscribe >> payloadDict [ 100 | 101 | "Answer my 'payloadDict' instance variable." 102 | 103 | ^payloadDict 104 | ] 105 | 106 | { #category : #mqtt } 107 | MQTTPacketSubscribe >> pendingJob [ 108 | 109 | "Return a pending SUBACK to complete later" 110 | 111 | ^MQTTPendingSubAckJob new originalPacket: self 112 | ] 113 | 114 | { #category : #mqtt } 115 | MQTTPacketSubscribe >> printOn: aStream [ 116 | 117 | "print useful data" 118 | 119 | super printOn: aStream. 120 | aStream nextPutAll: ' topics: '. 121 | payloadDict keys printOn: aStream. 122 | ] 123 | -------------------------------------------------------------------------------- /src/MQTT/MQTTPacketUnsubAck.class.st: -------------------------------------------------------------------------------- 1 | " 2 | The unsubscribe Ack packet 3 | " 4 | Class { 5 | #name : #MQTTPacketUnsubAck, 6 | #superclass : #MQTTPacketVariableHeaded, 7 | #category : #MQTT 8 | } 9 | 10 | { #category : #mqtt } 11 | MQTTPacketUnsubAck class >> commentOriginal [ 12 | 13 | "A MQTTPacketUnsubAck is returned by the server after an UNSUBSCRIBE. See section 3.11 14 | 15 | The variable header is just the msgID provided by the initiating UNSUBSCRIBE packet (3.11.2). There is no payload. An UNSUBSCRIBE that lists several topics is responded to with a single UNSUBACK (3.10.4). We get the UNSUBACK even if no topics are actually cancelled." 16 | 17 | ] 18 | 19 | { #category : #mqtt } 20 | MQTTPacketUnsubAck >> evaluateFor: anMQTTClient [ 21 | 22 | "The broker has responded to an UNSUB from me" 23 | 24 | anMQTTClient handleUnsubAckPacket: self 25 | ] 26 | 27 | { #category : #mqtt } 28 | MQTTPacketUnsubAck >> packetType [ 29 | 30 | ^11 31 | ] 32 | -------------------------------------------------------------------------------- /src/MQTT/MQTTPacketUnsubscribe.class.st: -------------------------------------------------------------------------------- 1 | " 2 | The UnSubscribe ACK packet 3 | " 4 | Class { 5 | #name : #MQTTPacketUnsubscribe, 6 | #superclass : #MQTTPacketVariableHeadedWithPayload, 7 | #instVars : [ 8 | 'topics' 9 | ], 10 | #category : #MQTT 11 | } 12 | 13 | { #category : #mqtt } 14 | MQTTPacketUnsubscribe class >> commentOriginal [ 15 | 16 | "A MQTTPacketUnsubscribe is how we unsubscribe from topics and we only send them to the server. See section 3.10 17 | 18 | The variable header is just a msgID that we provide. See also the MQTTPacketSubscribe class comment. (3.10.2) 19 | 20 | The payload is a list of topics that we no longer wish to subscribe to (3.10.3); it does not have to exactly match subscribed topics and mismatched topics will be ignored. The server may complete delivery of data relating to now unsubscribed topics if there are already buffered or in-progress broadcasts such as qos>0 messages. (3.10.4). 21 | " 22 | 23 | ] 24 | 25 | { #category : #mqtt } 26 | MQTTPacketUnsubscribe >> addTopic: aTopic [ 27 | 28 | "add a topic to unsubscribe from" 29 | 30 | topics add: aTopic 31 | ] 32 | 33 | { #category : #mqtt } 34 | MQTTPacketUnsubscribe >> decodePayloadFrom: aReadStream [ 35 | 36 | "read my payload from the stream" 37 | | topic estimatedEnd | 38 | 39 | estimatedEnd := remainingLength - 2. 40 | [ 41 | topic := self decodeStringFrom: aReadStream. "[MQTT-3.10.3-1]" 42 | (topic isNil or: [topic size = 0 or: [(MQTTSubscription for: topic qos: 0 value do: [:x :y :z | ]) isNil]]) 43 | ifTrue:[self badTopicError]. 44 | topics add: topic. 45 | estimatedEnd := estimatedEnd - (topic size) - 2. 46 | estimatedEnd = 0] whileFalse. 47 | ] 48 | 49 | { #category : #mqtt } 50 | MQTTPacketUnsubscribe >> encodeOn: aWriteStream [ 51 | 52 | "check for having at least one topic and qos-request pair - fail if not" 53 | 54 | topics ifEmpty: [^self badTopicListError]. 55 | ^super encodeOn: aWriteStream 56 | ] 57 | 58 | { #category : #mqtt } 59 | MQTTPacketUnsubscribe >> encodePayloadOn: aWriteStream [ 60 | 61 | "write my payload onto the stream" 62 | 63 | topics do: [:val | self encodeString: val on: aWriteStream] 64 | ] 65 | 66 | { #category : #mqtt } 67 | MQTTPacketUnsubscribe >> evaluateFor: anMQTTClient [ 68 | 69 | "The server has to handle unsubscribe" 70 | 71 | anMQTTClient handleUnsubscribePacket: self 72 | ] 73 | 74 | { #category : #mqtt } 75 | MQTTPacketUnsubscribe >> fixedHeaderFlags [ 76 | 77 | ^2 78 | ] 79 | 80 | { #category : #mqtt } 81 | MQTTPacketUnsubscribe >> initialize [ 82 | 83 | topics := OrderedCollection new: 2 84 | ] 85 | 86 | { #category : #mqtt } 87 | MQTTPacketUnsubscribe >> packetType [ 88 | 89 | ^10 90 | ] 91 | 92 | { #category : #mqtt } 93 | MQTTPacketUnsubscribe >> pendingJob [ 94 | 95 | "Return a pending UNSUBACK to complete later" 96 | 97 | ^MQTTPendingUnsubAckJob for: self 98 | ] 99 | 100 | { #category : #mqtt } 101 | MQTTPacketUnsubscribe >> printOn: aStream [ 102 | 103 | "print useful data" 104 | 105 | super printOn: aStream. 106 | aStream nextPutAll: ' topics: '. 107 | topics printOn: aStream. 108 | ] 109 | 110 | { #category : #mqtt } 111 | MQTTPacketUnsubscribe >> topics [ 112 | 113 | "Answer my 'topics' instance variable." 114 | 115 | ^topics 116 | ] 117 | -------------------------------------------------------------------------------- /src/MQTT/MQTTPacketVariableHeaded.class.st: -------------------------------------------------------------------------------- 1 | " 2 | Abstraction for variable header packets 3 | " 4 | Class { 5 | #name : #MQTTPacketVariableHeaded, 6 | #superclass : #MQTTPacket, 7 | #instVars : [ 8 | 'remainingLength', 9 | 'msgID' 10 | ], 11 | #category : #MQTT 12 | } 13 | 14 | { #category : #mqtt } 15 | MQTTPacketVariableHeaded class >> commentOriginal [ 16 | 17 | "MQTTPacketVariableHeaded is an abstract class for the packets that include a variable header. 18 | CONNACK 19 | PUBACK 20 | PUBCOMP 21 | PUBREC 22 | PUBREL 23 | UNSUBACK 24 | " 25 | 26 | ] 27 | 28 | { #category : #mqtt } 29 | MQTTPacketVariableHeaded >> decodeByteArrayFrom: aReadStream [ 30 | 31 | "read a byteArray from the stream, remembering that it will be prepended with a 16 bit integer indicating the length of the string. Conver t from UTF8 to vse character mapping." 32 | | ln bytes | 33 | 34 | ln := self decode16BitIntegerFrom: aReadStream. 35 | bytes := aReadStream next: ln. 36 | ^bytes 37 | ] 38 | 39 | { #category : #mqtt } 40 | MQTTPacketVariableHeaded >> decodeFrom: aReadStream [ 41 | 42 | "decode my fixed header and then my variable header" 43 | 44 | super decodeFrom: aReadStream. 45 | self decodeVariableHeaderFrom: aReadStream 46 | ] 47 | 48 | { #category : #mqtt } 49 | MQTTPacketVariableHeaded >> decodeLengthFrom: aReadStream [ 50 | 51 | "pull bytes from the stream and convert to the remainingLength value for this packet" 52 | | byte shift val | 53 | 54 | shift := 0. 55 | val := 0. 56 | [ 57 | byte := aReadStream next. 58 | (byte isNil or: [shift > 21]) 59 | ifTrue:[self encodedLengthError]. 60 | val := val + ((byte bitAnd: 127) << shift). 61 | shift := shift + 7. 62 | (byte bitAnd: 128) > 0] whileTrue. 63 | remainingLength := val 64 | ] 65 | 66 | { #category : #mqtt } 67 | MQTTPacketVariableHeaded >> decodeStringFrom: aReadStream [ 68 | 69 | "read a string from the stream, remembering that it will be prepended with a 16 bit integer indicating the length of the string. Conver t from UTF8 to vse character mapping." 70 | | ln string | 71 | 72 | ln := self decode16BitIntegerFrom: aReadStream. 73 | string := aReadStream next: ln. 74 | ^UTF8Encoder decode: string asString 75 | ] 76 | 77 | { #category : #mqtt } 78 | MQTTPacketVariableHeaded >> decodeVariableHeaderFrom: aReadStream [ 79 | 80 | "read the variable header from the stream. Not all subclasses actually use msgIDs, so be careful to catch those" 81 | 82 | msgID := self decode16BitIntegerFrom: aReadStream 83 | ] 84 | 85 | { #category : #mqtt } 86 | MQTTPacketVariableHeaded >> encodeFixedHeaderOn: aWriteStream [ 87 | 88 | "write the fixed header on the stream" 89 | 90 | aWriteStream nextPut: self fixedHeader. 91 | self encodeLengthOn: aWriteStream 92 | ] 93 | 94 | { #category : #mqtt } 95 | MQTTPacketVariableHeaded >> encodeLengthOn: aWriteStream [ 96 | 97 | "encode the remaining length in the odd 7-bit manner used; see mqtt doc table 2.4 etc. write each byte to the stream" 98 | | val byte | 99 | 100 | val := remainingLength. 101 | [ 102 | byte := val \\ 128. 103 | val := val // 128. 104 | val > 0 105 | ifTrue:[byte := byte bitOr: 128]. 106 | aWriteStream nextPut: byte. 107 | val > 0] whileTrue 108 | ] 109 | 110 | { #category : #mqtt } 111 | MQTTPacketVariableHeaded >> encodeOn: aWriteStream [ 112 | 113 | "when writing a packet out we can either 114 | 115 | 116 | write the fixed header 117 | 118 | 119 | calculate the remaining length (which involves doing almost all the work below) and writing that out 120 | 121 | 122 | write the variable header 123 | 124 | 125 | write the payload(s) 126 | 127 | 128 | or 129 | 130 | 131 | make a temporary stream 132 | 133 | 134 | write the variable header to the temp stream 135 | 136 | 137 | write the payload(s) tothe temp stream 138 | 139 | 140 | calculate the remaining length value from the size of the temp stream 141 | 142 | 143 | write the fixed header to the original stream 144 | 145 | 146 | write the calculated size to the original stream 147 | 148 | 149 | write the temp stream to the original stream 150 | 151 | 152 | I'm going to try the second method here. It might be even cleaner to use nextPutAllFlush: tempStream contents, but maybe later" 153 | | tempStream | 154 | 155 | tempStream := MQTTWriteStream on: (ByteArray new: self guessEncodedLength). 156 | self encodeVariableHeaderOn: tempStream. 157 | self encodePayloadOn: tempStream. 158 | remainingLength := tempStream position. 159 | self encodeFixedHeaderOn: aWriteStream. 160 | aWriteStream next: tempStream position putAll: tempStream contents startingAt: 1. 161 | aWriteStream flush. 162 | ^aWriteStream 163 | ] 164 | 165 | { #category : #mqtt } 166 | MQTTPacketVariableHeaded >> encodePayloadOn: ignored [ 167 | 168 | "do nothing; subclasses that actually have payloads must do Their Own Thing" 169 | 170 | ] 171 | 172 | { #category : #mqtt } 173 | MQTTPacketVariableHeaded >> encodeVariableHeaderOn: aWriteStream [ 174 | 175 | "write the basic variable header to the stream. Not all subclasses actually use msgIDs, so be careful to catch those, and some subclass this method to add more data" 176 | 177 | self encode16BitInteger: msgID on: aWriteStream 178 | ] 179 | 180 | { #category : #mqtt } 181 | MQTTPacketVariableHeaded >> guessEncodedLength [ 182 | 183 | "make a plausible guess at the final encoded length, erring on the side of excess" 184 | 185 | ^100 186 | ] 187 | 188 | { #category : #mqtt } 189 | MQTTPacketVariableHeaded >> packetID [ 190 | 191 | "return the packet identifier that many of my subclasses (but not all!) contain" 192 | 193 | ^msgID 194 | ] 195 | 196 | { #category : #mqtt } 197 | MQTTPacketVariableHeaded >> packetID: a16BitNumber [ 198 | 199 | "set the packet identifier that many of my subclasses (but not all!) contain. 200 | We ought to test this for validitiy" 201 | 202 | msgID := a16BitNumber 203 | ] 204 | 205 | { #category : #mqtt } 206 | MQTTPacketVariableHeaded >> printOn: aStream [ 207 | 208 | "printing" 209 | 210 | super printOn: aStream. 211 | msgID ifNotNil: [ 212 | aStream nextPutAll: ' msgID: '. 213 | msgID printOn: aStream] 214 | ] 215 | -------------------------------------------------------------------------------- /src/MQTT/MQTTPacketVariableHeadedWithPayload.class.st: -------------------------------------------------------------------------------- 1 | " 2 | Abstract packet for variable headers with payload 3 | " 4 | Class { 5 | #name : #MQTTPacketVariableHeadedWithPayload, 6 | #superclass : #MQTTPacketVariableHeaded, 7 | #category : #MQTT 8 | } 9 | 10 | { #category : #mqtt } 11 | MQTTPacketVariableHeadedWithPayload class >> commentOriginal [ 12 | 13 | "MQTTPacketVariableHeadedWithPayload is an abstract class for the packets with both variable headers and payloads. 14 | CONNECT 15 | PUBLISH 16 | SUBSCRIBE 17 | SUBACK 18 | UNSUBSCRIBE" 19 | 20 | ] 21 | 22 | { #category : #mqtt } 23 | MQTTPacketVariableHeadedWithPayload >> decodeFrom: aReadStream [ 24 | 25 | "decode my fixed header and then my variable header and then my payload" 26 | 27 | super decodeFrom: aReadStream. 28 | self decodePayloadFrom: aReadStream 29 | ] 30 | 31 | { #category : #mqtt } 32 | MQTTPacketVariableHeadedWithPayload >> decodePayloadFrom: aReadStream [ 33 | 34 | "read my payload from the stream" 35 | 36 | "default version does nothing yet" 37 | ] 38 | 39 | { #category : #mqtt } 40 | MQTTPacketVariableHeadedWithPayload >> encodePayloadOn: aWriteStream [ 41 | 42 | "write my payload onto the stream" 43 | 44 | "default version does nothing yet" 45 | ] 46 | 47 | { #category : #mqtt } 48 | MQTTPacketVariableHeadedWithPayload >> messageID [ 49 | 50 | "set the message ID; may need to check validity of these" 51 | 52 | ^msgID 53 | ] 54 | 55 | { #category : #mqtt } 56 | MQTTPacketVariableHeadedWithPayload >> messageID: aNumber [ 57 | 58 | "set the message ID; may need to check validity of these" 59 | 60 | msgID := aNumber 61 | ] 62 | -------------------------------------------------------------------------------- /src/MQTT/MQTTPendingJob.class.st: -------------------------------------------------------------------------------- 1 | " 2 | Abstract class for pending jobs. 3 | " 4 | Class { 5 | #name : #MQTTPendingJob, 6 | #superclass : #Object, 7 | #instVars : [ 8 | 'originalPacket', 9 | 'sendTime' 10 | ], 11 | #category : #MQTT 12 | } 13 | 14 | { #category : #mqtt } 15 | MQTTPendingJob class >> commentOriginal [ 16 | 17 | "MQTTPendingJobs are a way to record some response we are waiting to get from the broker. 18 | For example, when we publish something at qos=1 we add a pending job for a PUBACK - an MQTTPendingPubAckJob." 19 | 20 | ] 21 | 22 | { #category : #mqtt } 23 | MQTTPendingJob class >> for: anMQTTPacket [ 24 | 25 | "return a pending job for the packet" 26 | 27 | ^self new originalPacket: anMQTTPacket 28 | ] 29 | 30 | { #category : #mqtt } 31 | MQTTPendingJob class >> forDoNotTouchDupFlag: anMQTTPacket [ 32 | 33 | "return a pending job for the packet" 34 | 35 | ^self new originalPacketDoNotTouchDupFlag: anMQTTPacket 36 | ] 37 | 38 | { #category : #mqtt } 39 | MQTTPendingJob >> completeFor: anMQTTClient [ 40 | 41 | "we're done; close me down, release me and my ID to our rest" 42 | 43 | anMQTTClient releasePendingJob: self 44 | ] 45 | 46 | { #category : #mqtt } 47 | MQTTPendingJob >> debugLog: aString [ 48 | 49 | "debug" 50 | 51 | MQTTClientInterface debugLog: self packetID printString tag: ' PJ MsgID:' str2: aString 52 | ] 53 | 54 | { #category : #mqtt } 55 | MQTTPendingJob >> isPubAckID: msgID [ 56 | 57 | "am I a pending PUBACK job for msgId?" 58 | 59 | ^false 60 | ] 61 | 62 | { #category : #mqtt } 63 | MQTTPendingJob >> isPubCompID: msgID [ 64 | 65 | "am I a pending PUBCOMP job for msgId?" 66 | 67 | ^false 68 | ] 69 | 70 | { #category : #mqtt } 71 | MQTTPendingJob >> isPubRecID: msgID [ 72 | 73 | "am I a pending PUBREC job for msgId?" 74 | 75 | ^false 76 | ] 77 | 78 | { #category : #mqtt } 79 | MQTTPendingJob >> isPubRelID: msgID [ 80 | 81 | "am I a pending PUBREL job for msgId?" 82 | 83 | ^false 84 | ] 85 | 86 | { #category : #mqtt } 87 | MQTTPendingJob >> isSubAckID: msgID [ 88 | 89 | "am I a pending SUBACK job for msgId?" 90 | 91 | ^false 92 | ] 93 | 94 | { #category : #mqtt } 95 | MQTTPendingJob >> isUnsubAckID: msgID [ 96 | 97 | "am I a pending UNSUBACK job for msgId?" 98 | 99 | ^false 100 | ] 101 | 102 | { #category : #mqtt } 103 | MQTTPendingJob >> originalPacket [ 104 | 105 | "Answer my 'originalPacket' instance variable." 106 | 107 | ^originalPacket 108 | ] 109 | 110 | { #category : #mqtt } 111 | MQTTPendingJob >> originalPacket: mqttSubscribePacket [ 112 | 113 | "I need to remember the original subscribe data for later" 114 | 115 | originalPacket := mqttSubscribePacket. 116 | sendTime := Time totalSeconds + 60 * 60 * 23. "record the last time it was sent" 117 | 118 | "self debugLog: 'futureTime ' , self class asString , ' ' , (Time fromSeconds: sendTime) asString" 119 | ] 120 | 121 | { #category : #mqtt } 122 | MQTTPendingJob >> packetID [ 123 | 124 | ^originalPacket packetID 125 | ] 126 | 127 | { #category : #mqtt } 128 | MQTTPendingJob >> printOn: aStream [ 129 | 130 | "printing" 131 | 132 | super printOn: aStream. 133 | self packetID ifNotNil: [ 134 | aStream nextPutAll: ' msgID: '. 135 | self packetID printOn: aStream] 136 | ] 137 | 138 | { #category : #mqtt } 139 | MQTTPendingJob >> resendFor: anMQTTClient ifNeededAtTime: seconds [ 140 | 141 | "If seconds (which is the actual time - the retryTime currently in use) > my last sendTime, resend my original packet with any required changes and update that sendTime" 142 | 143 | "self debugLog: 'resendFor sendTime: ' , (Time fromSeconds: sendTime) asString , ' ifNeededAtTime: ' , ( 144 | Time fromSeconds: seconds) asString." 145 | seconds >= sendTime 146 | ifTrue:[ 147 | sendTime := Time totalSeconds. 148 | 149 | "self debugLog: 'resendFor sendTime: ' , (Time fromSeconds: sendTime) asString." 150 | self originalPacket: self originalPacket. 151 | anMQTTClient statPerform: #resendType: with: originalPacket class asString. 152 | anMQTTClient sendPacket: (MQTTPacketAndPendingJobPair packet: originalPacket pendingJob: self)] "[MQTT-4.6.0-1] [MQTT-4.3.2-1] " 153 | ] 154 | 155 | { #category : #mqtt } 156 | MQTTPendingJob >> resetSendTime: aTime [ 157 | 158 | "set the send time" 159 | 160 | sendTime := Time totalSeconds + aTime. "record the last time it was sent" 161 | 162 | "self debugLog: 'PostPacket Reset sendTime: ' , self class asString , ' ' , ( 163 | Time fromSeconds: sendTime) asString" 164 | ] 165 | -------------------------------------------------------------------------------- /src/MQTT/MQTTPendingPingJob.class.st: -------------------------------------------------------------------------------- 1 | " 2 | Pending, on a ping failure, try again 3 | " 4 | Class { 5 | #name : #MQTTPendingPingJob, 6 | #superclass : #MQTTPendingJob, 7 | #category : #MQTT 8 | } 9 | 10 | { #category : #mqtt } 11 | MQTTPendingPingJob class >> commentOriginal [ 12 | 13 | "A MQTTPendingPingJob is a bit of a hack to support the MQTT keep-alive pings. By adding a pending job for a PINGREQ we can tap into the regular cycle of testing for packets needing a resend; the ping job pushes out a ping and resets it's next-required time. " 14 | 15 | ] 16 | 17 | { #category : #mqtt } 18 | MQTTPendingPingJob >> completeFor: anMQTTClient [ 19 | 20 | "I do nothing here; I'm so very special" 21 | 22 | ] 23 | 24 | { #category : #mqtt } 25 | MQTTPendingPingJob >> initialize [ 26 | 27 | sendTime := Time totalSeconds 28 | ] 29 | 30 | { #category : #mqtt } 31 | MQTTPendingPingJob >> packetID [ 32 | 33 | "return a too-big value for real packets so the #newPacketID check never 'finds' this" 34 | 35 | ^16r10000 36 | ] 37 | 38 | { #category : #mqtt } 39 | MQTTPendingPingJob >> resendFor: anMQTTClient ifNeededAtTime: seconds [ 40 | 41 | "If seconds (which is the actual time - the retryTime currently in use) > my last sendTime, tell the client to ping and update the sendTime. #ping returns a good time to re-ping" 42 | 43 | seconds >= sendTime 44 | ifTrue:[sendTime := anMQTTClient ping] 45 | ] 46 | -------------------------------------------------------------------------------- /src/MQTT/MQTTPendingPubAckJob.class.st: -------------------------------------------------------------------------------- 1 | " 2 | Pending, on a publish failure, try again with the DUP bit set ! 3 | " 4 | Class { 5 | #name : #MQTTPendingPubAckJob, 6 | #superclass : #MQTTPendingJob, 7 | #category : #MQTT 8 | } 9 | 10 | { #category : #mqtt } 11 | MQTTPendingPubAckJob class >> commentOriginal [ 12 | 13 | "A PUBLISH packet with qos=1 has been sent and we expect a corresponding PUBACK sometime soon. 14 | An MQTTPendingPubAckJob is used to record that expectation; it also holds the original PUBLISH in case we need to re-send it - and we set the DUP flag to true when we get the original packet in order to be ready." 15 | 16 | ] 17 | 18 | { #category : #mqtt } 19 | MQTTPendingPubAckJob >> isPubAckID: msgID [ 20 | 21 | "am I a pending PUBACK job for msgId?" 22 | 23 | ^self packetID = msgID 24 | ] 25 | 26 | { #category : #mqtt } 27 | MQTTPendingPubAckJob >> originalPacket [ 28 | 29 | "I need to set the DUP bit in case of a resend" 30 | 31 | originalPacket prepareForResend. 32 | ^originalPacket 33 | ] 34 | 35 | { #category : #mqtt } 36 | MQTTPendingPubAckJob >> originalPacketDoNotTouchDupFlag: mqttSubscribePacket [ 37 | 38 | "don't touch dup flag" 39 | 40 | super originalPacket: mqttSubscribePacket. 41 | ] 42 | -------------------------------------------------------------------------------- /src/MQTT/MQTTPendingPubCompJob.class.st: -------------------------------------------------------------------------------- 1 | " 2 | Pending, on a publish failure, try again with a MQTTPacketPubRel 3 | " 4 | Class { 5 | #name : #MQTTPendingPubCompJob, 6 | #superclass : #MQTTPendingJob, 7 | #category : #MQTT 8 | } 9 | 10 | { #category : #mqtt } 11 | MQTTPendingPubCompJob class >> commentOriginal [ 12 | 13 | "A PUBLISH packet with qos=2 has been sent, the PUBREC has arrived, we have sent the PUBREL and we expect a corresponding PUBCOMP soon. 14 | An MQTTPendingPubCompJob is used to record that expectation; it also holds the original PUBREL in case we need to re-send it. Sending the PUBCOMP is the end of the dance." 15 | 16 | ] 17 | 18 | { #category : #mqtt } 19 | MQTTPendingPubCompJob >> isPubCompID: msgID [ 20 | 21 | "am I a pending PUBCOMP job for msgId?" 22 | 23 | ^self packetID = msgID 24 | ] 25 | 26 | { #category : #mqtt } 27 | MQTTPendingPubCompJob >> resendFor: anMQTTClient ifNeededAtTime: seconds [ 28 | 29 | "If seconds (which is the actual time - the retryTime currently in use) > my last sendTime, resend my original packet with any required changes and update that sendTime" 30 | 31 | seconds >= sendTime 32 | ifTrue:[ 33 | sendTime := Time totalSeconds. 34 | self debugLog: '*******************resendFor MQTTPacketPubRel for ' , originalPacket packetID asString. 35 | anMQTTClient statPerform: #resendType: with: MQTTPacketPubRel asString. 36 | anMQTTClient sendPacket: (MQTTPacketPubRel new packetID: originalPacket packetID)] "[MQTT-2.3.1-3]" 37 | ] 38 | -------------------------------------------------------------------------------- /src/MQTT/MQTTPendingPubRecJob.class.st: -------------------------------------------------------------------------------- 1 | " 2 | Pending, on a publish failure, try again 3 | " 4 | Class { 5 | #name : #MQTTPendingPubRecJob, 6 | #superclass : #MQTTPendingJob, 7 | #category : #MQTT 8 | } 9 | 10 | { #category : #mqtt } 11 | MQTTPendingPubRecJob class >> commentOriginal [ 12 | 13 | "A PUBLISH packet with qos=2 has been sent and we expect a corresponding PUBREC sometime soon. 14 | An MQTTPendingPubRecJob is used to record that expectation; it also holds the original PUBLISH in case we need to re-send it - and we set the DUP flag to true when we get the original packet in order to be ready. Once we get the PUBREC we send back a PUBREL packet and do a little dance" 15 | 16 | ] 17 | 18 | { #category : #mqtt } 19 | MQTTPendingPubRecJob >> completeFor: anMQTTClient [ 20 | 21 | "we're done; close me down, release me and *not* my ID to our rest" 22 | 23 | anMQTTClient releasePendingJob: self 24 | ] 25 | 26 | { #category : #mqtt } 27 | MQTTPendingPubRecJob >> isPubRecID: msgID [ 28 | 29 | "am I a pending PUBREC job for msgId?" 30 | 31 | ^self packetID = msgID 32 | ] 33 | 34 | { #category : #mqtt } 35 | MQTTPendingPubRecJob >> originalPacket [ 36 | 37 | "I need to set the DUP bit in case of a resend" 38 | 39 | originalPacket prepareForResend. 40 | ^originalPacket 41 | ] 42 | 43 | { #category : #mqtt } 44 | MQTTPendingPubRecJob >> originalPacketDoNotTouchDupFlag: mqttSubscribePacket [ 45 | 46 | "don't touch dup flag" 47 | 48 | super originalPacket: mqttSubscribePacket. 49 | ] 50 | -------------------------------------------------------------------------------- /src/MQTT/MQTTPendingPubRelJob.class.st: -------------------------------------------------------------------------------- 1 | " 2 | Pending, on a publish failure, try again 3 | " 4 | Class { 5 | #name : #MQTTPendingPubRelJob, 6 | #superclass : #MQTTPendingJob, 7 | #category : #MQTT 8 | } 9 | 10 | { #category : #mqtt } 11 | MQTTPendingPubRelJob class >> commentOriginal [ 12 | 13 | "A PUBLISH packet with qos=2 has been receieved and we have sent the appropriate PUBREC acknowledgment and now we expect a corresponding PUBREL sometime. 14 | An MQTTPendingPubRelJob is used to record that expectation; it also holds the original PUBREC in case we need to re-send it. Once we get the PUBREL we send back a PUBCOMP packet and conclude our little dance" 15 | 16 | ] 17 | 18 | { #category : #mqtt } 19 | MQTTPendingPubRelJob >> isPubRelID: msgID [ 20 | 21 | "am I a pending PUBREL job for msgId?" 22 | 23 | ^self packetID = msgID 24 | ] 25 | 26 | { #category : #mqtt } 27 | MQTTPendingPubRelJob >> resendFor: anMQTTClient ifNeededAtTime: seconds [ 28 | 29 | "If seconds (which is the actual time - the retryTime currently in use) > my last sendTime, resend my original packet with any required changes and update that sendTime" 30 | 31 | seconds >= sendTime 32 | ifTrue:[ 33 | sendTime := Time totalSeconds. 34 | self debugLog: '*******************resendFor MQTTPacketPubRec for ' , originalPacket packetID asString. 35 | anMQTTClient statPerform: #resendType: with: MQTTPacketPubRec asString. 36 | anMQTTClient sendPacket: (MQTTPacketPubRec new packetID: originalPacket packetID)] 37 | ] 38 | -------------------------------------------------------------------------------- /src/MQTT/MQTTPendingSubAckJob.class.st: -------------------------------------------------------------------------------- 1 | " 2 | Pending, on a subscription failure, just ignore 3 | " 4 | Class { 5 | #name : #MQTTPendingSubAckJob, 6 | #superclass : #MQTTPendingJob, 7 | #category : #MQTT 8 | } 9 | 10 | { #category : #mqtt } 11 | MQTTPendingSubAckJob class >> commentOriginal [ 12 | 13 | "A MQTTPendingSubAckJob is how we wait for an acknowledgement of a SUBSCRIBE. We need to check the returned error codes for each subscription we sent. 14 | 15 | It looks like we don't resend these." 16 | 17 | ] 18 | 19 | { #category : #mqtt } 20 | MQTTPendingSubAckJob >> completeFor: anMQTTClient [ 21 | 22 | "we're done; close me down, release me and my ID to our rest" 23 | 24 | super completeFor: anMQTTClient. 25 | 26 | "Now do the checking of the return codes" 27 | self debugLog: 'suback return codes need handling'. 28 | 29 | "PayLoad will contain 0x00 - Success - Maximum QoS 0 30 | 31 | 32 | 0x01 - Success - Maximum QoS 1 33 | 34 | 35 | 0x02 - Success - Maximum QoS 2 36 | 37 | 38 | 0x80 - Failure " 39 | ] 40 | 41 | { #category : #mqtt } 42 | MQTTPendingSubAckJob >> isSubAckID: msgID [ 43 | 44 | "am I a pending SUBACK job for msgId?" 45 | 46 | ^self packetID = msgID 47 | ] 48 | 49 | { #category : #mqtt } 50 | MQTTPendingSubAckJob >> resendFor: anMQTTClient ifNeededAtTime: seconds [ 51 | 52 | "do nothing for this packet" 53 | 54 | anMQTTClient statPerform: #resendType: with: MQTTPacketSubAck asString. 55 | ] 56 | -------------------------------------------------------------------------------- /src/MQTT/MQTTPendingUnsubAckJob.class.st: -------------------------------------------------------------------------------- 1 | " 2 | Pending, on a unsubscription failure, just ignore 3 | " 4 | Class { 5 | #name : #MQTTPendingUnsubAckJob, 6 | #superclass : #MQTTPendingJob, 7 | #category : #MQTT 8 | } 9 | 10 | { #category : #mqtt } 11 | MQTTPendingUnsubAckJob class >> commentOriginal [ 12 | 13 | "A MQTTPendingUnsubAckJob is how we wait for an acknowledgement of an UNSUBSCRIBE. 14 | 15 | It looks like we don't resend these." 16 | 17 | ] 18 | 19 | { #category : #mqtt } 20 | MQTTPendingUnsubAckJob >> isUnsubAckID: msgID [ 21 | 22 | "am I a pending UNSUBACK job for msgId?" 23 | 24 | ^self packetID = msgID 25 | ] 26 | 27 | { #category : #mqtt } 28 | MQTTPendingUnsubAckJob >> resendFor: anMQTTClient ifNeededAtTime: seconds [ 29 | 30 | "do nothing for this packet" 31 | 32 | anMQTTClient statPerform: #resendType: with: MQTTPacketUnsubAck asString. 33 | ] 34 | -------------------------------------------------------------------------------- /src/MQTT/MQTTServerInterface.class.st: -------------------------------------------------------------------------------- 1 | " 2 | (MQTTServerInterface openOnPort: 1883) start inspect. 3 | 4 | 5 | This class is based on work by John M McIntosh, Corporate Smalltalk Consulting Ltd for LabWare Inc. 6 | 7 | Copyright 2018, 2019. Corporate Smalltalk Consulting Ltd. 8 | Copyright 2018, 2019. LabWare Inc. 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ""Software""), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | " 16 | Class { 17 | #name : #MQTTServerInterface, 18 | #superclass : #Object, 19 | #instVars : [ 20 | 'socketDaemon', 21 | 'socketServers', 22 | 'clientIDs', 23 | 'retainedPackets', 24 | 'retainedPacketMutex', 25 | 'abort', 26 | 'timeOutProcess', 27 | 'socketServerMutex', 28 | 'clientIDMutex', 29 | 'statisticsMutex', 30 | 'statistics' 31 | ], 32 | #category : #MQTT 33 | } 34 | 35 | { #category : #mqtt } 36 | MQTTServerInterface class >> openOnPort: p [ 37 | 38 | "open listener on port" 39 | | instance | 40 | 41 | instance := self new. 42 | instance openOnPort: p. 43 | ^instance 44 | ] 45 | 46 | { #category : #mqtt } 47 | MQTTServerInterface >> abort [ 48 | 49 | "Answer my 'abort' instance variable." 50 | 51 | ^abort 52 | ] 53 | 54 | { #category : #mqtt } 55 | MQTTServerInterface >> addNewClientIDUsingTransport: newTransport 56 | withConnectPacket: aConnectPacket 57 | passingConnAck: aConnectAckPacket [ 58 | 59 | "add the client" 60 | | client possibleClient aClientID | 61 | 62 | "[MQTT-3.1.3-2] [MQTT-4.1.0-1] [MQTT-3.1.4-3]" 63 | self statPerform: #addConnectPacket: with: aConnectPacket. 64 | aClientID := aConnectPacket clientIDString. 65 | possibleClient := clientIDMutex critical: [ 66 | self clientIDs at: aClientID ifAbsent: [ 67 | client := MQTTClientIDHolder with: aClientID transport: newTransport connectPacket: aConnectPacket. 68 | self clientIDs at: aClientID put: client. 69 | self statPerform: #sentPackets. 70 | newTransport sendPacket: aConnectAckPacket. 71 | self forwardRetainMessagesTo: client usingPossibleSubscription: nil. "[MQTT-3.3.1-6]" 72 | ^self]]. 73 | 74 | "client exists let see if the same socket?" 75 | ((possibleClient transport notNil and: [possibleClient transport abort not]) and: 76 | [self halt. 77 | false] 78 | "possibleClient transport socketServer socket descriptor = newTransport socketServer socket descriptor]" 79 | ) 80 | ifTrue:[ 81 | newTransport debugLog: 'possible violation of [MQTT-3.1.0-2]'. 82 | self statPerform: #killSocket. 83 | newTransport socketServer socket close. "[MQTT-3.1.0-2]. [MQTT-3.1.4-2] " 84 | ^self]. 85 | 86 | " [MQTT-3.1.2-4]." 87 | [possibleClient transport sockStrm close] on: Error do: [:ex | ]. 88 | possibleClient cleanSession: aConnectPacket cleanSessionFlag withNewTransport: newTransport passingConnAck: aConnectAckPacket. 89 | possibleClient connectPacket: aConnectPacket. 90 | possibleClient transport: newTransport. 91 | ] 92 | 93 | { #category : #mqtt } 94 | MQTTServerInterface >> addNewSocketServer: aSocketServer [ 95 | 96 | "add new server" 97 | 98 | self statPerform: #addServer. 99 | aSocketServer transport serverInterface: self. 100 | aSocketServer transport socketServer: aSocketServer. 101 | socketServerMutex critical: [self socketServers add: aSocketServer] 102 | ] 103 | 104 | { #category : #mqtt } 105 | MQTTServerInterface >> addNewSubscription: aSubscribePacket forClientID: aClientID [ 106 | 107 | "subscribe logic" 108 | | possibleClient | 109 | 110 | self statPerform: #addSubscription: with: aSubscribePacket. 111 | possibleClient := clientIDMutex critical: [self clientIDs at: aClientID ifAbsent: [^false]]. 112 | possibleClient addSubscription: aSubscribePacket. 113 | ^true 114 | ] 115 | 116 | { #category : #mqtt } 117 | MQTTServerInterface >> checkSocketServersForTimeOut [ 118 | "check for zombies" 119 | 120 | | transport targets timeAgo | 121 | 122 | socketServerMutex 123 | critical: [ targets := self socketServers 124 | select: [ :e | 125 | transport := e transport. 126 | transport 127 | ifNil: [ false ] 128 | ifNotNil: [ timeAgo := Time primUTCSecondsClock - transport lastPingTime. 129 | "[MQTT-3.1.2-24]" 130 | transport keepAliveTime > 0 and: [ timeAgo > (1.5 * transport keepAliveTime) ] ] ] ]. 131 | targets 132 | do: [ :e | 133 | [ self statPerform: #timeOut. 134 | e debugLog: 'Kill Client based on Ping TimeOut'. 135 | e disconnect ] fork ] 136 | ] 137 | 138 | { #category : #mqtt } 139 | MQTTServerInterface >> clientIDs [ 140 | 141 | "Answer my 'clientIDs' instance variable." 142 | 143 | ^clientIDs 144 | ] 145 | 146 | { #category : #mqtt } 147 | MQTTServerInterface >> clientIDs: aValue [ 148 | 149 | "Set my 'clientIDs' instance variable to aValue." 150 | 151 | clientIDs := aValue 152 | ] 153 | 154 | { #category : #mqtt } 155 | MQTTServerInterface >> disableWillLogic: aClientIDString [ 156 | 157 | "disable the will logic" 158 | | possibleClient | 159 | 160 | "[MQTT-3.1.2-8]" 161 | possibleClient := clientIDMutex critical: [self clientIDs at: aClientIDString ifAbsent: [^false]]. 162 | possibleClient connectPacket willFlag: false. 163 | ^true 164 | ] 165 | 166 | { #category : #mqtt } 167 | MQTTServerInterface >> forwardRetainMessagesTo: aClientID usingPossibleSubscription: aSubscription [ 168 | 169 | "forward retained messages" 170 | 171 | (retainedPacketMutex critical: [self retainedPackets values]) do: [:v | 172 | self handlePublishedPacket: v usingTransport: nil forClients: (Array with: aClientID) usingSubscriptions: aSubscription tagWithRetainFlag: true]. "[MQTT-3.3.1-8]" 173 | ] 174 | 175 | { #category : #mqtt } 176 | MQTTServerInterface >> handlePublishedPacket: aPublishPacket usingTransport: aTransport [ 177 | 178 | "add new server" 179 | | existingPacket | 180 | 181 | "Handle [MQTT-3.3.2-2]." 182 | 183 | "[MQTT-3.3.5-2]. has no meaning for us" 184 | aPublishPacket retain = true 185 | ifTrue:[ " [MQTT-3.3.1-12]. [MQTT-3.1.2.7] " 186 | existingPacket := retainedPacketMutex critical: [ 187 | retainedPackets at: aPublishPacket topic ifAbsentPut: [aPublishPacket]]. "[MQTT-3.3.1-5]. [MQTT-3.3.1-7]." 188 | (((aPublishPacket message isNil) or: [aPublishPacket message size = 0])) 189 | ifTrue:[retainedPackets removeKey: aPublishPacket topic]. " [MQTT-3.3.1-10] [MQTT-3.3.1-11]." 190 | ]. 191 | self handlePublishedPacket: aPublishPacket usingTransport: aTransport forClients: (clientIDMutex critical: [self clientIDs values]) usingSubscriptions: nil tagWithRetainFlag: false. " [MQTT-3.3.1-9]." 192 | ] 193 | 194 | { #category : #mqtt } 195 | MQTTServerInterface >> handlePublishedPacket: aPublishPacket 196 | usingTransport: aTransport 197 | forClients: aClientIDs 198 | usingSubscriptions: subcriptions 199 | tagWithRetainFlag: aRetainFlag [ 200 | 201 | "add new server" 202 | | subscribers | 203 | 204 | "[MQTT-4.5.0-1] [MQTT-4.6.0-6]" 205 | aClientIDs do: [:client | 206 | subscribers := subcriptions. 207 | subscribers ifNil: [subscribers := client subscriptionsMutex critical: [client subscriptions copy]]. "make copy to solve race on add/remove subscriptions" 208 | subscribers associationsDo: [:assoc | 209 | aPublishPacket matchesSubscription: assoc value ifTrue: [:t :m :q | "[MQTT-3.3.2-3]." 210 | self sendPacket: aPublishPacket copy to: client retainFlag: aRetainFlag qos: (assoc value qos min: aPublishPacket qos)]]]. 211 | ] 212 | 213 | { #category : #mqtt } 214 | MQTTServerInterface >> initialize [ 215 | 216 | "setup" 217 | 218 | clientIDs := Dictionary new. 219 | socketDaemon := nil. 220 | socketServers := Set new. 221 | retainedPackets := Dictionary new. 222 | abort := false. 223 | socketServerMutex := Semaphore forMutualExclusion. 224 | clientIDMutex := Semaphore forMutualExclusion. 225 | retainedPacketMutex := Semaphore forMutualExclusion. 226 | statistics := MQTTStatistics new. 227 | statisticsMutex := Semaphore forMutualExclusion. 228 | self initializeTimeOutProcess. 229 | ] 230 | 231 | { #category : #mqtt } 232 | MQTTServerInterface >> initializeTimeOutProcess [ 233 | 234 | "Check for time outs on clients" 235 | 236 | timeOutProcess := [| loopDelay|loopDelay := Delay forSeconds: 10. 237 | [ 238 | loopDelay wait. 239 | self abort 240 | ifFalse:[self checkSocketServersForTimeOut]. 241 | self abort] whileFalse] forkNamed: 'MQTT server timeout loop' 242 | ] 243 | 244 | { #category : #mqtt } 245 | MQTTServerInterface >> openOnPort: p [ 246 | 247 | "open on port" 248 | 249 | socketDaemon := MQTTSocketDaemon openOnPort: p serverClass: MQTTSocketServer interface: self. 250 | ] 251 | 252 | { #category : #mqtt } 253 | MQTTServerInterface >> removeSocketServer: aSocketServer [ 254 | 255 | "remove server" 256 | | possibleClient cp | 257 | 258 | "[MQTT-3.14.4-1] [MQTT-4.1.0-2]" 259 | self statPerform: #removeServer. 260 | socketServerMutex critical: [self socketServers remove: aSocketServer]. 261 | possibleClient := clientIDMutex critical: [ 262 | self clientIDs at: aSocketServer transport clientIDString ifAbsent: [^false]]. 263 | possibleClient release. 264 | cp := possibleClient connectPacket. 265 | cp willFlag "[MQTT-3.1.2-12]" 266 | ifTrue:[ "[MQTT-3.1.2-8] [MQTT-3.1.2-10]" 267 | | pubPacket|pubPacket := MQTTPacketPublish new 268 | topic: cp willTopic message: cp willMessage asByteArray; 269 | messageID: ( 270 | (cp willQos > 0) 271 | ifTrue:[9999] 272 | ifFalse:[0]); 273 | qos: cp willQos. "[MQTT-3.1.2-16] [MQTT-3.1.2-17]" 274 | self handlePublishedPacket: pubPacket usingTransport: nil. 275 | cp willFlag: false]. 276 | ^true 277 | ] 278 | 279 | { #category : #mqtt } 280 | MQTTServerInterface >> removeSubscriptions: aUnsubscribePacket forClientID: aClientID [ 281 | 282 | "unsubscribe logic" 283 | | possibleClient | 284 | 285 | self statPerform: #removeSubscription: with: aUnsubscribePacket. 286 | possibleClient := clientIDMutex critical: [self clientIDs at: aClientID ifAbsent: [^false]]. 287 | possibleClient removeSubscription: aUnsubscribePacket. 288 | ^true 289 | ] 290 | 291 | { #category : #mqtt } 292 | MQTTServerInterface >> retainedPackets [ 293 | 294 | "Answer my 'retainedPackets' instance variable." 295 | 296 | ^retainedPackets 297 | ] 298 | 299 | { #category : #mqtt } 300 | MQTTServerInterface >> sendPacket: packet to: client retainFlag: aRetainFlag qos: aQos [ 301 | 302 | "send the packet" 303 | 304 | packet duplicateFlag: false. " [MQTT-3.3.1-3]. [MQTT-3.3.1-2]" 305 | packet retainFlag: aRetainFlag. 306 | packet qos: aQos. " [MQTT-3.8.4-5]. [MQTT-3.3.5-1] [MQTT-4.3.1-1]" 307 | client transport isNil 308 | ifTrue:[ 309 | self statPerform: #queuedPackets. 310 | client outgoingPacketQueue nextPut: packet] "[MQTT-3.1.2-5]" 311 | ifFalse:[ 312 | self statPerform: #sentPackets. 313 | client transport handlePublishResponse: packet] 314 | ] 315 | 316 | { #category : #mqtt } 317 | MQTTServerInterface >> sessionPresentViaConnectPacket: aConnectPacket [ 318 | 319 | "check for session existing" 320 | 321 | clientIDMutex critical: [| pastSession|pastSession := self clientIDs includesKey: aConnectPacket clientIDString. 322 | ^pastSession and: [aConnectPacket cleanSessionFlag not]]. "[MQTT-3.2.2-1]" 323 | 324 | "[MQTT-3.2.2-2]" 325 | ] 326 | 327 | { #category : #mqtt } 328 | MQTTServerInterface >> socketDaemon [ 329 | 330 | "Answer my 'socketDaemon' instance variable." 331 | 332 | ^socketDaemon 333 | ] 334 | 335 | { #category : #mqtt } 336 | MQTTServerInterface >> socketDaemon: aValue [ 337 | 338 | "Set my 'socketDaemon' instance variable to aValue." 339 | 340 | socketDaemon := aValue 341 | ] 342 | 343 | { #category : #mqtt } 344 | MQTTServerInterface >> socketServers [ 345 | 346 | "Answer my 'socketServers' instance variable." 347 | 348 | ^socketServers 349 | ] 350 | 351 | { #category : #mqtt } 352 | MQTTServerInterface >> start [ 353 | 354 | "start the listener" 355 | 356 | socketDaemon isNil 357 | ifFalse:[socketDaemon start] 358 | ] 359 | 360 | { #category : #mqtt } 361 | MQTTServerInterface >> statPerform: selector [ 362 | 363 | "consolidate error handler" 364 | 365 | statisticsMutex critical: [[statistics perform: selector] on: Error do: [:ex | ]]. 366 | ] 367 | 368 | { #category : #mqtt } 369 | MQTTServerInterface >> statPerform: selector with: arg [ 370 | 371 | "consolidate error handler" 372 | 373 | statisticsMutex critical: [[statistics perform: selector with: arg] on: Error do: [:ex | ]]. 374 | ] 375 | 376 | { #category : #mqtt } 377 | MQTTServerInterface >> stop [ 378 | 379 | "stop the listener" 380 | 381 | self stopSocketDaemon. 382 | socketServerMutex critical: [ 383 | self socketServers do: [:s | s disconnect]. 384 | abort := true. 385 | timeOutProcess terminate. 386 | timeOutProcess := nil]. 387 | clientIDs := Dictionary new. 388 | socketServers := Set new. 389 | retainedPackets := Dictionary new. 390 | ] 391 | 392 | { #category : #mqtt } 393 | MQTTServerInterface >> stopAcceptingConnections [ 394 | 395 | "Stop listening for connections" 396 | 397 | self stopSocketDaemon 398 | ] 399 | 400 | { #category : #mqtt } 401 | MQTTServerInterface >> stopSocketDaemon [ 402 | 403 | "stop accepting connections but continue to process any existing connections." 404 | 405 | socketDaemon isNil 406 | ifTrue:[^self]. 407 | socketDaemon stop. 408 | socketDaemon := nil 409 | ] 410 | -------------------------------------------------------------------------------- /src/MQTT/MQTTSocketClient.class.st: -------------------------------------------------------------------------------- 1 | " 2 | This class interfaces to a SocketStream Object. In VSE this is a more complicated object as it was a subclass of the 1995 VSE SocketClass 3 | " 4 | Class { 5 | #name : #MQTTSocketClient, 6 | #superclass : #Object, 7 | #instVars : [ 8 | 'transport', 9 | 'cleanSessionFlag', 10 | 'willTopic', 11 | 'willMessage', 12 | 'willRetainFlag', 13 | 'willQos', 14 | 'userNameString', 15 | 'passwordString', 16 | 'keepAliveTime', 17 | 'socket', 18 | 'interface' 19 | ], 20 | #category : #MQTT 21 | } 22 | 23 | { #category : #mqtt } 24 | MQTTSocketClient class >> openOnHostName: hostname keepAlive: aSeconds interface: anInterface [ 25 | 26 | ^self openOnHostName: hostname port: 1883 keepAlive: aSeconds interface: anInterface 27 | ] 28 | 29 | { #category : #mqtt } 30 | MQTTSocketClient class >> openOnHostName: hostname port: portNumber keepAlive: aSeconds interface: anInterface [ 31 | 32 | "Create an instance on the provided host and port" 33 | | instance sock addr | 34 | 35 | 36 | addr := NetNameResolver addressForName: hostname timeout: aSeconds. 37 | instance := self new. 38 | sock := Socket newTCP. 39 | sock connectTo: addr port: portNumber. 40 | sock waitForConnectionFor: aSeconds. 41 | instance keepAliveTime: aSeconds. 42 | instance interface: anInterface. 43 | instance acceptFrom: sock. 44 | ^instance 45 | ] 46 | 47 | { #category : #mqtt } 48 | MQTTSocketClient >> acceptFrom: aSocket [ 49 | "start the socket" 50 | 51 | self debugLog: 'MQTTSocketClient saw connectedOn: on socket'. 52 | transport isNil 53 | ifTrue: [ transport := MQTTTransportLayerClient new. 54 | transport keepAliveTime: self keepAliveTime. 55 | transport socketClient: self. 56 | transport start: aSocket. 57 | self initializeMQTTConnection: true] 58 | ifFalse: [ transport restart: aSocket. 59 | self initializeMQTTConnection: false]. 60 | 61 | ] 62 | 63 | { #category : #mqtt } 64 | MQTTSocketClient >> badWill [ 65 | 66 | "raise error" 67 | 68 | self debugLog: 'badWill'. 69 | self halt: 'Bad Will topic, message etc' 70 | ] 71 | 72 | { #category : #mqtt } 73 | MQTTSocketClient >> close [ 74 | 75 | "close logic" 76 | 77 | self debugLog: 'MQTTSocketClient saw close on socket'. 78 | 79 | self transport ifNotNil: [self transport disconnect: true]. 80 | self release. 81 | ] 82 | 83 | { #category : #mqtt } 84 | MQTTSocketClient >> debugLog: aString [ 85 | 86 | "debug data" 87 | 88 | MQTTClientInterface debugLog: self printString tag: ' TC ' str2: aString 89 | ] 90 | 91 | { #category : #mqtt } 92 | MQTTSocketClient >> disconnect [ 93 | 94 | "disconnect on purpose" 95 | 96 | self transport ifNotNil: [self transport disconnect: false]. 97 | ] 98 | 99 | { #category : #mqtt } 100 | MQTTSocketClient >> initializeMQTTConnection: cleanSessionTrue [ 101 | 102 | "assemble a CONNECT packet and send it [MQTT-3.1.0-1]" 103 | | connectPacket | 104 | 105 | connectPacket := MQTTPacketConnect new keepAliveTime: self keepAliveTime. 106 | 107 | "do I want to set the will data?" 108 | (willTopic isString & willMessage isString & willRetainFlag isBoolean & willQos isInteger) 109 | ifTrue:[ 110 | connectPacket willTopic: willTopic message: willMessage retain: willRetainFlag qos: willQos]. 111 | 112 | "If CleanSession is set to 0, the Server MUST resume communications with the Client based on state from the current Session (as identified by the Client identifier). If there is no Session associated with the Client identifier the Server MUST create a new Session. The Client and Server MUST store the Session after the Client and Server are disconnected [MQTT-3.1.2-4]. After the disconnection of a Session that had CleanSession set to 0, the Server MUST store further QoS 1 and QoS 2 messages that match any subscriptions that the client had at the time of disconnection as part of the Session state [MQTT-3.1.2-5]. It MAY also store QoS 0 messages that meet the same criteria. 113 | 114 | If CleanSession is set to 1, the Client and Server MUST discard any previous Session and start a new one. This Session lasts as long as the Network Connection. State data associated with this Session MUST NOT be reused in any subsequent Session [MQTT-3.1.2-6]. 115 | " 116 | cleanSessionFlag := cleanSessionTrue. 117 | connectPacket cleanSession: cleanSessionFlag. 118 | 119 | "do I need a username&|password? Nils are handled by the packet" 120 | connectPacket user: userNameString password: passwordString. 121 | 122 | "set the clientID by which the server knows us - see 3.1.3 123 | 124 | 125 | It defaults to SqueakMQTT but should be set separately by each user to keep it unique" 126 | connectPacket clientID: self transport clientIDString. 127 | cleanSessionTrue 128 | ifTrue: 129 | [" [MQTT-3.1.0-1] " 130 | self transport outgoingPacketQueue: SharedQueue new. "clear outgoing packet queue" 131 | self transport packetInFlightQueue: SharedQueue new. "clear inflight queue" 132 | self transport sendPacket: connectPacket] 133 | ifFalse:[| pifq| 134 | pifq := SharedQueue new. 135 | pifq nextPut: connectPacket. 136 | pifq nextPutAll: self transport packetInFlightQueue contents. 137 | self transport packetInFlightQueue: pifq]. 138 | ] 139 | 140 | { #category : #mqtt } 141 | MQTTSocketClient >> interface [ 142 | 143 | "Answer my 'interface' instance variable." 144 | 145 | ^interface 146 | ] 147 | 148 | { #category : #mqtt } 149 | MQTTSocketClient >> interface: aValue [ 150 | 151 | "Set my 'interface' instance variable to aValue." 152 | 153 | interface := aValue. 154 | transport := interface socketClient ifNotNil: [transport := interface socketClient transport]. 155 | ] 156 | 157 | { #category : #mqtt } 158 | MQTTSocketClient >> keepAliveTime [ 159 | 160 | "Answer my 'keepAliveTime' instance variable." 161 | 162 | ^keepAliveTime 163 | ] 164 | 165 | { #category : #mqtt } 166 | MQTTSocketClient >> keepAliveTime: aValue [ 167 | 168 | "Set my 'keepAliveTime' instance variable to aValue." 169 | 170 | keepAliveTime := aValue 171 | ] 172 | 173 | { #category : #mqtt } 174 | MQTTSocketClient >> onTopic: topicString qos: qos do: aBlock [ 175 | 176 | "a basic subscribe and do something message. We must check the topicString's acceptability and fail if there are issues" 177 | 178 | self transport onTopic: topicString qos: qos do: aBlock 179 | ] 180 | 181 | { #category : #mqtt } 182 | MQTTSocketClient >> printOn: aStream [ 183 | 184 | "print useful data" 185 | 186 | super printOn: aStream. 187 | aStream nextPutAll: ' socket: '. 188 | ] 189 | 190 | { #category : #mqtt } 191 | MQTTSocketClient >> publishTopic: aTopic message: msgString qos: qos retain: retainFlag [ 192 | 193 | "publish the msgString to the connected broker. If qos > 0 we'll need to schedule a pending job for the ack sequence(s)" 194 | 195 | self transport publishTopic: aTopic message: msgString qos: qos retain: retainFlag 196 | ] 197 | 198 | { #category : #mqtt } 199 | MQTTSocketClient >> readWaitTime: aSeconds [ 200 | 201 | "set read time out" 202 | 203 | self transport readWaitTime: aSeconds 204 | ] 205 | 206 | { #category : #mqtt } 207 | MQTTSocketClient >> release [ 208 | 209 | "release logic" 210 | 211 | interface := nil. 212 | transport := nil. 213 | ] 214 | 215 | { #category : #mqtt } 216 | MQTTSocketClient >> transport [ 217 | 218 | "Answer my 'transport' instance variable." 219 | 220 | ^transport 221 | ] 222 | 223 | { #category : #mqtt } 224 | MQTTSocketClient >> transport: aValue [ 225 | 226 | "Set my 'transport' instance variable to aValue." 227 | 228 | transport := aValue 229 | ] 230 | 231 | { #category : #mqtt } 232 | MQTTSocketClient >> unsubscribeFrom: aTopic [ 233 | 234 | "unsubscribe from aTopic - remove the subscription from currentSubscription" 235 | 236 | self transport unsubscribeFrom: aTopic 237 | ] 238 | 239 | { #category : #mqtt } 240 | MQTTSocketClient >> username: uName password: pwd [ 241 | 242 | "set the MQTT user name and password to be used by the connection. Nils are ok for either or both" 243 | 244 | uName ifNotNil: [userNameString := UTF8Encoder encode: uName asString]. 245 | pwd ifNotNil: [passwordString := UTF8Encoder encode: pwd asString] 246 | ] 247 | 248 | { #category : #mqtt } 249 | MQTTSocketClient >> willTopic: topicString message: messageString retain: retainBoolean qos: qosValue [ 250 | 251 | "setup a will message. We must have a topic, the actual message, a retain flag and requested QOS" 252 | 253 | ((topicString isString & messageString isString & (retainBoolean = true | retainBoolean = false)) & (qosValue isInteger & (qosValue between: 0 and: 2))) 254 | ifFalse:[^self badWill]. 255 | willTopic := topicString. 256 | willMessage := messageString. 257 | willRetainFlag := retainBoolean. 258 | willQos := qosValue 259 | ] 260 | -------------------------------------------------------------------------------- /src/MQTT/MQTTSocketDaemon.class.st: -------------------------------------------------------------------------------- 1 | " 2 | This class interfaces to aZnMultiThreadedServer Object. In VSE this is a more complicated object as it was a subclass of the 1995 VSE Socket Listener object 3 | " 4 | Class { 5 | #name : #MQTTSocketDaemon, 6 | #superclass : #ZnMultiThreadedServer, 7 | #instVars : [ 8 | 'interface', 9 | 'serverClass' 10 | ], 11 | #category : #MQTT 12 | } 13 | 14 | { #category : #mqtt } 15 | MQTTSocketDaemon class >> openOnPort: p serverClass: s interface: anInterface [ 16 | 17 | "A convenient way to create a daemon which simply starts a server on each connection attempt" 18 | | aDaemon | 19 | 20 | aDaemon := self new. 21 | aDaemon optionAt: #port put: p. 22 | aDaemon serverClass: s. 23 | aDaemon interface: anInterface. 24 | ^aDaemon 25 | ] 26 | 27 | { #category : #mqtt } 28 | MQTTSocketDaemon >> defaultPort [ 29 | 30 | "Answer the default port for the MQTTDaemon" 31 | 32 | ^1883 33 | ] 34 | 35 | { #category : #mqtt } 36 | MQTTSocketDaemon >> interface [ 37 | 38 | "Answer my 'interface' instance variable." 39 | 40 | ^interface 41 | ] 42 | 43 | { #category : #mqtt } 44 | MQTTSocketDaemon >> interface: aValue [ 45 | 46 | "Set my 'interface' instance variable to aValue." 47 | 48 | interface := aValue 49 | ] 50 | 51 | { #category : #mqtt } 52 | MQTTSocketDaemon >> port [ 53 | "Return the integer port number we are (or will be) listening on" 54 | 55 | ^ self optionAt: #port ifAbsent: [ self defaultPort ] 56 | ] 57 | 58 | { #category : #mqtt } 59 | MQTTSocketDaemon >> serveConnectionsOn: listeningSocket [ 60 | "We wait up to acceptWaitTimeout seconds for an incoming connection. 61 | If we get one we wrap it in a SocketStream and #executeRequestResponseLoopOn: on it" 62 | 63 | | socket server | 64 | socket := listeningSocket waitForAcceptFor: self acceptWaitTimeout. 65 | socket ifNil: [ ^ self noteAcceptWaitTimedOut ]. 66 | server := self serverClass new. 67 | server interface: interface. 68 | server acceptFrom: socket 69 | ] 70 | 71 | { #category : #mqtt } 72 | MQTTSocketDaemon >> serverClass [ 73 | 74 | "Answer the class we want to instantiate when a connection is accepted" 75 | 76 | ^serverClass 77 | ] 78 | 79 | { #category : #mqtt } 80 | MQTTSocketDaemon >> serverClass: aClass [ 81 | 82 | serverClass := aClass 83 | ] 84 | -------------------------------------------------------------------------------- /src/MQTT/MQTTSocketServer.class.st: -------------------------------------------------------------------------------- 1 | " 2 | This class interfaces to our Data Brokder . In VSE this is a more complicated object as it was a subclass of the 1995 VSE Socket object for sockets connected to the data broker. 3 | " 4 | Class { 5 | #name : #MQTTSocketServer, 6 | #superclass : #Object, 7 | #instVars : [ 8 | 'transport', 9 | 'interface' 10 | ], 11 | #category : #MQTT 12 | } 13 | 14 | { #category : #mqtt } 15 | MQTTSocketServer >> acceptFrom: aSocket [ 16 | "Let the superclass set up the server for socket events then remove the 17 | 18 | 19 | write event from those we're responding to. This server doesn't need them." 20 | 21 | self debugLog: 'MQTTServer saw Socket start:'. 22 | transport := MQTTTransportLayerServer new. "[MQTT-7.0.0-1]" 23 | transport start: aSocket. 24 | interface addNewSocketServer: self. 25 | self initializeMQTTConnection: true 26 | ] 27 | 28 | { #category : #mqtt } 29 | MQTTSocketServer >> debugLog: aString [ 30 | 31 | "debug data" 32 | 33 | MQTTClientInterface debugLog: self printString tag: ' TS ' str2: aString 34 | ] 35 | 36 | { #category : #mqtt } 37 | MQTTSocketServer >> disconnect [ 38 | 39 | "disconnect on purpose" 40 | 41 | self transport ifNotNil: [self transport disconnect: false]. 42 | transport := nil 43 | ] 44 | 45 | { #category : #mqtt } 46 | MQTTSocketServer >> initializeMQTTConnection: anObject [ 47 | 48 | "initialize" 49 | 50 | ] 51 | 52 | { #category : #mqtt } 53 | MQTTSocketServer >> interface: aValue [ 54 | 55 | "Set my 'interface' instance variable to aValue." 56 | 57 | interface := aValue 58 | ] 59 | 60 | { #category : #mqtt } 61 | MQTTSocketServer >> printOn: aStream [ 62 | 63 | "print useful data" 64 | 65 | super printOn: aStream. 66 | aStream nextPutAll: ' socket: '. 67 | ] 68 | 69 | { #category : #mqtt } 70 | MQTTSocketServer >> transport [ 71 | 72 | "Answer my 'transport' instance variable." 73 | 74 | ^transport 75 | ] 76 | 77 | { #category : #mqtt } 78 | MQTTSocketServer >> transport: aValue [ 79 | 80 | "Set my 'transport' instance variable to aValue." 81 | 82 | transport := aValue 83 | ] 84 | -------------------------------------------------------------------------------- /src/MQTT/MQTTStatistics.class.st: -------------------------------------------------------------------------------- 1 | " 2 | Collect statistics on the server & client socket/interface usage 3 | " 4 | Class { 5 | #name : #MQTTStatistics, 6 | #superclass : #Object, 7 | #instVars : [ 8 | 'pin', 9 | 'pout', 10 | 'poutMaxSize', 11 | 'poutMinSize', 12 | 'pinMaxSize', 13 | 'pinMinSize', 14 | 'pinBytes', 15 | 'poutBytes', 16 | 'pinTypeCount', 17 | 'poutTypeCount', 18 | 'errorTypes', 19 | 'connections', 20 | 'disconnect', 21 | 'connected', 22 | 'connectionTypes', 23 | 'servers', 24 | 'subscriptions', 25 | 'timeOut', 26 | 'killSocket', 27 | 'sentPackets', 28 | 'queuedPackets', 29 | 'unsubscribe', 30 | 'badConnections', 31 | 'missingType', 32 | 'resendType' 33 | ], 34 | #category : #MQTT 35 | } 36 | 37 | { #category : #mqtt } 38 | MQTTStatistics >> addConnectPacket: aConnectPacket [ 39 | 40 | "connect packet" 41 | 42 | connections := connections + 1. 43 | connected add: aConnectPacket clientIDString. 44 | aConnectPacket cleanSessionFlag 45 | ifTrue:[connectionTypes add: #clean]. 46 | aConnectPacket willFlag 47 | ifTrue:[connectionTypes add: #will]. 48 | ] 49 | 50 | { #category : #mqtt } 51 | MQTTStatistics >> addServer [ 52 | 53 | "server" 54 | 55 | servers := servers + 1. 56 | ] 57 | 58 | { #category : #mqtt } 59 | MQTTStatistics >> addSubscription: aSubscribePacket [ 60 | 61 | "server" 62 | 63 | aSubscribePacket payloadDict keys do: [:t | subscriptions add: t]. 64 | ] 65 | 66 | { #category : #mqtt } 67 | MQTTStatistics >> badConnections [ 68 | 69 | "badConnections" 70 | 71 | badConnections := badConnections + 1. 72 | ] 73 | 74 | { #category : #mqtt } 75 | MQTTStatistics >> disconnect [ 76 | 77 | "disconnect" 78 | 79 | disconnect := disconnect + 1. 80 | ] 81 | 82 | { #category : #mqtt } 83 | MQTTStatistics >> errorTypes: aMessage [ 84 | 85 | "error types" 86 | 87 | errorTypes add: aMessage 88 | ] 89 | 90 | { #category : #mqtt } 91 | MQTTStatistics >> initialize [ 92 | 93 | "zero" 94 | 95 | pin := 0. 96 | pout := 0. 97 | poutMaxSize := 0. 98 | poutMinSize := 1000000. 99 | poutBytes := 0. 100 | poutTypeCount := Bag new. 101 | pinMaxSize := 0. 102 | pinMinSize := 1000000. 103 | pinBytes := 0. 104 | pinTypeCount := Bag new. 105 | errorTypes := Bag new. 106 | connections := 0. 107 | disconnect := 0. 108 | connected := Set new. 109 | connectionTypes := Bag new. 110 | servers := 0. 111 | subscriptions := Bag new. 112 | timeOut := 0. 113 | killSocket := 0. 114 | sentPackets := 0. 115 | queuedPackets := 0. 116 | unsubscribe := 0. 117 | badConnections := 0. 118 | missingType := Bag new. 119 | resendType := Bag new. 120 | ] 121 | 122 | { #category : #mqtt } 123 | MQTTStatistics >> killSocket [ 124 | 125 | "killSocket" 126 | 127 | killSocket := killSocket + 1. 128 | ] 129 | 130 | { #category : #mqtt } 131 | MQTTStatistics >> packetIn: aPacket [ 132 | 133 | "pin" 134 | | pType | 135 | 136 | pin := pin + 1. 137 | pType := aPacket class. 138 | pinTypeCount add: pType asString asSymbol. 139 | pType = MQTTPacketPublish 140 | ifTrue:[ 141 | | sz|sz := aPacket message size. 142 | pinBytes := pinBytes + sz. 143 | pinMaxSize < sz 144 | ifTrue:[pinMaxSize := sz]. 145 | pinMinSize > sz 146 | ifTrue:[pinMinSize := sz]]. 147 | ] 148 | 149 | { #category : #mqtt } 150 | MQTTStatistics >> packetOut: aPacket [ 151 | 152 | "pin" 153 | | pType thePacket | 154 | 155 | pout := pout + 1. 156 | pType := aPacket class. 157 | thePacket := aPacket. 158 | pType = MQTTPacketAndPendingJobPair 159 | ifTrue:[ 160 | pType := aPacket packet class. 161 | thePacket := aPacket packet]. 162 | poutTypeCount add: pType asString asSymbol. 163 | pType = MQTTPacketPublish 164 | ifTrue:[ 165 | | sz|sz := thePacket message size. 166 | poutBytes := poutBytes + sz. 167 | poutMaxSize < sz 168 | ifTrue:[poutMaxSize := sz]. 169 | poutMinSize > sz 170 | ifTrue:[poutMinSize := sz]]. 171 | ] 172 | 173 | { #category : #mqtt } 174 | MQTTStatistics >> queuedPackets [ 175 | 176 | "queuedPackets" 177 | 178 | queuedPackets := queuedPackets + 1. 179 | ] 180 | 181 | { #category : #mqtt } 182 | MQTTStatistics >> removeServer [ 183 | 184 | "server" 185 | 186 | servers := servers - 1. 187 | ] 188 | 189 | { #category : #mqtt } 190 | MQTTStatistics >> removeSubscription: aUnsubscribePacket [ 191 | 192 | "aUnsubscribePacket" 193 | 194 | unsubscribe := unsubscribe + aUnsubscribePacket topics size 195 | ] 196 | 197 | { #category : #mqtt } 198 | MQTTStatistics >> resendType: aType [ 199 | 200 | "resendType:" 201 | 202 | resendType add: aType 203 | ] 204 | 205 | { #category : #mqtt } 206 | MQTTStatistics >> sentPackets [ 207 | 208 | "sentPackets" 209 | 210 | sentPackets := sentPackets + 1. 211 | ] 212 | 213 | { #category : #mqtt } 214 | MQTTStatistics >> timeOut [ 215 | 216 | "timeOut" 217 | 218 | timeOut := timeOut + 1. 219 | ] 220 | -------------------------------------------------------------------------------- /src/MQTT/MQTTSubscription.class.st: -------------------------------------------------------------------------------- 1 | " 2 | The Subscription object. Holds onto the action block and other meta-data needed for subscribing. 3 | " 4 | Class { 5 | #name : #MQTTSubscription, 6 | #superclass : #Object, 7 | #instVars : [ 8 | 'topic', 9 | 'actionBlock', 10 | 'qos', 11 | 'retainFlag' 12 | ], 13 | #category : #MQTT 14 | } 15 | 16 | { #category : #mqtt } 17 | MQTTSubscription class >> commentOriginal [ 18 | 19 | "A MQTTSubscription is how we keep track of subscribed topics and the action the user wants when a packet comes in 20 | 21 | topic - a String that we match against an incoming packet to see if we need to do anything. See the MQTT documentation for the rules about wildcards 22 | 23 | actionBlock - the Block we run if we match the topic; it gets two parameters, the topic string and the message data" 24 | 25 | ] 26 | 27 | { #category : #mqtt } 28 | MQTTSubscription class >> for: aTopic qos: qos do: aBlock [ 29 | 30 | ^super new for: aTopic qos: qos do: aBlock 31 | ] 32 | 33 | { #category : #mqtt } 34 | MQTTSubscription >> = aSubscription [ 35 | 36 | "equal" 37 | 38 | (aSubscription class = self class) 39 | ifFalse:[^false]. 40 | ^topic = aSubscription topic 41 | ] 42 | 43 | { #category : #mqtt } 44 | MQTTSubscription >> for: aTopic qos: aQos do: aBlock [ 45 | 46 | "build a response to data for aTopic being received; check the validity of aTopic (no nulls, + or # used properly etc) and 47 | return nil if there is a problem, otherwise return self" 48 | | strm priorChar currentChar nextChar count | 49 | 50 | topic := aTopic. 51 | actionBlock := aBlock. 52 | qos := aQos. 53 | priorChar := nil. 54 | count := 0. 55 | retainFlag := true. 56 | strm := aTopic readStream. 57 | [currentChar := [strm next] on: Error do: [:e | nil]] whileNotNil: [ 58 | nextChar := strm peek. 59 | currentChar = $+ 60 | ifTrue:[ 61 | ((priorChar notNil and: [priorChar ~= $/]) or: [nextChar notNil and: [nextChar ~= $/]]) 62 | ifTrue:[^nil]] 63 | ifFalse:[ 64 | currentChar = $# 65 | ifTrue:[ 66 | ((priorChar notNil and: [priorChar ~= $/]) or: [nextChar notNil]) 67 | ifTrue:[^nil]] 68 | ifFalse:[ 69 | currentChar = Character null "MQTT-4.7.3-2" 70 | ifTrue:[^nil]]]. 71 | count := count + 1. 72 | priorChar := currentChar]. 73 | count > 65535 74 | ifTrue:[^nil] 75 | ] 76 | 77 | { #category : #mqtt } 78 | MQTTSubscription >> handlePacket: aPublishPacket [ 79 | 80 | "see if the packet's topic matches mine; if so, evaluate the actionBlock" 81 | 82 | aPublishPacket matchesSubscription: self ifTrue: actionBlock 83 | ] 84 | 85 | { #category : #mqtt } 86 | MQTTSubscription >> hash [ 87 | 88 | ^topic hash 89 | ] 90 | 91 | { #category : #mqtt } 92 | MQTTSubscription >> printOn: aStream [ 93 | 94 | "print useful data" 95 | 96 | super printOn: aStream. 97 | aStream nextPutAll: ' t: '. 98 | topic asString printOn: aStream. 99 | aStream nextPutAll: ' r: '. 100 | retainFlag asString printOn: aStream. 101 | aStream nextPutAll: ' qos: '. 102 | qos asString printOn: aStream. 103 | ] 104 | 105 | { #category : #mqtt } 106 | MQTTSubscription >> qos [ 107 | 108 | "Answer my 'qos' instance variable." 109 | 110 | ^qos 111 | ] 112 | 113 | { #category : #mqtt } 114 | MQTTSubscription >> retainFlag [ 115 | 116 | "Answer my 'retainFlag' instance variable." 117 | 118 | ^retainFlag 119 | ] 120 | 121 | { #category : #mqtt } 122 | MQTTSubscription >> retainFlag: aValue [ 123 | 124 | "Set my 'retainFlag' instance variable to aValue." 125 | 126 | retainFlag := aValue 127 | ] 128 | 129 | { #category : #mqtt } 130 | MQTTSubscription >> topic [ 131 | 132 | "Answer my 'topic' instance variable." 133 | 134 | ^topic 135 | ] 136 | 137 | { #category : #mqtt } 138 | MQTTSubscription >> topic: aValue [ 139 | 140 | "Set my 'topic' instance variable to aValue." 141 | 142 | topic := aValue 143 | ] 144 | 145 | { #category : #mqtt } 146 | MQTTSubscription >> topicMatches: candidateString [ 147 | 148 | "does my topic match this topic? First version simply does a string = 149 | test, needs to actually parse the pattern properly" 150 | 151 | "^topic sameAs: candidateString" 152 | 153 | "copied from Python and C. Ugly code. Fix! 154 | See git.eclipse.org/mosquitto/org.eclipse.mosquitto.git/tree/llib/util_mosq.c for the original" 155 | | result multilevel topicLen csLen topicPos csPos | 156 | 157 | "[MQTT-3.3.2-3] [MQTT-4.7.3-4] [MQTT-4.7.2-1]" 158 | result := true. 159 | multilevel := false. 160 | topicLen := topic size. 161 | csLen := candidateString size. 162 | (topicLen > 0 and: [csLen > 0]) 163 | ifTrue:[ 164 | (topic first = $$ xor: candidateString first = $$) 165 | ifTrue:[^false]]. "testing for both or neither first char being $$" 166 | topicPos := csPos := 1. 167 | [topicPos <= topicLen and: [csPos <= csLen]] 168 | whileTrue:[ 169 | (topic at: topicPos) = (candidateString at: csPos) 170 | ifTrue:[ 171 | csPos = csLen 172 | ifTrue:[ "check for topic ending in /#" 173 | (topicPos = (topicLen - 2) and: [(topic last: 2) = '/#']) 174 | ifTrue:[^true]]. 175 | 176 | "move to next char" 177 | topicPos := topicPos + 1. 178 | csPos := csPos + 1. 179 | (csPos > csLen and: [topicPos > topicLen]) 180 | ifTrue:[^true] 181 | ifFalse:[ 182 | ((csPos > csLen and: [topicPos = topicLen]) & (topic last = $+)) 183 | ifTrue:[^true]]] 184 | ifFalse:[ 185 | (topic at: topicPos) = $+ 186 | ifTrue:[ 187 | topicPos := topicPos + 1. 188 | [csPos <= csLen and: [(candidateString at: csPos) ~= $/]] 189 | whileTrue:[csPos := csPos + 1]. 190 | (csPos > csLen and: [topicPos > topicLen]) 191 | ifTrue:[^true]] 192 | ifFalse:[ 193 | (topic at: topicPos) = $# 194 | ifTrue:[ 195 | multilevel := true. 196 | ^topicPos = topicLen] 197 | ifFalse:[^false]]]]. 198 | (multilevel not and: [csPos <= csLen or: [topicPos <= topicLen]]) 199 | ifTrue:[^false]. 200 | ^result 201 | ] 202 | -------------------------------------------------------------------------------- /src/MQTT/MQTTTransportLayer.class.st: -------------------------------------------------------------------------------- 1 | " 2 | This is the abstract class for a client or server/data broker session. It manages the ping logic, retry logic, reader, writer, and restart logic. The concrete class implement client or data broker specific logic. 3 | " 4 | Class { 5 | #name : #MQTTTransportLayer, 6 | #superclass : #Object, 7 | #instVars : [ 8 | 'sockStrm', 9 | 'abort', 10 | 'incomingPacketProcess', 11 | 'packetInFlightQueue', 12 | 'outgoingPacketQueue', 13 | 'outgoingPacketProcess', 14 | 'retryTime', 15 | 'pendingJobsMutex', 16 | 'abortMutex', 17 | 'retryProcess', 18 | 'lastMID', 19 | 'pendingJobs', 20 | 'currentSubscriptions', 21 | 'readWaitTime', 22 | 'statisticsMutex', 23 | 'statistics' 24 | ], 25 | #category : #MQTT 26 | } 27 | 28 | { #category : #mqtt } 29 | MQTTTransportLayer >> abort [ 30 | 31 | "Answer my 'abort' instance variable." 32 | 33 | ^abort 34 | ] 35 | 36 | { #category : #mqtt } 37 | MQTTTransportLayer >> addPending: mqttJobToComplete [ 38 | 39 | "Add a record of a pending job that needs completion" 40 | 41 | "self debugLog: 'add pending: ' , mqttJobToComplete asString." 42 | pendingJobsMutex critical: [ 43 | pendingJobs ifNil: [pendingJobs := OrderedCollection new]. 44 | pendingJobs add: mqttJobToComplete] 45 | ] 46 | 47 | { #category : #mqtt } 48 | MQTTTransportLayer >> badPacketID [ 49 | 50 | "fatal error" 51 | 52 | self debugLog: 'badPacketID'. 53 | self disconnect: false. "[MQTT-2.3.1-2]" 54 | ] 55 | 56 | { #category : #mqtt } 57 | MQTTTransportLayer >> badTopicError [ 58 | 59 | "raise an exception at some point; for now just halt" 60 | 61 | self debugLog: 'badTopicError'. 62 | MQTTBadTopicException signal: 'badTopicError'. 63 | ] 64 | 65 | { #category : #mqtt } 66 | MQTTTransportLayer >> badWill [ 67 | 68 | self debugLog: 'badWill'. 69 | self halt: 'Bad Will topic, message etc' 70 | ] 71 | 72 | { #category : #mqtt } 73 | MQTTTransportLayer >> currentSubscriptions [ 74 | 75 | "Answer my 'currentSubscriptions' instance variable." 76 | 77 | ^currentSubscriptions 78 | ] 79 | 80 | { #category : #mqtt } 81 | MQTTTransportLayer >> currentSubscriptions: aValue [ 82 | 83 | "Set my 'currentSubscriptions' instance variable to aValue." 84 | 85 | currentSubscriptions := aValue 86 | ] 87 | 88 | { #category : #mqtt } 89 | MQTTTransportLayer >> debugLog: aString [ 90 | 91 | "debug data" 92 | | datum | 93 | 94 | datum := sockStrm ifNotNil: [sockStrm socket printString] ifNil: ['???']. 95 | MQTTClientInterface debugLog: datum tag: self transportType str2: aString 96 | ] 97 | 98 | { #category : #mqtt } 99 | MQTTTransportLayer >> disconnect: restart [ 100 | "close the connection" 101 | 102 | [ (Delay forSeconds: 2) wait. 103 | outgoingPacketProcess notNil 104 | ifTrue: [ outgoingPacketProcess terminate ]. 105 | self debugLog: 'kill outgoingPacketProcess' ] 106 | forkAt: Processor activePriority - 1 107 | named: 'MQTTKillOutGoing'. 108 | outgoingPacketProcess := nil. 109 | [ (Delay forSeconds: 2) wait. 110 | incomingPacketProcess notNil 111 | ifTrue: [ incomingPacketProcess terminate ]. 112 | self debugLog: 'kill incomingPacketProcess' ] 113 | forkAt: Processor activePriority - 1 114 | named: 'MQTTKillIncoming'. 115 | incomingPacketProcess := nil. 116 | retryProcess 117 | ifNotNil: [ self debugLog: 'kill retryProcess'. 118 | retryProcess terminate. 119 | retryProcess := nil ] 120 | ] 121 | 122 | { #category : #mqtt } 123 | MQTTTransportLayer >> disconnectPostCleanup [ 124 | 125 | "zap socketStream" 126 | | s | 127 | 128 | s := sockStrm socket. 129 | [s ifNotNil: [[s close] on: Error do: [:ex | self debugLog: ex description]]] fork. 130 | ] 131 | 132 | { #category : #mqtt } 133 | MQTTTransportLayer >> exceptionHandler: e [ 134 | 135 | "handle exception" 136 | | connectAckPacket et | 137 | 138 | 139 | e class = MQTTEmptyStreamErrorException 140 | ifTrue:[self debugLog: 'MQTTEmptyStreamErrorException'. 141 | ^e retry]. 142 | e class = ConnectionTimedOut 143 | ifTrue:[self debugLog: 'ConnectionTimedOut'.]. 144 | et := e printString. 145 | et ifNotNil: [self debugLog: et. 146 | self statPerform: #errorTypes: with: et]. 147 | e class = MQTTCONNECTException 148 | ifTrue:[ 149 | self statPerform: #badConnections. 150 | connectAckPacket := MQTTPacketConnAck new. 151 | connectAckPacket sessionPresent: false. 152 | connectAckPacket byte2: e connectReturnCode. 153 | self sendPacket: connectAckPacket. "[MQTT-3.2.2-1] [MQTT-3.1.4-5]" 154 | (Delay forSeconds: 5) wait.]. 155 | self disconnect: self restartTheSocket. 156 | ^nil 157 | ] 158 | 159 | { #category : #mqtt } 160 | MQTTTransportLayer >> findAndRunJobsOverdueForAcknowledgement [ 161 | 162 | "Find and process the pending job(s) that needs to resend a packet" 163 | | tick | 164 | 165 | pendingJobs ifNil: [^nil]. 166 | tick := Time totalSeconds - retryTime. 167 | 168 | "looks odd but works as a way to handle possible changes in retryTime - 169 | 170 | we actually check if the last send time is more than retryTime seconds ago" 171 | pendingJobsMutex critical: [pendingJobs do: [:j | j resendFor: self ifNeededAtTime: tick]] 172 | ] 173 | 174 | { #category : #mqtt } 175 | MQTTTransportLayer >> findPendingJob: testBlock [ 176 | 177 | "Find the matching record of a pending job that needs completion. Your testBlock should be written to find only a single result but we use #detect: here and only give you back the first one" 178 | 179 | pendingJobs ifNil: [^nil]. 180 | pendingJobsMutex critical: [^pendingJobs detect: [:e | testBlock value: e] ifNone: [nil]] 181 | ] 182 | 183 | { #category : #mqtt } 184 | MQTTTransportLayer >> handleIncomingPacket: anMQTTPacket [ 185 | 186 | "most of the packets that come to me are acknowledgements of one sort or another; the exception is the PUBLISH, which we have to handle since it is how we get that pesky data. 187 | 188 | Now most people might be expecting some sort of case statement here, switching based on the type of the packet. But we're Smalltalkers and that is more than a little gauche when we can just do it right and delegate to the packet" 189 | 190 | anMQTTPacket evaluateFor: self 191 | ] 192 | 193 | { #category : #mqtt } 194 | MQTTTransportLayer >> handlePubAckPacket: aPubAckPacket [ 195 | 196 | "The broker has confirmed a qos=1 publish process (3.4)" 197 | | job | 198 | 199 | "Find my pending job by matching the msgID tag" 200 | job := self findPendingJob: [:j | j isPubAckID: aPubAckPacket packetID]. 201 | job ifNil: [^self missingPendingJob: #pubAck]. 202 | 203 | "What do we do?" 204 | job completeFor: self. 205 | 206 | "c) consider the publish completed. Not sure what that means in practice" 207 | ] 208 | 209 | { #category : #mqtt } 210 | MQTTTransportLayer >> handlePubCompPacket: aPubCompPacket [ 211 | 212 | "The broker has confirmed a qos=2 publish process is completed (3.7)" 213 | | job | 214 | 215 | "Find my pending job by matching the msgID tag" 216 | job := self findPendingJob: [:j | j isPubCompID: aPubCompPacket packetID]. 217 | job ifNil: [^self missingPendingJob: #pubComp]. 218 | 219 | "What do we do?" 220 | job completeFor: self. 221 | 222 | "c) consider the publish completed. Not sure what that means in practice" 223 | ] 224 | 225 | { #category : #mqtt } 226 | MQTTTransportLayer >> handlePubRecPacket: aPubRecPacket [ 227 | 228 | "The broker has confirmed a qos=2 publish process is in progress (3.5)" 229 | | job pubRelPacket pj | 230 | 231 | "Find my pending job by matching the msgID tag" 232 | job := self findPendingJob: [:j | j isPubRecID: aPubRecPacket packetID]. 233 | job ifNil: [self missingPendingJob: #pubRec]. 234 | 235 | "add a new pending job for the response to the PUBREL" 236 | 237 | "We could have a pubrec that is stale" 238 | pubRelPacket := aPubRecPacket acknowledgement. 239 | self addPending: (pj := pubRelPacket pendingJob). 240 | 241 | "We need to respond by sending out a PUBREL packet with the *same* packetID, and add a pending job to handle the closing PUBCOMP" 242 | self sendPacket: (MQTTPacketAndPendingJobPair packet: pubRelPacket pendingJob: pj). 243 | 244 | "release the pending job from the list but do *not* release the packetID yet" 245 | job ifNotNil: [job completeFor: self]. 246 | ] 247 | 248 | { #category : #mqtt } 249 | MQTTTransportLayer >> handlePubRelPacket: aPubRelPacket [ 250 | 251 | "The broker has confirmed a qos=2 publish process is completing, so accept the end of this relationship gracefully (3.6)" 252 | | job | 253 | 254 | "Find my pending job by matching the msgID tag" 255 | job := self findPendingJob: [:j | j isPubRelID: aPubRelPacket packetID]. 256 | 257 | "I thought that we would only get a PUBREL once per job, but it 258 | turns out that you can get repeats and so we cannot allow a complaint 259 | about a missing pending job. 260 | We also have to only complete the job if we find one - see later" 261 | job ifNil: [self missingPendingJob: #pubRel]. 262 | 263 | "We need to respond by sending out a PUBCOMP packet with the *same* packetID" 264 | self sendPacket: aPubRelPacket acknowledgement. 265 | 266 | "release the pending job from the list and release the packetID" 267 | job ifNotNil: [ 268 | job completeFor: self. 269 | self doSubscriptionActionsFor: job originalPacket originalPacket] 270 | ] 271 | 272 | { #category : #mqtt } 273 | MQTTTransportLayer >> handlePublishPacket: aPublishPacket qos: packetQos [ 274 | 275 | "The broker has sent me a PUBLISH packet (3.3); extract the data and..." 276 | | pubRecOrAckPacket pj | 277 | 278 | "[MQTT-4.3.3-2] [MQTT-4.6.0-2] [MQTT-4.5.0-2]" 279 | 280 | "Find my pending job by matching the msgID tag" 281 | 282 | "What do we do?" 283 | 284 | "qos = 0 ifTrue: [do nothing]." 285 | packetQos >= 1 286 | ifTrue:[ "send back an appropriate PUBACK or PUBREC packet" 287 | 288 | "[MQTT-4.3.2-2]" 289 | pubRecOrAckPacket := aPublishPacket acknowledgement. 290 | packetQos = 1 291 | ifTrue:[self sendPacket: pubRecOrAckPacket]]. 292 | packetQos = 2 293 | ifTrue:[ "more complex. 294 | 295 | see if there is already a pending job to deal with a PUBREL 296 | 297 | if so, we're done, return 298 | 299 | if not, add a pending PUBREL job with the sent PUBREC and pass on the data" 300 | (self findPendingJob: [:j | j isPubRelID: aPublishPacket packetID]) ifNotNil: [^self]. 301 | self addPending: (pj := MQTTPendingPubRelJob for: pubRecOrAckPacket). "[MQTT-4.6.0-4] change from MQTT-tpr.23" 302 | self sendPacket: (MQTTPacketAndPendingJobPair packet: pubRecOrAckPacket pendingJob: pj). 303 | ^self]. 304 | 305 | "and here we do whatever to let the user have the data" 306 | self doSubscriptionActionsFor: aPublishPacket 307 | ] 308 | 309 | { #category : #mqtt } 310 | MQTTTransportLayer >> handleSubAckPacket: aSubAckPacket [ 311 | 312 | "The broker has confirmed a SUBSCRIBE from me (3.9)" 313 | | job | 314 | 315 | "Find my pending job by matching the msgID tag" 316 | job := self findPendingJob: [:j | j isSubAckID: aSubAckPacket packetID]. 317 | job ifNil: [^self missingPendingJob: #subAck]. 318 | job completeFor: self 319 | ] 320 | 321 | { #category : #mqtt } 322 | MQTTTransportLayer >> handleUnsubAckPacket: anUnsubAckPacket [ 323 | 324 | "The broker has confirmed an UNSUB process (3.10)" 325 | | job | 326 | 327 | "Find my pending job by matching the msgID tag" 328 | job := self findPendingJob: [:j | j isUnsubAckID: anUnsubAckPacket packetID]. 329 | job ifNil: [^self missingPendingJob: #unSubAck]. 330 | job completeFor: self 331 | ] 332 | 333 | { #category : #mqtt } 334 | MQTTTransportLayer >> initializeKeepAlive [ 335 | 336 | "keep alive action" 337 | 338 | ] 339 | 340 | { #category : #mqtt } 341 | MQTTTransportLayer >> initializeMQTTConnection: anObject [ 342 | 343 | "assemble a CONNECT packet and send it" 344 | 345 | ] 346 | 347 | { #category : #mqtt } 348 | MQTTTransportLayer >> initializePacketReading [ 349 | 350 | "set up the shared queue and process to write packets. The connection must be open" 351 | | innerBlock | 352 | 353 | innerBlock := self packetReader. 354 | incomingPacketProcess := [ 355 | [| packet|packet := [innerBlock value] on: Error do: [:e | self exceptionHandler: e]. "[MQTT-4.8.0-2]" 356 | packet ifNotNil: [ 357 | self statPerform: #packetIn: with: packet. 358 | (packet packetType = 3 and: [packet qos > 0]) 359 | ifTrue: [self debugLog: 'Rx ' , packet asString]. 360 | [self handleIncomingPacket: packet] on: Error do: [:e | self exceptionHandler: e]. "[MQTT-4.8.0-2]" 361 | ]. 362 | Processor yield. 363 | self abort] whileFalse] forkNamed: 'MQTT packet reading'. 364 | ] 365 | 366 | { #category : #mqtt } 367 | MQTTTransportLayer >> initializePacketWriting [ 368 | 369 | "set up the shared queue and process to write packets. The connection must be open" 370 | | innerBlock | 371 | 372 | "[MQTT-4.6.0-5]" 373 | innerBlock := self packetWriter. 374 | outgoingPacketProcess := [ 375 | [| packet|packet := self packetInFlightQueue nextOrNil. 376 | packet ifNil: [packet := self outgoingPacketQueue next]. 377 | self packetInFlightQueue nextPut: packet. "Idea here is to save packet in in flight queue until we have processed it" 378 | self outgoingPacketQueue size > 10 379 | ifTrue:[self debugLog: 'write packet backlog at: ' , self outgoingPacketQueue size printString]. 380 | [innerBlock value: packet] on: Error do: [:e | self exceptionHandler: e]. 381 | self abort 382 | ifFalse:[self packetInFlightQueue nextOrNil]. 383 | Processor yield. 384 | self abort] whileFalse] forkNamed: 'MQTT packet writing' 385 | ] 386 | 387 | { #category : #mqtt } 388 | MQTTTransportLayer >> initializeRetryProcess [ 389 | 390 | "some packets may need resending if they don't get acknowledged" 391 | 392 | retryProcess := [| loopDelay|loopDelay := Delay forSeconds: 2. 393 | [ 394 | self abort 395 | ifFalse:[self findAndRunJobsOverdueForAcknowledgement]. 396 | loopDelay wait. 397 | self abort] whileFalse] forkNamed: 'MQTT retry loop' 398 | ] 399 | 400 | { #category : #mqtt } 401 | MQTTTransportLayer >> initializeSocketStream: socket [ 402 | 403 | "make the basic socket connection to the broker" 404 | 405 | sockStrm := ZdcSocketStream on: socket. 406 | sockStrm timeout: self readWaitTime. 407 | ] 408 | 409 | { #category : #mqtt } 410 | MQTTTransportLayer >> lastMID [ 411 | 412 | "Answer my 'lastMID' instance variable." 413 | 414 | ^lastMID 415 | ] 416 | 417 | { #category : #mqtt } 418 | MQTTTransportLayer >> lastMID: aValue [ 419 | 420 | "Set my 'lastMID' instance variable to aValue." 421 | 422 | lastMID := aValue 423 | ] 424 | 425 | { #category : #mqtt } 426 | MQTTTransportLayer >> missingPendingJob: type [ 427 | 428 | "I couldn't find a matching pending job. oops." 429 | 430 | self debugLog: '************ No matching pending job for type: ' , type asString. 431 | self statPerform: #missingType: with: type 432 | ] 433 | 434 | { #category : #mqtt } 435 | MQTTTransportLayer >> newPacketID [ 436 | 437 | "provide a suitable 16 bit numeric non-zero packet id. 438 | 439 | This is supposed to be a unique number, presumably across the field of curently pending jobs. 440 | 441 | This ought to find a sutable value quickly in most cases, since we don't expect to have many pending jobs. If we get back to the 442 | 443 | value that was last used then we must have gone all the way round and must fail bcause we're out of IDs" 444 | | prevMID pj | 445 | 446 | lastMID ifNil: [lastMID := 1]. 447 | prevMID := lastMID. 448 | [ 449 | lastMID := lastMID + 1 \\ 16rFFFF. 450 | lastMID = 0 451 | ifTrue:[lastMID := 1]. "Fix for MQTT-2.3.1-1" 452 | pendingJobsMutex critical: [ 453 | pj := pendingJobs. 454 | pj ifNil: [^lastMID] ifNotNil: [pj detect: [:jb | jb packetID = lastMID] ifNone: [^lastMID]]]. "[MQTT-2.3.1-2]" 455 | prevMID = lastMID] whileFalse. 456 | ^self badPacketID "[MQTT-2.3.1-2]" 457 | ] 458 | 459 | { #category : #mqtt } 460 | MQTTTransportLayer >> onTopic: topicString qos: qos do: aBlock [ 461 | 462 | "a basic subscribe and do something message. We must check the topicString's acceptability and fail if there are issues" 463 | | subscription | 464 | 465 | subscription := MQTTSubscription for: topicString qos: qos do: aBlock. 466 | subscription ifNil: [^self badTopicError]. 467 | currentSubscriptions add: subscription. 468 | self subscribe: topicString qos: qos 469 | ] 470 | 471 | { #category : #mqtt } 472 | MQTTTransportLayer >> outgoingPacketQueue [ 473 | 474 | "Answer my 'outgoingPacketQueue' instance variable." 475 | 476 | ^outgoingPacketQueue 477 | ] 478 | 479 | { #category : #mqtt } 480 | MQTTTransportLayer >> outgoingPacketQueue: aQueue [ 481 | 482 | outgoingPacketQueue := aQueue 483 | ] 484 | 485 | { #category : #mqtt } 486 | MQTTTransportLayer >> packetInFlightQueue [ 487 | 488 | "Answer my 'packetInFlightQueue' instance variable." 489 | 490 | ^packetInFlightQueue 491 | ] 492 | 493 | { #category : #mqtt } 494 | MQTTTransportLayer >> packetInFlightQueue: aValue [ 495 | 496 | "Set my 'packetInFlightQueue' instance variable to aValue." 497 | 498 | packetInFlightQueue := aValue 499 | ] 500 | 501 | { #category : #mqtt } 502 | MQTTTransportLayer >> packetReader [ 503 | 504 | "Testing Read 10 bytes" 505 | 506 | ^[MQTTPacket readFrom: sockStrm] 507 | ] 508 | 509 | { #category : #mqtt } 510 | MQTTTransportLayer >> packetWriter [ 511 | 512 | "Testing Write Data" 513 | 514 | ^[:packet | 515 | self debugLog: 'Wx ' , packet asString. 516 | packet encodeOn: sockStrm. 517 | self statPerform: #packetOut: with: packet.] 518 | ] 519 | 520 | { #category : #mqtt } 521 | MQTTTransportLayer >> pendingJobs [ 522 | 523 | "Answer my 'pendingJobs' instance variable." 524 | 525 | ^pendingJobs 526 | ] 527 | 528 | { #category : #mqtt } 529 | MQTTTransportLayer >> pendingJobs: aValue [ 530 | 531 | "Set my 'pendingJobs' instance variable to aValue." 532 | 533 | pendingJobs := aValue 534 | ] 535 | 536 | { #category : #mqtt } 537 | MQTTTransportLayer >> preambleWorkAbortEarlyIfTrue: restart [ 538 | abortMutex 539 | critical: [ abort 540 | ifTrue: [ ^ true ]. "[MQTT-3.14.4-1] [MQTT-3.14.4-2]" 541 | self debugLog: 'disconnecting with restart: ' , restart printString. 542 | self statPerform: #disconnect. 543 | self sendPossibleDisconnectPackage. 544 | abort := true. 545 | self disconnectPostCleanup ]. 546 | 547 | ^false 548 | ] 549 | 550 | { #category : #mqtt } 551 | MQTTTransportLayer >> printOn: aStream [ 552 | 553 | "print useful data" 554 | 555 | super printOn: aStream. 556 | aStream nextPutAll: ' abort: '. 557 | abort asString printOn: aStream. 558 | aStream nextPutAll: ' socket: '. 559 | sockStrm asString printOn: aStream. 560 | aStream nextPutAll: ' pifq: '. 561 | aStream cr. 562 | packetInFlightQueue printOn: aStream. 563 | aStream nextPutAll: ' opq: '. 564 | outgoingPacketQueue printOn: aStream. 565 | aStream nextPutAll: ' pj: '. 566 | pendingJobs printOn: aStream. 567 | ] 568 | 569 | { #category : #mqtt } 570 | MQTTTransportLayer >> publishTopic: aTopic message: msgString qos: qos retain: retainFlag [ 571 | 572 | "publish the msgString to the connected broker. If qos > 0 we'll need to schedule a pending job for the ack sequence(s)" 573 | | pubPacket pj | 574 | 575 | pubPacket := MQTTPacketPublish new 576 | topic: aTopic message: msgString asByteArray; 577 | retainFlag: retainFlag; 578 | qos: qos. 579 | qos > 0 580 | ifTrue:[ 581 | pubPacket messageID: self newPacketID. 582 | self addPending: (pj := pubPacket pendingJob)]. 583 | self sendPacket: (MQTTPacketAndPendingJobPair packet: pubPacket pendingJob: pj) 584 | ] 585 | 586 | { #category : #mqtt } 587 | MQTTTransportLayer >> readWaitTime [ 588 | 589 | "Answer my 'readWaitTime' instance variable." 590 | 591 | ^readWaitTime 592 | ] 593 | 594 | { #category : #mqtt } 595 | MQTTTransportLayer >> readWaitTime: aValue [ 596 | 597 | "Set my 'readWaitTime' instance variable to aValue." 598 | 599 | readWaitTime := aValue 600 | ] 601 | 602 | { #category : #mqtt } 603 | MQTTTransportLayer >> releasePendingJob: pendingJob [ 604 | 605 | "Find the matching record of a pending job and remove it from the list" 606 | 607 | pendingJobs ifNil: [^nil]. 608 | pendingJobsMutex critical: [^pendingJobs remove: pendingJob ifAbsent: [nil]] 609 | ] 610 | 611 | { #category : #mqtt } 612 | MQTTTransportLayer >> restart: aSocket [ 613 | "Set up the server to run in asynchronous mode on the socket given as argument. 614 | 615 | This entails making the socket non-blocking and responding to a good set of events. 616 | 617 | The first read event should start things going" 618 | 619 | 620 | (Delay forSeconds: 3) wait. 621 | self debugLog: 'reStart Socket'. 622 | abortMutex critical: [ abort := false ]. 623 | self initializeSocketStream: aSocket. 624 | self 625 | initializeMQTTConnection: false; 626 | initializePacketReading; 627 | initializePacketWriting; 628 | initializeRetryProcess 629 | ] 630 | 631 | { #category : #mqtt } 632 | MQTTTransportLayer >> restartTheSocket [ 633 | 634 | "flag to indicate if we restart after disconnect" 635 | 636 | ^true 637 | ] 638 | 639 | { #category : #mqtt } 640 | MQTTTransportLayer >> retryTime [ 641 | 642 | "Answer my 'retryTime' instance variable." 643 | 644 | ^retryTime 645 | ] 646 | 647 | { #category : #mqtt } 648 | MQTTTransportLayer >> sendPacket: anMQTTPacket [ 649 | 650 | "add the packet to the outgoing queue where it will get sucked into The Machine as soon as possible" 651 | 652 | outgoingPacketQueue nextPut: anMQTTPacket 653 | ] 654 | 655 | { #category : #mqtt } 656 | MQTTTransportLayer >> sendPossibleDisconnectPackage [ 657 | 658 | "do nothing" 659 | 660 | ] 661 | 662 | { #category : #accessing } 663 | MQTTTransportLayer >> sockStrm [ 664 | ^ sockStrm 665 | ] 666 | 667 | { #category : #accessing } 668 | MQTTTransportLayer >> sockStrm: anObject [ 669 | sockStrm := anObject 670 | ] 671 | 672 | { #category : #mqtt } 673 | MQTTTransportLayer >> start: aSocket [ 674 | "Set up the server to run in asynchronous mode on the socket given as argument. 675 | 676 | 677 | 678 | This entails making the socket non-blocking and responding to a good set of events. 679 | 680 | 681 | 682 | The first read event should start things going" 683 | 684 | self debugLog: 'Start Socket'. 685 | abort := false. 686 | retryTime := 20. "from mosquitto specs" 687 | readWaitTime := 30. 688 | pendingJobsMutex := Semaphore forMutualExclusion. 689 | currentSubscriptions := OrderedCollection new: 4. 690 | outgoingPacketQueue := SharedQueue new. 691 | packetInFlightQueue := SharedQueue new. 692 | statistics := MQTTStatistics new. 693 | statisticsMutex := Semaphore forMutualExclusion. 694 | abortMutex := Semaphore forMutualExclusion. 695 | self initializeSocketStream: aSocket. 696 | self 697 | initializeMQTTConnection: true; 698 | initializePacketReading; 699 | initializePacketWriting; 700 | initializeRetryProcess. 701 | self debugLog: 'Started Socket' 702 | ] 703 | 704 | { #category : #mqtt } 705 | MQTTTransportLayer >> statPerform: selector [ 706 | 707 | "consolidate error handler" 708 | 709 | statisticsMutex critical: [[statistics perform: selector] on: Error do: [:ex | ]]. 710 | ] 711 | 712 | { #category : #mqtt } 713 | MQTTTransportLayer >> statPerform: selector with: arg [ 714 | 715 | "consolidate error handler" 716 | 717 | statisticsMutex critical: [[statistics perform: selector with: arg] on: Error do: [:ex | ]]. 718 | ] 719 | 720 | { #category : #mqtt } 721 | MQTTTransportLayer >> subscribe: aTopic qos: qos [ 722 | 723 | "set up a subscription to aTopic with the broker. Use #onTopic:do: for actual client applications" 724 | | subPacket pj | 725 | 726 | subPacket := MQTTPacketSubscribe new 727 | addTopic: aTopic qos: qos; 728 | packetID: self newPacketID. 729 | pj := subPacket pendingJob. 730 | self addPending: pj. 731 | self sendPacket: (MQTTPacketAndPendingJobPair packet: subPacket pendingJob: pj) 732 | ] 733 | -------------------------------------------------------------------------------- /src/MQTT/MQTTTransportLayerClient.class.st: -------------------------------------------------------------------------------- 1 | " 2 | This is the MQTT client logic. 3 | " 4 | Class { 5 | #name : #MQTTTransportLayerClient, 6 | #superclass : #MQTTTransportLayer, 7 | #instVars : [ 8 | 'pingOK', 9 | 'keepAliveTime', 10 | 'clientID', 11 | 'socketClient' 12 | ], 13 | #classVars : [ 14 | 'ClientID' 15 | ], 16 | #category : #MQTT 17 | } 18 | 19 | { #category : #mqtt } 20 | MQTTTransportLayerClient class >> clientID [ 21 | 22 | "get clientID" 23 | 24 | ClientID ifNil: [ClientID := UUID new asString]. 25 | ^ClientID 26 | ] 27 | 28 | { #category : #mqtt } 29 | MQTTTransportLayerClient >> clientID: clientIDString [ 30 | 31 | "set the id that the server will use to identify the session being established by this connect packet. It ought to be unique within the scope of this image, ideally totally unique. That's up to the user of this class though" 32 | 33 | clientID := clientIDString 34 | ] 35 | 36 | { #category : #mqtt } 37 | MQTTTransportLayerClient >> clientIDString [ 38 | 39 | "ought to come from the client but if it hasn't been set use a vaguely useful default" 40 | 41 | ^clientID ifNil: [self class clientID] 42 | ] 43 | 44 | { #category : #mqtt } 45 | MQTTTransportLayerClient >> disconnect: restart [ 46 | 47 | "close the connection" 48 | 49 | (self preambleWorkAbortEarlyIfTrue: restart) 50 | ifTrue: [ ^self ]. 51 | 52 | super disconnect: restart. 53 | (restart) 54 | ifTrue:[ 55 | [| itf|itf := self socketClient interface. 56 | itf ifNotNil: [itf restart]] fork] 57 | ] 58 | 59 | { #category : #mqtt } 60 | MQTTTransportLayerClient >> disconnectPostCleanup [ 61 | 62 | "disconnect" 63 | 64 | super disconnectPostCleanup. 65 | 66 | ] 67 | 68 | { #category : #mqtt } 69 | MQTTTransportLayerClient >> doSubscriptionActionsFor: aPublishPacket [ 70 | 71 | "find the subscriptions matching the topic of this packet and the corresponding Blocks to evaluate, then run them" 72 | 73 | currentSubscriptions copy do: [:s | 74 | s handlePacket: aPublishPacket] "Make a copy to avoid race with unsubscribe" 75 | ] 76 | 77 | { #category : #mqtt } 78 | MQTTTransportLayerClient >> handleConnAckPacket: aConnAckPacket [ 79 | 80 | "check for Error" 81 | 82 | aConnAckPacket byte2 > 0 83 | ifTrue:[MQTTConnectionException signal]. 84 | ] 85 | 86 | { #category : #mqtt } 87 | MQTTTransportLayerClient >> handlePingRespPacket: aPingRespPacket [ 88 | 89 | "just set the pingOK status to true" 90 | 91 | pingOK := true 92 | ] 93 | 94 | { #category : #mqtt } 95 | MQTTTransportLayerClient >> initializeKeepAlive [ 96 | 97 | "set up the keep alive process. If the keepalive time is 0 the server should simply leave the connection open as long as possible and so we don't worry about this process. Otherwise, add a pending ping job that will repeat" 98 | 99 | keepAliveTime = 0 "[MQTT-3.1.2-23]" 100 | ifFalse:[self addPending: MQTTPendingPingJob new] 101 | ] 102 | 103 | { #category : #mqtt } 104 | MQTTTransportLayerClient >> keepAliveTime: timeInSecs [ 105 | 106 | "set the keep-alive time, in seconds not mS, that should be used by the server and the keepalive process. Do not alter this after the connection is opened" 107 | 108 | keepAliveTime := timeInSecs 109 | ] 110 | 111 | { #category : #mqtt } 112 | MQTTTransportLayerClient >> ping [ 113 | 114 | "send a PINGREQ and scehdule a pending PINGRESP. Return a time for the next ping" 115 | 116 | pingOK := false. 117 | self sendPacket: MQTTPacketPingReq new. 118 | ^Time totalSeconds + keepAliveTime 119 | ] 120 | 121 | { #category : #mqtt } 122 | MQTTTransportLayerClient >> restart: aSocket [ 123 | 124 | "restart logic" 125 | 126 | outgoingPacketQueue := outgoingPacketQueue copy flushAllSuchThat: [:p | 127 | p class = MQTTPacketDisconnect or: [p class = MQTTPacketConnect]]. 128 | packetInFlightQueue := packetInFlightQueue copy flushAllSuchThat: [:p | 129 | p class = MQTTPacketDisconnect or: [p class = MQTTPacketConnect]]. 130 | super restart: aSocket. 131 | self initializeKeepAlive 132 | ] 133 | 134 | { #category : #mqtt } 135 | MQTTTransportLayerClient >> sendPossibleDisconnectPackage [ 136 | 137 | "do nothing" 138 | 139 | self sendPacket: MQTTPacketDisconnect new. 140 | Processor yield. 141 | (Delay forSeconds: 2) wait. 142 | ] 143 | 144 | { #category : #mqtt } 145 | MQTTTransportLayerClient >> socketClient [ 146 | 147 | "Answer my 'socketClient' instance variable." 148 | 149 | ^socketClient 150 | ] 151 | 152 | { #category : #mqtt } 153 | MQTTTransportLayerClient >> socketClient: aValue [ 154 | 155 | "Set my 'socketClient' instance variable to aValue." 156 | 157 | socketClient := aValue 158 | ] 159 | 160 | { #category : #mqtt } 161 | MQTTTransportLayerClient >> start: aSocket [ 162 | 163 | "start logic" 164 | 165 | super start: aSocket. 166 | self initializeKeepAlive 167 | ] 168 | 169 | { #category : #mqtt } 170 | MQTTTransportLayerClient >> subscribe: aTopic qos: qos [ 171 | 172 | "set up a subscription to aTopic with the broker. Use #onTopic:do: for actual client applications" 173 | | subPacket pj | 174 | 175 | subPacket := MQTTPacketSubscribe new 176 | addTopic: aTopic qos: qos; 177 | packetID: self newPacketID. 178 | self addPending: (pj := subPacket pendingJob). 179 | self sendPacket: (MQTTPacketAndPendingJobPair packet: subPacket pendingJob: pj) 180 | ] 181 | 182 | { #category : #mqtt } 183 | MQTTTransportLayerClient >> transportType [ 184 | 185 | ^' CC ' 186 | ] 187 | 188 | { #category : #mqtt } 189 | MQTTTransportLayerClient >> unsubscribe: aTopic [ 190 | 191 | "unsubscribe from aTopic and scehdule a pending job to handle the UnsubAck" 192 | | unsubPacket pj | 193 | 194 | unsubPacket := MQTTPacketUnsubscribe new 195 | addTopic: aTopic; 196 | packetID: self newPacketID. 197 | self addPending: (pj := unsubPacket pendingJob). 198 | self sendPacket: (MQTTPacketAndPendingJobPair packet: unsubPacket pendingJob: pj) 199 | ] 200 | 201 | { #category : #mqtt } 202 | MQTTTransportLayerClient >> unsubscribeFrom: aTopic [ 203 | 204 | "unsubscribe from aTopic - remove the subscription from currentSubscriptions and then tell the broker to unsubscribe" 205 | 206 | currentSubscriptions removeAllSuchThat: [:s | s topicMatches: aTopic]. 207 | self unsubscribe: aTopic 208 | ] 209 | -------------------------------------------------------------------------------- /src/MQTT/MQTTTransportLayerServer.class.st: -------------------------------------------------------------------------------- 1 | " 2 | This is the MQTT data broker partner logic, which handles packets not normally handled by a MQTT client. Such as the Connect/Disconnect packet. 3 | " 4 | Class { 5 | #name : #MQTTTransportLayerServer, 6 | #superclass : #MQTTTransportLayer, 7 | #instVars : [ 8 | 'serverInterface', 9 | 'socketServer', 10 | 'clientIDString', 11 | 'mutexForDisconnect', 12 | 'keepAliveTime', 13 | 'lastPingTime' 14 | ], 15 | #category : #MQTT 16 | } 17 | 18 | { #category : #mqtt } 19 | MQTTTransportLayerServer >> abort: aValue [ 20 | 21 | "Set my 'abort' instance variable to aValue." 22 | 23 | abort := aValue 24 | ] 25 | 26 | { #category : #mqtt } 27 | MQTTTransportLayerServer >> clientIDString [ 28 | 29 | "Answer my 'clientIDString' instance variable." 30 | 31 | ^clientIDString 32 | ] 33 | 34 | { #category : #mqtt } 35 | MQTTTransportLayerServer >> disconnect: restart [ 36 | 37 | "close the connection" 38 | 39 | (self preambleWorkAbortEarlyIfTrue: restart) 40 | ifTrue: [ ^self ]. 41 | super disconnect: false. 42 | self serverInterface ifNotNil: [self serverInterface removeSocketServer: self socketServer]. 43 | ] 44 | 45 | { #category : #mqtt } 46 | MQTTTransportLayerServer >> disconnectPostCleanup [ 47 | 48 | "disconnect" 49 | 50 | super disconnectPostCleanup. 51 | 52 | ] 53 | 54 | { #category : #mqtt } 55 | MQTTTransportLayerServer >> doSubscriptionActionsFor: aPublishPacket [ 56 | 57 | "find the subscriptions matching the topic of this packet and the corresponding Blocks to evaluate, then run them" 58 | 59 | self serverInterface handlePublishedPacket: aPublishPacket usingTransport: self 60 | ] 61 | 62 | { #category : #mqtt } 63 | MQTTTransportLayerServer >> handleConnectPacket: aConnectPacket [ 64 | 65 | "check for Error" 66 | | connectAckPacket | 67 | 68 | clientIDString := aConnectPacket clientIDString. 69 | keepAliveTime := aConnectPacket keepAliveTime. 70 | lastPingTime := Time primUTCSecondsClock. 71 | connectAckPacket := MQTTPacketConnAck new. 72 | connectAckPacket sessionPresent: ( 73 | self serverInterface sessionPresentViaConnectPacket: aConnectPacket). 74 | connectAckPacket byte2: aConnectPacket returnCode. 75 | mutexForDisconnect critical: [ 76 | self serverInterface addNewClientIDUsingTransport: self withConnectPacket: aConnectPacket passingConnAck: connectAckPacket]. "[MQTT-3.1.4-4] [MQTT-3.2.0-1]" 77 | 78 | "At the momment we don't make any return codes other than zero 79 | 80 | [MQTT-3.2.2-4] [MQTT-3.2.2-5] [MQTT-3.2.2-6]" 81 | ] 82 | 83 | { #category : #mqtt } 84 | MQTTTransportLayerServer >> handleDisconnectPacket: aDisconnectPacket [ 85 | 86 | "disconnect" 87 | 88 | self mutexForDisconnect critical: [| sock| " " 89 | self serverInterface ifNil: [^self]. 90 | self serverInterface disableWillLogic: clientIDString. "[MQTT-3.1.2-10]" 91 | sock := self socketServer. 92 | [[sock close] on: Error do: [:ex | ]] fork.]. "[MQTT-3.14.4-1]" 93 | ] 94 | 95 | { #category : #mqtt } 96 | MQTTTransportLayerServer >> handlePingReqPacket: aPingPacket [ 97 | 98 | "ping" 99 | | packetPingResp | 100 | 101 | "[MQTT-3.12.4-1]" 102 | lastPingTime := Time primUTCSecondsClock. 103 | packetPingResp := MQTTPacketPingResp new. 104 | self sendPacket: packetPingResp 105 | ] 106 | 107 | { #category : #mqtt } 108 | MQTTTransportLayerServer >> handlePublishResponse: aPublishPacket [ 109 | 110 | "publish the packet" 111 | | pj | 112 | 113 | " [MQTT-3.3.4-1], [MQTT-2.3.1-4] [MQTT-4.3.1-1] [MQTT-4.3.2-2][MQTT-4.3.3-1]" 114 | aPublishPacket qos = 0 115 | ifTrue:[^self sendPacket: aPublishPacket]. 116 | aPublishPacket messageID: self newPacketID. 117 | aPublishPacket qos = 1 118 | ifTrue:[ "[MQTT-4.3.2-1]" 119 | (self findPendingJob: [:j | j isPubAckID: aPublishPacket packetID]) ifNotNil: [^self]. 120 | self addPending: (pj := aPublishPacket pendingAckJob). 121 | ^self sendPacket: (MQTTPacketAndPendingJobPair packet: aPublishPacket pendingJob: pj)]. 122 | aPublishPacket qos = 2 123 | ifTrue:[ "[MQTT-4.3.3-1]" 124 | (self findPendingJob: [:j | j isPubRecID: aPublishPacket packetID]) ifNotNil: [^self]. 125 | self addPending: (pj := aPublishPacket pendingReceiveJob). 126 | ^self sendPacket: (MQTTPacketAndPendingJobPair packet: aPublishPacket pendingJob: pj)] 127 | ] 128 | 129 | { #category : #mqtt } 130 | MQTTTransportLayerServer >> handleSubscribePacket: aSubscribePacket [ 131 | 132 | "subscribe" 133 | | packetSubAck bitsBack | 134 | 135 | self debugLog: aSubscribePacket payloadDict keys printString. 136 | ((aSubscribePacket payloadDict isNil) or: [aSubscribePacket payloadDict size = 0]) 137 | ifTrue:[^self badTopicError]. "[MQTT-3.8.3-3]" 138 | self serverInterface addNewSubscription: aSubscribePacket forClientID: clientIDString. 139 | packetSubAck := MQTTPacketSubAck new. 140 | packetSubAck messageID: aSubscribePacket messageID. "[MQTT-2.3.1-3] [MQTT-3.8.4-2]" 141 | bitsBack := ByteArray new: aSubscribePacket payloadDict size. 142 | 1 to: bitsBack size do: [:i | 143 | bitsBack at: i put: ( 144 | (aSubscribePacket payloadDict keys at: i) = 'test/nosubscribe' 145 | ifTrue:[16r80] 146 | ifFalse:[aSubscribePacket payloadDict at: (aSubscribePacket payloadDict keyAtIndex: i)])]. 147 | self debugLog: bitsBack printString. 148 | packetSubAck returnCodes: bitsBack. "MQTT-3.8.4-5] [MQTT-3.9.3-1] [MQTT-3.9.3-2]" 149 | self sendPacket: packetSubAck. "[MQTT-3.8.4-1] [MQTT-3.8.4-4]" 150 | ] 151 | 152 | { #category : #mqtt } 153 | MQTTTransportLayerServer >> handleUnsubscribePacket: aUnsubscribePacket [ 154 | 155 | "unsubscribe" 156 | | packetUnsubAck | 157 | 158 | self serverInterface removeSubscriptions: aUnsubscribePacket forClientID: clientIDString. 159 | ((aUnsubscribePacket topics isNil) or: [aUnsubscribePacket topics size = 0]) 160 | ifTrue:[^self badTopicError]. "[MQTT-3.10.3-2]" 161 | packetUnsubAck := MQTTPacketUnsubAck new. 162 | packetUnsubAck packetID: aUnsubscribePacket messageID. "[MQTT-3.10.4-4]" 163 | self sendPacket: packetUnsubAck. "[MQTT-3.10.4-4] MQTT-3.10.4-5]" 164 | ] 165 | 166 | { #category : #mqtt } 167 | MQTTTransportLayerServer >> initialize [ 168 | 169 | super initialize. 170 | lastPingTime := Time primUTCSecondsClock. 171 | ] 172 | 173 | { #category : #mqtt } 174 | MQTTTransportLayerServer >> keepAliveTime [ 175 | 176 | "Answer my 'keepAliveTime' instance variable." 177 | 178 | ^keepAliveTime 179 | ] 180 | 181 | { #category : #mqtt } 182 | MQTTTransportLayerServer >> lastPingTime [ 183 | 184 | "Answer my 'lastPingTime' instance variable." 185 | 186 | ^lastPingTime 187 | ] 188 | 189 | { #category : #mqtt } 190 | MQTTTransportLayerServer >> lastPingTime: aTime [ 191 | 192 | lastPingTime := aTime 193 | ] 194 | 195 | { #category : #mqtt } 196 | MQTTTransportLayerServer >> mutexForDisconnect [ 197 | 198 | "Answer my 'mutexForDisconnect' instance variable." 199 | 200 | ^mutexForDisconnect 201 | ] 202 | 203 | { #category : #mqtt } 204 | MQTTTransportLayerServer >> printOn: aStream [ 205 | 206 | "print useful data" 207 | 208 | super printOn: aStream. 209 | aStream nextPutAll: ' ClientID: '. 210 | clientIDString asString printOn: aStream. 211 | aStream nextPutAll: ' kat: '. 212 | keepAliveTime asString printOn: aStream. 213 | ] 214 | 215 | { #category : #mqtt } 216 | MQTTTransportLayerServer >> release [ 217 | 218 | "release cycles" 219 | 220 | super release. 221 | serverInterface := nil. 222 | socketServer release. 223 | socketServer := nil. 224 | ] 225 | 226 | { #category : #mqtt } 227 | MQTTTransportLayerServer >> restartTheSocket [ 228 | 229 | "flag to indicate if we restart after disconnect" 230 | 231 | ^false 232 | ] 233 | 234 | { #category : #mqtt } 235 | MQTTTransportLayerServer >> serverInterface [ 236 | 237 | "Answer my 'serverInterface' instance variable." 238 | 239 | ^serverInterface 240 | ] 241 | 242 | { #category : #mqtt } 243 | MQTTTransportLayerServer >> serverInterface: aValue [ 244 | 245 | "Set my 'serverInterface' instance variable to aValue." 246 | 247 | serverInterface := aValue 248 | ] 249 | 250 | { #category : #mqtt } 251 | MQTTTransportLayerServer >> socketServer [ 252 | 253 | "Answer my 'socketServer' instance variable." 254 | 255 | ^socketServer 256 | ] 257 | 258 | { #category : #mqtt } 259 | MQTTTransportLayerServer >> socketServer: aValue [ 260 | 261 | "Set my 'socketServer' instance variable to aValue." 262 | 263 | socketServer := aValue 264 | ] 265 | 266 | { #category : #mqtt } 267 | MQTTTransportLayerServer >> start: aSocket [ 268 | 269 | "startup logic" 270 | 271 | mutexForDisconnect := Semaphore forMutualExclusion. 272 | lastPingTime := Time primUTCSecondsClock. 273 | keepAliveTime := 0. 274 | super start: aSocket 275 | ] 276 | 277 | { #category : #mqtt } 278 | MQTTTransportLayerServer >> statPerform: selector [ 279 | 280 | "consolidate error handler" 281 | 282 | super statPerform: selector. 283 | [self serverInterface statPerform: selector] on: Error do: [:ex | ]. 284 | ] 285 | 286 | { #category : #mqtt } 287 | MQTTTransportLayerServer >> statPerform: selector with: arg [ 288 | 289 | "consolidate error handler" 290 | 291 | super statPerform: selector with: arg. 292 | self serverInterface ifNotNil: [self serverInterface statPerform: selector with: arg] 293 | ] 294 | 295 | { #category : #mqtt } 296 | MQTTTransportLayerServer >> transportType [ 297 | 298 | "transport type" 299 | 300 | ^' SS ' 301 | ] 302 | -------------------------------------------------------------------------------- /src/MQTT/MQTTWriteStream.class.st: -------------------------------------------------------------------------------- 1 | " 2 | This is a write stream subclass, it was specific to VSE to handle issues with memory allocation and to set the stream to binary. Perhaps it can disappear? 3 | " 4 | Class { 5 | #name : #MQTTWriteStream, 6 | #superclass : #RWBinaryOrTextStream, 7 | #category : #MQTT 8 | } 9 | 10 | { #category : #mqtt } 11 | MQTTWriteStream >> initialize [ 12 | super initialize. 13 | self binary. 14 | 15 | ] 16 | -------------------------------------------------------------------------------- /src/MQTT/Object.extension.st: -------------------------------------------------------------------------------- 1 | Extension { #name : #Object } 2 | 3 | { #category : #'*MQTT' } 4 | Object >> isBoolean [ 5 | ^false 6 | ] 7 | -------------------------------------------------------------------------------- /src/MQTT/Socket.extension.st: -------------------------------------------------------------------------------- 1 | Extension { #name : #Socket } 2 | 3 | { #category : #'*MQTT' } 4 | Socket >> printOn: aStream [ 5 | 6 | super printOn: aStream. 7 | aStream nextPutAll: '[', self statusString, ' ', self socketHandle hash printString, ']'. 8 | 9 | ] 10 | -------------------------------------------------------------------------------- /src/MQTT/UTF8Encoder.class.st: -------------------------------------------------------------------------------- 1 | " 2 | An abstraction class used in VSE to enable UTF8 encoding/decoding. Backs to Pharo/Squeak classes that do that work. 3 | " 4 | Class { 5 | #name : #UTF8Encoder, 6 | #superclass : #Object, 7 | #category : #MQTT 8 | } 9 | 10 | { #category : #encoding } 11 | UTF8Encoder class >> decode: aString [ 12 | ^aString convertFromWithConverter: UTF8TextConverter new. 13 | ] 14 | 15 | { #category : #encoding } 16 | UTF8Encoder class >> encode: aString [ 17 | ^aString convertToWithConverter: UTF8TextConverter new. 18 | ] 19 | -------------------------------------------------------------------------------- /src/MQTT/package.st: -------------------------------------------------------------------------------- 1 | Package { #name : #MQTT } 2 | --------------------------------------------------------------------------------