From bb59974376588c73de509df93aaefe0c90229dcd Mon Sep 17 00:00:00 2001 From: Charles Magahern Date: Mon, 18 Jan 2016 19:18:06 -0800 Subject: [PATCH] Fix bug where sometimes "ALL ON" or "ALL OFF" don't work --- XIONControlPanel.xcodeproj/project.pbxproj | 4 + XIONControlPanel/Controllers/WemoServer.swift | 156 +++++++++++++----- XIONControlPanel/Utilities/Semaphore.swift | 38 +++++ 3 files changed, 155 insertions(+), 43 deletions(-) create mode 100644 XIONControlPanel/Utilities/Semaphore.swift diff --git a/XIONControlPanel.xcodeproj/project.pbxproj b/XIONControlPanel.xcodeproj/project.pbxproj index 48de343..237ac48 100644 --- a/XIONControlPanel.xcodeproj/project.pbxproj +++ b/XIONControlPanel.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 0C20E2B91C33514100BA789B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C20E2B51C33514100BA789B /* AppDelegate.swift */; }; 0C20E2BB1C33529700BA789B /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C20E2BA1C33529700BA789B /* MainViewController.swift */; }; 0C20E2BD1C33534F00BA789B /* VisualizationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C20E2BC1C33534F00BA789B /* VisualizationViewController.swift */; }; + 0C21329B1C4DDC3E001E8F08 /* Semaphore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C21329A1C4DDC3E001E8F08 /* Semaphore.swift */; }; 0C3CAEAF1C3350C800B856AD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0C3CAEAE1C3350C800B856AD /* Assets.xcassets */; }; 0C3CAEB21C3350C800B856AD /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0C3CAEB01C3350C800B856AD /* LaunchScreen.storyboard */; }; 0C41EF4A1C34B51500AF847F /* SwitchesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41EF491C34B51500AF847F /* SwitchesViewController.swift */; }; @@ -41,6 +42,7 @@ 0C20E2B51C33514100BA789B /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 0C20E2BA1C33529700BA789B /* MainViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; 0C20E2BC1C33534F00BA789B /* VisualizationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VisualizationViewController.swift; sourceTree = ""; }; + 0C21329A1C4DDC3E001E8F08 /* Semaphore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Semaphore.swift; sourceTree = ""; }; 0C3CAEA41C3350C800B856AD /* XION.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = XION.app; sourceTree = BUILT_PRODUCTS_DIR; }; 0C3CAEAE1C3350C800B856AD /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 0C3CAEB11C3350C800B856AD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; @@ -203,6 +205,7 @@ 0C63A8791C33C622007E4B52 /* Utilities */ = { isa = PBXGroup; children = ( + 0C21329A1C4DDC3E001E8F08 /* Semaphore.swift */, 0CAA327C1C361ECA00B353CF /* StandardError.swift */, 0CAA327A1C361CBE00B353CF /* NSErrorAdditions.swift */, 0CF0F8951C362D8F00FE7752 /* NSURLAdditions.swift */, @@ -304,6 +307,7 @@ 0C20E2B91C33514100BA789B /* AppDelegate.swift in Sources */, 0C20E2BB1C33529700BA789B /* MainViewController.swift in Sources */, 0C41EF501C34B97700AF847F /* HeaderView.swift in Sources */, + 0C21329B1C4DDC3E001E8F08 /* Semaphore.swift in Sources */, 0C41EF541C34BC9800AF847F /* XIONLogoView.swift in Sources */, 0C4315061C35E67600164559 /* WemoCellView.swift in Sources */, 0CAA327D1C361ECA00B353CF /* StandardError.swift in Sources */, diff --git a/XIONControlPanel/Controllers/WemoServer.swift b/XIONControlPanel/Controllers/WemoServer.swift index db38c5f..edba070 100644 --- a/XIONControlPanel/Controllers/WemoServer.swift +++ b/XIONControlPanel/Controllers/WemoServer.swift @@ -16,11 +16,12 @@ enum ConnectionStatus { } class WemoServer { - private(set) var baseURL: NSURL - private(set) var connected: Bool = false + private(set) var baseURL: NSURL + private(set) var connected: Bool = false - private var _urlSession: NSURLSession - private var _errorStream: StandardErrorOutputStream = StandardErrorOutputStream() + private var _urlSession: NSURLSession + private var _errorStream: StandardErrorOutputStream = StandardErrorOutputStream() + private var _operationQueue: NSOperationQueue = NSOperationQueue() init(_ url: NSURL) { @@ -28,28 +29,26 @@ class WemoServer { let config = NSURLSessionConfiguration.defaultSessionConfiguration() _urlSession = NSURLSession(configuration: config) + + _operationQueue.maxConcurrentOperationCount = 1 } func connect(completion: (NSError?) -> Void) { if (!self.connected) { - let url = self.baseURL.URLByAppendingPathComponent("api/environment") - let request = NSMutableURLRequest(URL: url) - request.HTTPMethod = "POST" - - let task = _urlSession.dataTaskWithRequest(request) { (data: NSData?, response: NSURLResponse?, error: NSError?) -> Void in - var clientError: NSError? = nil - - if (error != nil) { - clientError = NSError.xionError(.ConnectionError, underlying: error!) - self._logError("Error connecting to server", error: clientError!) + let op = ConnectOperation(baseURL: self.baseURL, session: _urlSession) + weak var weakOp = op + op.completionBlock = { + guard let strongOp = weakOp else { completion(nil) ; return } + if (strongOp.error != nil) { + self._logError("Error connecting to server", error: strongOp.error!) } else { self.connected = true } - completion(clientError) + completion(strongOp.error) } - task.resume() + _operationQueue.addOperation(op) } else { completion(nil) } @@ -64,21 +63,17 @@ class WemoServer { func fetchDevices(completion: ([WemoDevice], NSError?) -> Void) { if (self.connected) { - let url = self.baseURL.URLByAppendingPathComponent("api/environment") - let task = _urlSession.dataTaskWithURL(url) { (data: NSData?, response: NSURLResponse?, error: NSError?) -> Void in - var devices: [WemoDevice] = [] - var clientError: NSError? = nil - - if (data != nil) { - devices = self._parseDevices(data!) - } else { - clientError = NSError.xionError(.ConnectionError, underlying: error) - self._logError("Error fetching devices", error: clientError!) + let op = FetchDevicesOperation(baseURL: self.baseURL, session: _urlSession) + weak var weakOp = op + op.completionBlock = { + guard let strongOp = weakOp else { completion([], nil) ; return } + if (strongOp.error != nil) { + self._logError("Error fetching devices", error: strongOp.error!) } - completion(devices, clientError) + completion(strongOp.devices, strongOp.error) } - task.resume() + _operationQueue.addOperation(op) } else { let err = NSError.xionError(.ConnectionError) completion([], err) @@ -88,24 +83,17 @@ class WemoServer { func toggleDevice(device: WemoDevice, state: WemoDevice.State, completion: (NSError?) -> Void) { if (self.connected) { - let stateArg = (state == .On ? "on" : "off") - let url = self.baseURL.URLByAppendingPathComponent("api/device/\(device.name)").URLByAppendingRequestParameters(["state" : stateArg]) - let request = NSMutableURLRequest(URL: url!) - request.HTTPMethod = "POST" - - print("Toggle: \(url!.absoluteString)") - - let task = _urlSession.dataTaskWithRequest(request) { (data: NSData?, response: NSURLResponse?, error: NSError?) -> Void in - var clientError: NSError? = nil - - if (error != nil) { - clientError = NSError.xionError(.ConnectionError, underlying: error) - self._logError("Error toggling device \"\(device.name)\"", error: clientError!) + let op = ToggleDeviceOperation(baseURL: self.baseURL, session: _urlSession, device: device, state: state) + weak var weakOp = op + op.completionBlock = { + guard let strongOp = weakOp else { completion(nil) ; return } + if (strongOp.error != nil) { + self._logError("Error toggling device", error: strongOp.error!) } - completion(clientError) + completion(strongOp.error) } - task.resume() + _operationQueue.addOperation(op) } else { let err = NSError.xionError(.ConnectionError) completion(err) @@ -118,6 +106,58 @@ class WemoServer { { print("ERROR: \(description) \(error)", toStream: &_errorStream) } +} + +internal class WemoOperation : NSOperation { + var baseURL: NSURL + var session: NSURLSession + + internal(set) var error: NSError? + + init(baseURL: NSURL, session: NSURLSession) + { + self.baseURL = baseURL + self.session = session + } +} + +internal class ConnectOperation : WemoOperation { + override func main() + { + let semaphore = Semaphore(value: 0) + let url = self.baseURL.URLByAppendingPathComponent("api/environment") + let request = NSMutableURLRequest(URL: url) + request.HTTPMethod = "POST" + + let task = self.session.dataTaskWithRequest(request) { (data: NSData?, response: NSURLResponse?, error: NSError?) -> Void in + if (error != nil) { + self.error = NSError.xionError(.ConnectionError, underlying: error!) + } + semaphore.signal() + } + task.resume() + semaphore.wait() + } +} + +internal class FetchDevicesOperation : WemoOperation { + internal(set) var devices: [WemoDevice] = [] + + override func main() + { + let semaphore = Semaphore(value: 0) + let url = self.baseURL.URLByAppendingPathComponent("api/environment") + let task = self.session.dataTaskWithURL(url) { (data: NSData?, response: NSURLResponse?, error: NSError?) -> Void in + if (data != nil) { + self.devices = self._parseDevices(data!) + } else { + self.error = NSError.xionError(.ConnectionError, underlying: error) + } + semaphore.signal() + } + task.resume() + semaphore.wait() + } internal func _parseDevices(data: NSData) -> [WemoDevice] { @@ -135,3 +175,33 @@ class WemoServer { return devices } } + +internal class ToggleDeviceOperation : WemoOperation { + var device: WemoDevice + var state: WemoDevice.State + + init(baseURL: NSURL, session: NSURLSession, device: WemoDevice, state: WemoDevice.State) + { + self.device = device + self.state = state + super.init(baseURL: baseURL, session: session) + } + + override func main() + { + let semaphore = Semaphore(value: 0) + let stateArg = (self.state == .On ? "on" : "off") + let url = self.baseURL.URLByAppendingPathComponent("api/device/\(self.device.name)").URLByAppendingRequestParameters(["state" : stateArg]) + let request = NSMutableURLRequest(URL: url!) + request.HTTPMethod = "POST" + + let task = self.session.dataTaskWithRequest(request) { (data: NSData?, response: NSURLResponse?, error: NSError?) -> Void in + if (error != nil) { + self.error = NSError.xionError(.ConnectionError, underlying: error) + } + semaphore.signal() + } + task.resume() + semaphore.wait() + } +} diff --git a/XIONControlPanel/Utilities/Semaphore.swift b/XIONControlPanel/Utilities/Semaphore.swift new file mode 100644 index 0000000..6f8be57 --- /dev/null +++ b/XIONControlPanel/Utilities/Semaphore.swift @@ -0,0 +1,38 @@ +// +// Semaphore.swift +// XIONControlPanel +// +// Created by Charles Magahern on 1/18/16. +// Copyright © 2016 XION. All rights reserved. +// + +import Foundation + +class Semaphore { + private var _semaphore: dispatch_semaphore_t + + init(value: Int) + { + _semaphore = dispatch_semaphore_create(value) + } + + func wait() + { + self.wait(nil) + } + + func wait(untilDate: NSDate?) + { + var time: dispatch_time_t = DISPATCH_TIME_FOREVER + if (untilDate != nil) { + time = UInt64(untilDate!.timeIntervalSinceNow) * NSEC_PER_SEC + } + + dispatch_semaphore_wait(_semaphore, time) + } + + func signal() + { + dispatch_semaphore_signal(_semaphore) + } +}