Skip to content

Commit

Permalink
Remove device when connection drops (#40)
Browse files Browse the repository at this point in the history
- Use logging handler instead of using os_log directly
- Lock unit test version to 17.5
- Update README, documentation and changelog
- Add app installation check to example
- Work around macro bug un XCode 16 betas
- Add promo info to docs
  • Loading branch information
r-dent authored Aug 29, 2024
1 parent 85a0846 commit 98793dd
Show file tree
Hide file tree
Showing 16 changed files with 220 additions and 54 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/code-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,4 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Test StreamDeckKit
run: set -o pipefail && xcodebuild -scheme StreamDeckKit-Package test -destination "platform=iOS Simulator,name=iPhone 15,OS=latest" -skipMacroValidation | xcpretty
run: set -o pipefail && xcodebuild -scheme StreamDeckKit-Package test -destination "platform=iOS Simulator,name=iPhone 15,OS=17.5" -skipMacroValidation | xcpretty
11 changes: 11 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [1.0.0] - 2024-08-22

This is the first official release. 🎉

### Added
- __SDK - Error Handling:__ Auto-remove erroneous devices.

### Changed
- __Example App - Stream Deck Connect App Detection:__ The example app now contains a check for the Stream Deck Connect app


## [0.0.2-alpha] - 2024-04-18

Expand Down
6 changes: 1 addition & 5 deletions Documentation/GettingStarted.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@ To interact with a physical Stream Deck device, ensure you have the following:

However, if you want to verify your implementation using the [Stream Deck Simulator](Simulator.md) only, no additional prerequisites are necessary.

{% hint style="info" %}
During the alpha phase, the app is not in available in the App Store. [Click here to participate in the public alpha of Stream Deck Connect](https://testflight.apple.com/join/U4bWfk8O) in [TestFlight](https://developer.apple.com/testflight/).
{% endhint %}

| iOS Version | Swift Version | XCode Version |
| ----------- | ------------- | ------------- |
| >= 16 | >= 5.9 | >= 15 |
Expand All @@ -28,7 +24,7 @@ If you want to add it to your own libraries `Package.swift`, use this code inste

```swift
dependencies: [
.package(url: "https://github.com/elgatosf/streamdeck-kit-ipad.git", upToNextMajor: "0.0.1")
.package(url: "https://github.com/elgatosf/streamdeck-kit-ipad.git", upToNextMajor: "1.0.0")
]
```

Expand Down
14 changes: 9 additions & 5 deletions Documentation/StreamDeckConnect.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@ The [Elgato Stream Deck Connect](https://apps.apple.com/app/elgato-stream-deck-c

Users download it from the Apple App Store, and are guided to activate the driver once within the iPadOS settings. Going forward, they then are all set for using their Stream Deck(s) with any iPadOS application that integrates the Elgato Stream Deck for iPad SDK.

{% hint style="info" %}
During the alpha phase, the app is not in available in the App Store. [Click here to participate in the public alpha of Stream Deck Connect](https://testflight.apple.com/join/U4bWfk8O) in [TestFlight](https://developer.apple.com/testflight/).
{% endhint %}

## Verify app installation

In your app, consider addressing the scenario where Stream Deck Connect, and consequently, its driver, is not installed on the user's device. In such cases, you could prompt users with a message instructing users to install the app and enable the driver before utilizing your app with Stream Deck.
Expand All @@ -18,4 +14,12 @@ To determine if Stream Deck Connect is installed within your project, use the fo
UIApplication.shared.canOpenURL(URL(string: "elgato-device-driver://")!)
```

Ensure to include `"elgato-device-driver"` in the `LSApplicationQueriesSchemes` section of your Info.plist file.
Ensure to include `"elgato-device-driver"` in the `LSApplicationQueriesSchemes` section of your Info.plist file.

## Promote your app

Once you have published a new version of your app that supports Stream Deck, you can apply for a promotion inside of the Stream Deck Connect app.

Do that by creating a pull request to the [Stream Deck Kit - Compatible Apps](https://github.com/elgatosf/streamdeck-kit-ipad-compatible-apps/) repository on GitHub. You need to provide a few infos about your app as well as a logo image.

These infos will be displayed at a prominent place in Stream Deck Connect, when your request is accepted.
11 changes: 11 additions & 0 deletions Example/Example App/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@ struct ContentView: View {

@Environment(\.exampleDataModel) var dataModel

private let appWillEnterForeground = NotificationCenter.default
.publisher(for: UIApplication.willEnterForegroundNotification)

@State private var stateDescription: String = StreamDeckSession.State.idle.debugDescription
@State private var devices: [StreamDeck] = []
@State private var isDriverHostInstalled: Bool = false

var body: some View {
@Bindable var dataModel = dataModel
Expand Down Expand Up @@ -45,6 +49,7 @@ struct ContentView: View {
case .stateful: Text("2. Example - Stateful").font(.title).padding()
case .animated: Text("3. Example - Animated").font(.title).padding()
}
Text("Stream Deck Connect installation: \(isDriverHostInstalled ? "done" : "not installed")")
Text("Session State: \(stateDescription)")
if devices.isEmpty {
Text("Please connect a Stream Deck device!")
Expand Down Expand Up @@ -72,6 +77,8 @@ struct ContentView: View {
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onReceive(StreamDeckSession.instance.$state) { stateDescription = $0.debugDescription }
.onReceive(StreamDeckSession.instance.$devices) { devices = $0 }
.onReceive(appWillEnterForeground) { _ in checkDriverHostAppInstallation() }
.onAppear { checkDriverHostAppInstallation() }
.overlay(alignment: .bottomTrailing) {
Button("Show Stream Deck Simulator") {
StreamDeckSimulator.show(streamDeck: .regular)
Expand All @@ -80,6 +87,10 @@ struct ContentView: View {
.padding()
}
}

private func checkDriverHostAppInstallation() {
isDriverHostInstalled = UIApplication.shared.canOpenURL(URL(string: "elgato-device-driver://")!)
}
}

#if DEBUG
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1540"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "307301682B3DB70000C5F5B4"
BuildableName = "StreamDeckKit StreamDeckKitExample.app"
BlueprintName = "StreamDeckKitExample App"
ReferencedContainer = "container:StreamDeckKitExample.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "307301682B3DB70000C5F5B4"
BuildableName = "StreamDeckKit StreamDeckKitExample.app"
BlueprintName = "StreamDeckKitExample App"
ReferencedContainer = "container:StreamDeckKitExample.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "307301682B3DB70000C5F5B4"
BuildableName = "StreamDeckKit StreamDeckKitExample.app"
BlueprintName = "StreamDeckKitExample App"
ReferencedContainer = "container:StreamDeckKitExample.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
5 changes: 1 addition & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,6 @@ To interact with a physical Stream Deck device, ensure you have the following:

However, if you want to verify your implementation using the [Stream Deck Simulator](#utilizing-the-simulator) only, no additional prerequisites are necessary.

> [!IMPORTANT]
> During the alpha phase, the app is not in available in the App Store. [Click here to participate in the public alpha of Stream Deck Connect](https://testflight.apple.com/join/U4bWfk8O) in [TestFlight](https://developer.apple.com/testflight/).
| iOS Version | Swift Version | XCode Version |
| ----------- | ------------- | ------------- |
| >= 16 | >= 5.9 | >= 15 |
Expand All @@ -52,7 +49,7 @@ If you want to add it to your own libraries `Package.swift`, use this code inste

```swift
dependencies: [
.package(url: "https://github.com/elgatosf/streamdeck-kit-ipad.git", upToNextMajor: "0.0.1")
.package(url: "https://github.com/elgatosf/streamdeck-kit-ipad.git", upToNextMajor: "1.0.0")
]
```

Expand Down
28 changes: 18 additions & 10 deletions Sources/StreamDeckKit/Device/StreamDeckClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ final class StreamDeckClient {
}

private let inputEventMapper = InputEventMapper()
private var errorHandler: ClientErrorHandler?

private var service: io_service_t
private var connection: io_connect_t = IO_OBJECT_NULL
Expand Down Expand Up @@ -150,7 +151,7 @@ final class StreamDeckClient {
let (ret, output) = getScalar(SDExternalMethod_getDriverVersion, 3)

guard let output = output else {
os_log(.error, "Error calling scalar method getDriverVersion (\(String(ioReturn: ret))")
log(.error, "Error calling scalar method getDriverVersion (\(String(ioReturn: ret))")
return nil
}

Expand Down Expand Up @@ -207,7 +208,7 @@ final class StreamDeckClient {
}

guard ret == kIOReturnSuccess else {
os_log(.error, "Error getStruct<\(String(reflecting: Value.self))>() \(String(format: "0x%08X", ret)) (\(String(ioReturn: ret)))")
log(.error, "Error getStruct<\(String(reflecting: Value.self))>() \(String(format: "0x%08X", ret)) (\(String(ioReturn: ret)))")
return nil
}

Expand All @@ -233,7 +234,7 @@ final class StreamDeckClient {
private func callScalar(_ method: SDExternalMethod, _ args: UInt64 ...) -> IOReturn {
let ret = IOConnectCallScalarMethod(connection, method.rawValue, args, UInt32(args.count), nil, nil)
if ret != kIOReturnSuccess {
os_log(.error, "Error calling scalar method \(String(describing: method)) (\(String(ioReturn: ret))")
log(.error, "Error calling scalar method \(String(describing: method)) (\(String(ioReturn: ret))")
}
return ret
}
Expand Down Expand Up @@ -268,14 +269,15 @@ final class StreamDeckClient {

let callback: IOAsyncCallback = { context, result, args, argsCount in
guard let context = context else {
os_log(.error, "Context is nil in async input event callback")
log(.error, "Context is nil in async input event callback")
return
}

let client = Unmanaged<StreamDeckClient>.fromOpaque(context).takeUnretainedValue()

guard result == kIOReturnSuccess else {
os_log(.error, "Input event callback received non-success status (\(String(ioReturn: result))) - going to close")
log(.error, "Input event callback received non-success status (\(String(ioReturn: result))) - going to close")
client.errorHandler?(.disconnected(reason: "Input event callback is erroneous."))
client.close()
return
}
Expand Down Expand Up @@ -306,7 +308,9 @@ final class StreamDeckClient {
)

guard ret == kIOReturnSuccess else {
os_log(.error, "Error subscribing to input events (\(String(ioReturn: ret)))")
let errorMessage = "Error subscribing to input events (\(String(ioReturn: ret)))"
log(.error, errorMessage)
errorHandler?(.disconnected(reason: errorMessage))
return
}
}
Expand All @@ -315,6 +319,10 @@ final class StreamDeckClient {

extension StreamDeckClient: StreamDeckClientProtocol {

func setErrorHandler(_ handler: @escaping ClientErrorHandler) {
errorHandler = handler
}

func setBrightness(_ brightness: Int) {
callScalar(SDExternalMethod_setBrightness, UInt64(brightness))
}
Expand All @@ -330,7 +338,7 @@ extension StreamDeckClient: StreamDeckClientProtocol {
}

guard ret == kIOReturnSuccess else {
os_log(.error, "Error calling struct method `setKeyImage` (\(String(ioReturn: ret)))")
log(.error, "Error calling struct method `setKeyImage` (\(String(ioReturn: ret)))")
return
}
}
Expand All @@ -343,7 +351,7 @@ extension StreamDeckClient: StreamDeckClientProtocol {
}

guard ret == kIOReturnSuccess else {
os_log(.error, "Error calling setScreenImage \(String(format: "0x%08X", ret)) (\(String(ioReturn: ret)))")
log(.error, "Error calling setScreenImage \(String(format: "0x%08X", ret)) (\(String(ioReturn: ret)))")
return
}
}
Expand All @@ -356,7 +364,7 @@ extension StreamDeckClient: StreamDeckClientProtocol {
}

guard ret == kIOReturnSuccess else {
os_log(.error, "Error calling setWindowImage \(String(format: "0x%08X", ret)) (\(String(ioReturn: ret)))")
log(.error, "Error calling setWindowImage \(String(format: "0x%08X", ret)) (\(String(ioReturn: ret)))")
return
}
}
Expand All @@ -378,7 +386,7 @@ extension StreamDeckClient: StreamDeckClientProtocol {
}

guard ret == kIOReturnSuccess else {
os_log(.error, "Error calling struct method `setWindowImageAtXY` (\(String(ioReturn: ret)))")
log(.error, "Error calling struct method `setWindowImageAtXY` (\(String(ioReturn: ret)))")
return
}
}
Expand Down
15 changes: 15 additions & 0 deletions Sources/StreamDeckKit/Device/StreamDeckClientProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,11 @@ import Foundation
import StreamDeckCApi

public typealias InputEventHandler = @MainActor (InputEvent) -> Void
public typealias ClientErrorHandler = (StreamDeckClientError) -> Void

public protocol StreamDeckClientProtocol {
@MainActor func setInputEventHandler(_ handler: @escaping InputEventHandler)
func setErrorHandler(_ handler: @escaping ClientErrorHandler)
func setBrightness(_ brightness: Int)
func setKeyImage(_ data: Data, at index: Int)
func setScreenImage(_ data: Data)
Expand All @@ -43,3 +45,16 @@ public protocol StreamDeckClientProtocol {
func showLogo()
func close()
}

public enum StreamDeckClientError: Error {
/// Client will close.
case disconnected(reason: String)
}

extension StreamDeckClientError: LocalizedError {
public var errorDescription: String? {
switch self {
case .disconnected(let reason): "Client will close. Reason: \(reason)"
}
}
}
1 change: 1 addition & 0 deletions Sources/StreamDeckKit/Layout/StreamDeckClientDummy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import Foundation
final class StreamDeckClientDummy: StreamDeckClientProtocol {
public init() {}
public func setInputEventHandler(_ handler: @escaping InputEventHandler) {}
func setErrorHandler(_ handler: @escaping ClientErrorHandler) {}
func setBrightness(_ brightness: Int) {}
func setKeyImage(_ data: Data, at index: Int) {}
func setScreenImage(_ data: Data) {}
Expand Down
Loading

0 comments on commit 98793dd

Please sign in to comment.