1 module doap.client.request; 2 3 import doap.client.client : CoapClient; 4 import doap.protocol; 5 import doap.client.exceptions; 6 import core.time : Duration; 7 import std.datetime.stopwatch : StopWatch, AutoStart; 8 9 /** 10 * Represents a request that has been made. This is 11 * normally stored inside the `CoapClient` and used 12 * to find matching responses that come through in 13 * the messaging layer. 14 * 15 * It is composed of the `token` and the future 16 * which was created. Therefore when the messaging 17 * layer receives a new CoAP packet it can then try 18 * match it to one of these requests, in the event 19 * it finds a match it can retrieve the future, place 20 * the received `CoapPacket` into it and then wake up 21 * anyone doing a blocking `get()` on it. 22 */ 23 package class CoapRequest 24 { 25 /** 26 * The original packet (to be able to access the token 27 * such that we can match it up with a respnse) 28 */ 29 private CoapPacket requestPacket; 30 31 /** 32 * The future which we can fill up with the 33 * response and then wake up the receiver 34 */ 35 package CoapRequestFuture future; 36 37 /** 38 * Stopwatch for counting elapsed time 39 */ 40 private StopWatch timer; 41 42 /** 43 * Constructs a new request 44 * 45 * Params: 46 * requestPacket = the actual request 47 * future = the `CoapRequestFuture` to wake up 48 * on data arrival 49 */ 50 this(CoapPacket requestPacket, CoapRequestFuture future) 51 { 52 this.requestPacket = requestPacket; 53 this.future = future; 54 this.timer = StopWatch(AutoStart.no); 55 } 56 57 /** 58 * Gets the original request made 59 * 60 * Returns: the request packet 61 */ 62 public CoapPacket getRequestPacket() 63 { 64 return this.requestPacket; 65 } 66 67 /** 68 * Gets the token from the original request 69 * that was made 70 * 71 * Returns: the token 72 */ 73 public ubyte[] getToken() 74 { 75 return this.requestPacket.getToken(); 76 } 77 78 /** 79 * Gets the message ID from the original 80 * request that was made 81 * 82 * Returns: the message id 83 */ 84 public ushort getMid() 85 { 86 return this.requestPacket.getMessageId(); 87 } 88 89 /** 90 * Starts the timer 91 */ 92 package void startTime() 93 { 94 timer.start(); 95 } 96 97 /** 98 * Checks if this request has expired 99 * according to the given timeout 100 * threshold 101 * 102 * If timed out then the timer 103 * restarts. 104 * 105 * Returns: `true` if timed out, 106 * `false` if not 107 */ 108 package bool hasTimedOut(Duration timeoutThreshold) 109 { 110 // Check if the threshold has been reached 111 if(timer.peek() >= timeoutThreshold) 112 { 113 timer.reset(); 114 return true; 115 } 116 else 117 { 118 return false; 119 } 120 } 121 122 /** 123 * Returns the elapsed time of this request 124 * thus far 125 * 126 * Returns: the elapsed time 127 */ 128 public Duration getElapsedTime() 129 { 130 return timer.peek(); 131 } 132 } 133 134 /** 135 * This allows one to build up a new 136 * CoAP request of some form in a 137 * method-call-by-method-call manner. 138 * 139 * In order to instantiate one of these 140 * please do so via the `CoapClient`. 141 */ 142 package class CoapRequestBuilder 143 { 144 /** 145 * The associated client for 146 * making the actual request 147 */ 148 private CoapClient client; 149 150 /** 151 * The request code 152 */ 153 package Code requestCode; 154 155 /** 156 * The message type 157 */ 158 package MessageType type; 159 160 /** 161 * The payload 162 */ 163 package ubyte[] pyld; 164 165 /** 166 * The token 167 */ 168 package ubyte[] tkn; 169 170 /** 171 * Constructs a new builder 172 * 173 * This requires a working `CoapClient` 174 * such that finalization can be done 175 * on its side 176 * 177 * Params: 178 * client = the client to associate with 179 */ 180 this(CoapClient client) 181 { 182 this.client = client; 183 this.requestCode = Code.GET; 184 this.type = MessageType.CONFIRMABLE; 185 } 186 187 /** 188 * Set the payload for this request 189 * 190 * Params: 191 * payload = the payload 192 * Returns: this builder 193 */ 194 public CoapRequestBuilder payload(ubyte[] payload) 195 { 196 this.pyld = payload; 197 return this; 198 } 199 200 /** 201 * Set the token to use for this request 202 * 203 * Params: 204 * tkn = the token 205 * Returns: this builder 206 * Throws: 207 * CoapClientException = invalid token 208 * length 209 */ 210 public CoapRequestBuilder token(ubyte[] tkn) 211 { 212 if(tkn.length > 8) 213 { 214 throw new CoapClientException("The token cannot be more than 8 bytes"); 215 } 216 217 this.tkn = tkn; 218 return this; 219 } 220 221 /** 222 * Sets this message as confirmable 223 * 224 * Returns: this builder 225 */ 226 public CoapRequestBuilder con() 227 { 228 this.type = MessageType.CONFIRMABLE; 229 return this; 230 } 231 232 /** 233 * Sets this message as non-confirmable 234 * 235 * Returns: this builder 236 */ 237 public CoapRequestBuilder non() 238 { 239 this.type = MessageType.NON_CONFIRMABLE; 240 return this; 241 } 242 243 /** 244 * Build the request, set it in flight 245 * and return the future handle to it. 246 * 247 * This sets the request code to POST. 248 * 249 * Returns: the `CoapRequestFuture` for 250 * this request 251 */ 252 public CoapRequestFuture post() 253 { 254 // Set the request code to POST 255 this.requestCode = Code.POST; 256 257 // Register the request via the client 258 // ... and obtain the future 259 return this.client.doRequest(this); 260 } 261 262 /** 263 * Build the request, set it in flight 264 * and return the future handle to it. 265 * 266 * This sets the request code to GET. 267 * 268 * Returns: the `CoapRequestFuture` for 269 * this request 270 */ 271 public CoapRequestFuture get() 272 { 273 // Set the request code to GET 274 this.requestCode = Code.GET; 275 276 // Register the request via the client 277 // ... and obtain the future 278 return this.client.doRequest(this); 279 } 280 } 281 282 import core.sync.mutex : Mutex; 283 import core.sync.condition : Condition; 284 285 286 /** 287 * The state of a `CoapRequestFuture` 288 */ 289 public enum RequestState 290 { 291 /** 292 * The future has been created 293 */ 294 CREATED, 295 296 /** 297 * The future has completed 298 * successfully 299 */ 300 COMPLETED, 301 302 /** 303 * The future was cancelled 304 */ 305 CANCELLED, 306 307 /** 308 * The future timed out 309 */ 310 TIMEDOUT 311 } 312 313 /** 314 * This is returned to the user when he 315 * does a finalizing call on a `CoapRequestBuilder` 316 * such as calling `post()`. The client 317 * will then creating the underlying request 318 * such that the messaging layer can match 319 * a future response to it. The client then 320 * sends the CoAP packet and then returns 321 * this so-called "future" as a handle 322 * to it which can be waited on via 323 * a call to `get()`. 324 */ 325 public class CoapRequestFuture 326 { 327 /** 328 * The received response 329 * 330 * This is filled in by the 331 * messaging layer. 332 */ 333 private CoapPacket response; // TODO: Volatility? 334 335 /** 336 * State of the future 337 */ 338 private RequestState state; // TODO: Volatility? 339 340 /** 341 * Mutex (for the condition to use) 342 */ 343 private Mutex mutex; 344 345 /** 346 * Condition variable 347 * 348 * Used for doing `wait()`/`notify()` 349 * such that the messaging layer can 350 * wake up a sleeping/blocking `get()` 351 */ 352 private Condition condition; 353 354 /** 355 * Constructs a new `CoapRequestFuture` 356 */ 357 package this() 358 { 359 this.state = RequestState.CREATED; 360 this.mutex = new Mutex(); 361 this.condition = new Condition(mutex); 362 } 363 364 /** 365 * Called when a matching (by token) CoAP packet 366 * is received. This will store the received response 367 * and also wake up anyone blocking on the `get()` 368 * call to this future. 369 * 370 * Params: 371 * response = the `CoapPacket` response 372 */ 373 package void receiveWake(CoapPacket response) 374 { 375 // Set the received response 376 this.response = response; 377 378 // Set completion state 379 this.state = RequestState.COMPLETED; 380 381 // Wake up the sleepers 382 this.condition.notifyAll(); 383 } 384 385 /** 386 * Cancels this future such that 387 * all calls to `get()` will 388 * unblock and throw an exception 389 */ 390 package void cancel() 391 { 392 // Set cancelled state 393 this.state = RequestState.CANCELLED; 394 395 // Wake up the sleepers 396 this.condition.notifyAll(); 397 } 398 399 /** 400 * Blocks until the response is received 401 * 402 * Returns: the response as a `CoapPacket` 403 * Throws: 404 * CoapClientException on cancelled request 405 */ 406 public CoapPacket get() 407 { 408 // We can only wait on a condition if we 409 // ... first have a-hold of the lock 410 this.mutex.lock(); 411 412 // Await a response 413 this.condition.wait(); 414 415 // Upon waking up release lock 416 this.mutex.unlock(); 417 418 // If successfully completed 419 if(this.state == RequestState.COMPLETED) 420 { 421 return this.response; 422 } 423 // On error 424 else 425 { 426 throw new CoapClientException("Request future cancelled"); 427 } 428 } 429 430 /** 431 * Blocks until the response is received 432 * but will unbllock if the timeout given 433 * is exceeded 434 * 435 * Returns: the response as a `CoapPacket` 436 * Throws: 437 * RequestTimeoutException on the 438 * future request timing out 439 * CoapClientException on cancellation 440 * of the request 441 */ 442 public CoapPacket get(Duration timeout) 443 { 444 // We can only wait on a condition if we 445 // ... first have a-hold of the lock 446 this.mutex.lock(); 447 448 scope(exit) 449 { 450 // Unlock the lock (either from successfully 451 // ... waiting or timing out) 452 this.mutex.unlock(); 453 } 454 455 // Await a response 456 if(this.condition.wait(timeout)) 457 { 458 // If successfully completed 459 if(this.state == RequestState.COMPLETED) 460 { 461 return this.response; 462 } 463 // On error 464 else 465 { 466 throw new CoapClientException("Request future cancelled"); 467 } 468 } 469 else 470 { 471 this.state = RequestState.TIMEDOUT; 472 throw new RequestTimeoutException(this, timeout); 473 } 474 } 475 476 /** 477 * Returns the state of this future 478 * 479 * Returns: the state 480 */ 481 public RequestState getState() 482 { 483 return this.state; 484 } 485 }