diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d97a636 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,38 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: ["*"] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + lfs: true + + - run: make lint-ci + - run: make format-ci + + build: + needs: + - lint + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + lfs: true + + - uses: actions/cache@v3 + with: + path: | + .swiftpm-packages + key: v0-${{ runner.os }}-swiftpm-${{ hashFiles('**/Package.resolved') }} + restore-keys: v0-${{ runner.os }}-swiftpm- + + - run: make build diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 0000000..574859b --- /dev/null +++ b/.swiftformat @@ -0,0 +1,4 @@ +--swiftversion 5.7 +--indent 2 +--maxwidth 120 +--exclude WebSocketDemo-Shared/Schemas/foxglove diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..81f458a --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,15 @@ +excluded: + - .build + - .swiftpm-packages + - WebSocketDemo-Shared/Schemas/foxglove + +disabled_rules: + - function_body_length + - type_body_length + - file_length + - cyclomatic_complexity + - identifier_name + - opening_brace # conflicts with swiftformat + +trailing_comma: + mandatory_comma: true diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c8f56cd --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +.PHONY: build +build: + # https://developer.apple.com/documentation/xcode/building-swift-packages-or-apps-that-use-them-in-continuous-integration-workflows + # https://stackoverflow.com/questions/4969932/separate-build-directory-using-xcodebuild + # https://forums.swift.org/t/swiftpm-with-git-lfs/42396/4 + GIT_LFS_SKIP_DOWNLOAD_ERRORS=1 \ + xcodebuild \ + -disableAutomaticPackageResolution \ + -clonedSourcePackagesDirPath .swiftpm-packages \ + -destination generic/platform=iOS \ + -scheme "Foxglove Bridge" \ + SYMROOT="$(PWD)"/build \ + CODE_SIGN_IDENTITY="" \ + CODE_SIGNING_REQUIRED=NO \ + -configuration Release \ + clean build analyze + +.PHONY: lint-ci +lint-ci: + docker run -t --platform linux/amd64 --rm -v "$(PWD)":/work -w /work ghcr.io/realm/swiftlint:0.53.0 + +.PHONY: format-ci +format-ci: + docker run -t --rm -v "$(PWD)":/work ghcr.io/nicklockwood/swiftformat:0.52.8 --lint /work diff --git a/WebSocketDemo-Shared/CameraManager.swift b/WebSocketDemo-Shared/CameraManager.swift index a39f345..f6c9d2f 100644 --- a/WebSocketDemo-Shared/CameraManager.swift +++ b/WebSocketDemo-Shared/CameraManager.swift @@ -1,8 +1,8 @@ import AVFoundation -import VideoToolbox import Combine import CoreImage import UIKit +import VideoToolbox enum Camera: CaseIterable, Identifiable, CustomStringConvertible { case back @@ -18,7 +18,7 @@ enum Camera: CaseIterable, Identifiable, CustomStringConvertible { } var id: Self { - return self + self } } @@ -36,7 +36,7 @@ private func configureInputs(in session: AVCaptureSession, for camera: Camera) t for input in session.inputs { session.removeInput(input) } - + let device: AVCaptureDevice? switch camera { case .back: @@ -44,7 +44,6 @@ private func configureInputs(in session: AVCaptureSession, for camera: Camera) t case .front: device = .default(.builtInWideAngleCamera, for: .video, position: .front) } - guard let device else { throw CameraError.noCameraDevice } @@ -53,7 +52,7 @@ private func configureInputs(in session: AVCaptureSession, for camera: Camera) t let input = try AVCaptureDeviceInput(device: device) print("ranges: \(input.device.activeFormat.videoSupportedFrameRateRanges)") session.addInput(input) - } catch let error { + } catch { print("failed to create device input: \(error)") } } @@ -70,26 +69,26 @@ class CameraManager: NSObject, ObservableObject { private var compressionSession: VTCompressionSession? private var videoOutput: AVCaptureVideoDataOutput? private var forceKeyFrame = true - + let h264Frames = PassthroughSubject() let jpegFrames = PassthroughSubject() let calibrationData = PassthroughSubject() - + @Published var currentError: Error? - + @Published var droppedFrames = 0 - + @Published var activeCamera: Camera = .back { didSet { print("set active camera \(activeCamera)") reconfigureSession() } } - + private var _useVideoCompressionFlag: Int32 = 0 public var useVideoCompression: Bool { get { - return OSAtomicAdd32(0, &_useVideoCompressionFlag) != 0 + OSAtomicAdd32(0, &_useVideoCompressionFlag) != 0 } set { if newValue { @@ -97,36 +96,35 @@ class CameraManager: NSObject, ObservableObject { } else { OSAtomicTestAndClear(0, &_useVideoCompressionFlag) } - + reconfigureSession() } } - + override init() { super.init() useVideoCompression = true } - + @MainActor func startCameraUpdates() { Task { @MainActor in droppedFrames = 0 } - let activeCamera = self.activeCamera - + let activeCamera = activeCamera + queue.async { [self] in let captureSession = AVCaptureSession() - + do { try configureInputs(in: captureSession, for: activeCamera) - } catch let error { + } catch { print("error starting session: \(error)") } captureSession.sessionPreset = useVideoCompression ? .high : .medium - - + let output = AVCaptureVideoDataOutput() - output.setSampleBufferDelegate(self, queue: self.queue) + output.setSampleBufferDelegate(self, queue: queue) captureSession.addOutput(output) if let connection = output.connection(with: .video) { print("intrinsic supported: \(connection.isCameraIntrinsicMatrixDeliverySupported)") @@ -134,8 +132,8 @@ class CameraManager: NSObject, ObservableObject { connection.isCameraIntrinsicMatrixDeliveryEnabled = true } } - self.videoOutput = output - + videoOutput = output + do { try createCompressionSession(for: output) } catch let err { @@ -147,40 +145,40 @@ class CameraManager: NSObject, ObservableObject { Task { @MainActor in currentError = nil } - + captureSession.startRunning() self.captureSession = captureSession } } - + @MainActor func stopCameraUpdates() { queue.async(qos: .userInitiated) { [self] in captureSession?.stopRunning() } } - + private func createCompressionSession(for output: AVCaptureVideoDataOutput) throws { if let compressionSession { VTCompressionSessionInvalidate(compressionSession) self.compressionSession = nil } - + guard let width = output.videoSettings[kCVPixelBufferWidthKey as String] as? Int32, let height = output.videoSettings[kCVPixelBufferHeightKey as String] as? Int32 else { throw CameraError.unknownSize } - + var err: OSStatus - + err = VTCompressionSessionCreate( allocator: kCFAllocatorDefault, width: width, height: height, codecType: kCMVideoCodecType_H264, encoderSpecification: [ - kVTVideoEncoderSpecification_EnableLowLatencyRateControl: kCFBooleanTrue + kVTVideoEncoderSpecification_EnableLowLatencyRateControl: kCFBooleanTrue, ] as CFDictionary, imageBufferAttributes: nil, compressedDataAllocator: nil, @@ -188,17 +186,21 @@ class CameraManager: NSObject, ObservableObject { refcon: nil, compressionSessionOut: &compressionSession ) - guard err == noErr, let compressionSession = compressionSession else { + guard err == noErr, let compressionSession else { print("VTCompressionSessionCreate failed (\(err))") throw NSError(domain: NSOSStatusErrorDomain, code: Int(err)) } - - err = VTSessionSetProperty(compressionSession, key: kVTCompressionPropertyKey_ProfileLevel, value: kVTProfileLevel_H264_Main_AutoLevel) + + err = VTSessionSetProperty( + compressionSession, + key: kVTCompressionPropertyKey_ProfileLevel, + value: kVTProfileLevel_H264_Main_AutoLevel + ) guard err == noErr else { print("VTSessionSetProperty(kVTCompressionPropertyKey_ProfileLevel) failed (\(err))") throw NSError(domain: NSOSStatusErrorDomain, code: Int(err)) } - + // Indicate that the compression session is in real time, which streaming // requires. err = VTSessionSetProperty(compressionSession, key: kVTCompressionPropertyKey_RealTime, value: kCFBooleanTrue) @@ -207,30 +209,42 @@ class CameraManager: NSObject, ObservableObject { throw NSError(domain: NSOSStatusErrorDomain, code: Int(err)) } // Enables temporal compression. - err = VTSessionSetProperty(compressionSession, key: kVTCompressionPropertyKey_AllowTemporalCompression, value: kCFBooleanTrue) + err = VTSessionSetProperty( + compressionSession, + key: kVTCompressionPropertyKey_AllowTemporalCompression, + value: kCFBooleanTrue + ) guard err == noErr else { print("Warning: VTSessionSetProperty(kVTCompressionPropertyKey_AllowTemporalCompression) failed (\(err))") throw NSError(domain: NSOSStatusErrorDomain, code: Int(err)) } - + // Disable frame reordering - err = VTSessionSetProperty(compressionSession, key: kVTCompressionPropertyKey_AllowFrameReordering, value: kCFBooleanFalse) + err = VTSessionSetProperty( + compressionSession, + key: kVTCompressionPropertyKey_AllowFrameReordering, + value: kCFBooleanFalse + ) guard err == noErr else { print("Warning: VTSessionSetProperty(kVTCompressionPropertyKey_AllowFrameReordering) failed (\(err))") throw NSError(domain: NSOSStatusErrorDomain, code: Int(err)) } // Require key frames every 2 seconds - err = VTSessionSetProperty(compressionSession, key: kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration, value: 2 as CFNumber) + err = VTSessionSetProperty( + compressionSession, + key: kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration, + value: 2 as CFNumber + ) guard err == noErr else { print("Warning: VTSessionSetProperty(kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration) failed (\(err))") throw NSError(domain: NSOSStatusErrorDomain, code: Int(err)) } } - + private func reconfigureSession() { - let activeCamera = self.activeCamera - + let activeCamera = activeCamera + queue.async(qos: .userInitiated) { [self] in guard let session = captureSession else { return @@ -239,7 +253,7 @@ class CameraManager: NSObject, ObservableObject { session.beginConfiguration() do { try configureInputs(in: session, for: activeCamera) - } catch let error { + } catch { print("error changing session: \(error)") } session.sessionPreset = useVideoCompression ? .high : .medium @@ -250,14 +264,15 @@ class CameraManager: NSObject, ObservableObject { } extension CameraManager: AVCaptureVideoDataOutputSampleBufferDelegate { - func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { + func captureOutput(_: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from _: AVCaptureConnection) { guard let imageBuffer = sampleBuffer.imageBuffer else { print("no image buffer :(") return } - + if let matrixData = sampleBuffer.attachments[.cameraIntrinsicMatrix]?.value as? Data, - matrixData.count == MemoryLayout.size { + matrixData.count == MemoryLayout.size + { let matrix = matrixData.withUnsafeBytes { $0.load(as: matrix_float3x3.self) } let width = CVPixelBufferGetWidth(imageBuffer) let height = CVPixelBufferGetHeight(imageBuffer) @@ -265,7 +280,7 @@ extension CameraManager: AVCaptureVideoDataOutputSampleBufferDelegate { calibrationData.send(CalibrationData(intrinsicMatrix: matrix, width: width, height: height)) } } - + if !useVideoCompression { let img = UIImage(ciImage: CIImage(cvImageBuffer: imageBuffer)) guard let jpeg = img.jpegData(compressionQuality: 0.8) else { @@ -277,30 +292,30 @@ extension CameraManager: AVCaptureVideoDataOutputSampleBufferDelegate { } return } - + guard let compressionSession else { print("no compression session") return } - + var err: OSStatus - + err = VTCompressionSessionEncodeFrame( compressionSession, imageBuffer: imageBuffer, presentationTimeStamp: sampleBuffer.presentationTimeStamp, duration: .invalid, frameProperties: [ - kVTEncodeFrameOptionKey_ForceKeyFrame: forceKeyFrame ? kCFBooleanTrue : kCFBooleanFalse + kVTEncodeFrameOptionKey_ForceKeyFrame: forceKeyFrame ? kCFBooleanTrue : kCFBooleanFalse, ] as CFDictionary, infoFlagsOut: nil - ) { [self] (status, infoFlags, sampleBuffer) in + ) { [self] status, infoFlags, sampleBuffer in queue.async(qos: .userInteractive) { [self] in if infoFlags.contains(.frameDropped) { print("Encoder dropped the frame with status \(status)") return } - + guard status == noErr else { forceKeyFrame = true print("Encoder returned an error for frame with \(status)") @@ -313,24 +328,24 @@ extension CameraManager: AVCaptureVideoDataOutputSampleBufferDelegate { print("Encoder returned an unexpected NULL sampleBuffer for frame") return } - + guard let annexBData = sampleBuffer.dataBufferAsAnnexB() else { print("Unable to translate to Annex B format") forceKeyFrame = true return } - + forceKeyFrame = false Task { @MainActor in h264Frames.send(annexBData) } } } - + guard err == noErr else { forceKeyFrame = true print("Encode call failed: \(err) \(NSError(domain: NSOSStatusErrorDomain, code: Int(err)))") - + // When the app is backgrounded and comes back to the foreground, // the compression session becomes invalid, so we recreate it if err == kVTInvalidSessionErr, let videoOutput { @@ -357,7 +372,7 @@ extension CameraManager: AVCaptureVideoDataOutputSampleBufferDelegate { } } - func captureOutput(_ output: AVCaptureOutput, didDrop sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { + func captureOutput(_: AVCaptureOutput, didDrop _: CMSampleBuffer, from _: AVCaptureConnection) { Task { @MainActor in droppedFrames += 1 } @@ -374,12 +389,12 @@ extension CMSampleBuffer { do { var result = Data() let startCode = Data([0x00, 0x00, 0x00, 0x01]) - + try formatDescription.forEachParameterSet { buf in result.append(startCode) result.append(buf) } - + try dataBuffer.withContiguousStorage { rawBuffer in // Since the startCode is 4 bytes, we can append the whole AVCC buffer to the output, // and then replace the 4-byte length values with start codes. @@ -387,13 +402,16 @@ extension CMSampleBuffer { result.append(rawBuffer.assumingMemoryBound(to: UInt8.self)) result.withUnsafeMutableBytes { resultBuffer in while offset + 4 < resultBuffer.count { - let nalUnitLength = Int(UInt32(bigEndian: resultBuffer.loadUnaligned(fromByteOffset: offset, as: UInt32.self))) - resultBuffer[offset..? = nil + + for idx in 0 ..< parameterSetCount { + var ptr: UnsafePointer? var size = 0 status = CMVideoFormatDescriptionGetH264ParameterSetAtIndex( self, diff --git a/WebSocketDemo-Shared/CardToggle.swift b/WebSocketDemo-Shared/CardToggle.swift index c835d46..865682e 100644 --- a/WebSocketDemo-Shared/CardToggle.swift +++ b/WebSocketDemo-Shared/CardToggle.swift @@ -9,8 +9,8 @@ struct CardToggle: View { @Environment(\.isEnabled) var isEnabled - internal init(isOn: Binding, dashed: Bool = false, @ViewBuilder content: @escaping () -> Content) { - self._isOn = isOn + init(isOn: Binding, dashed: Bool = false, @ViewBuilder content: @escaping () -> Content) { + _isOn = isOn self.dashed = dashed self.content = content } diff --git a/WebSocketDemo-Shared/ContentView.swift b/WebSocketDemo-Shared/ContentView.swift index b41dcbb..cb54e67 100644 --- a/WebSocketDemo-Shared/ContentView.swift +++ b/WebSocketDemo-Shared/ContentView.swift @@ -1,6 +1,6 @@ -import SwiftUI import Charts import StoreKit +import SwiftUI private let isAppClip = Bundle.main.bundleIdentifier?.hasSuffix("appclip") ?? false private let feedbackGenerator = UINotificationFeedbackGenerator() @@ -44,9 +44,9 @@ public struct ContentView: View { .onAppear { onboardingShown = !onboardingCompleted } - .sheet(isPresented: $onboardingShown, onDismiss: { + .sheet(isPresented: $onboardingShown) { onboardingCompleted = true - }) { + } content: { OnboardingView( isConnected: !server.clientEndpointNames.isEmpty, serverURL: server.addresses.first.flatMap { @@ -130,25 +130,25 @@ public struct ContentView: View { } } /* - CardToggle(isOn: $server.sendWatchData, dashed: isAppClip) { - Text("Heart Rate (Apple Watch)") - .multilineTextAlignment(.center) - if isAppClip { - Text("Requires full app") - .font(.caption) - .foregroundColor(.secondary) - } - }.disabled(isAppClip) - .onTapGesture { - if isAppClip && !appStoreOverlayShown { - appStoreOverlayShown = true - feedbackGenerator.notificationOccurred(.warning) - } - } - .appStoreOverlay(isPresented: $appStoreOverlayShown) { - SKOverlay.AppClipConfiguration(position: .bottom) - } - */ + CardToggle(isOn: $server.sendWatchData, dashed: isAppClip) { + Text("Heart Rate (Apple Watch)") + .multilineTextAlignment(.center) + if isAppClip { + Text("Requires full app") + .font(.caption) + .foregroundColor(.secondary) + } + }.disabled(isAppClip) + .onTapGesture { + if isAppClip && !appStoreOverlayShown { + appStoreOverlayShown = true + feedbackGenerator.notificationOccurred(.warning) + } + } + .appStoreOverlay(isPresented: $appStoreOverlayShown) { + SKOverlay.AppClipConfiguration(position: .bottom) + } + */ }.padding() } } @@ -158,7 +158,7 @@ public struct ContentView: View { if let port = server.actualPort { Section { let addrs = Array(server.addresses.enumerated()) - ForEach(addrs, id: \.offset) { (_, addr) in + ForEach(addrs, id: \.offset) { _, addr in IPAddressRow(address: addr, port: port) } } header: { @@ -186,7 +186,8 @@ public struct ContentView: View { } footer: { let info = Bundle.main.infoDictionary if let version = info?["CFBundleShortVersionString"] as? String, - let build = info?["CFBundleVersion"] as? String { + let build = info?["CFBundleVersion"] as? String + { Text("Version \(version) (\(build))") .frame(maxWidth: .infinity) } diff --git a/WebSocketDemo-Shared/FoxgloveServer.swift b/WebSocketDemo-Shared/FoxgloveServer.swift index 0791504..21f1b14 100644 --- a/WebSocketDemo-Shared/FoxgloveServer.swift +++ b/WebSocketDemo-Shared/FoxgloveServer.swift @@ -1,11 +1,14 @@ -import Network -import Foundation import CoreMotion +import Foundation +import Network + +// swiftlint:disable:next blanket_disable_command +// swiftlint:disable force_try todo extension NWConnection: Hashable, Comparable, Identifiable { public static func < (lhs: NWConnection, rhs: NWConnection) -> Bool { switch (lhs.endpoint, rhs.endpoint) { - case (.hostPort(let host1, _), .hostPort(host: let host2, _)): + case let (.hostPort(host1, _), .hostPort(host: host2, _)): return host1.debugDescription < host2.debugDescription default: @@ -14,7 +17,7 @@ extension NWConnection: Hashable, Comparable, Identifiable { } public static func == (lhs: NWConnection, rhs: NWConnection) -> Bool { - return lhs === rhs + lhs === rhs } public func hash(into hasher: inout Hasher) { @@ -22,7 +25,7 @@ extension NWConnection: Hashable, Comparable, Identifiable { } public var id: ObjectIdentifier { - return ObjectIdentifier(self) + ObjectIdentifier(self) } } @@ -37,7 +40,7 @@ class ClientInfo { init(connection: NWConnection) { self.connection = connection - self.name = connection.endpoint.debugDescription + name = connection.endpoint.debugDescription } } @@ -62,7 +65,7 @@ class FoxgloveServer: ObservableObject { @MainActor var clientEndpointNames: [String] { - return clients.keys.sorted().map { $0.endpoint.debugDescription } + clients.keys.sorted().map(\.endpoint.debugDescription) } @MainActor var channels: [ChannelID: Channel] = [:] @@ -74,7 +77,7 @@ class FoxgloveServer: ObservableObject { print(params.defaultProtocolStack.applicationProtocols) let opts = NWProtocolWebSocket.Options() - opts.setClientRequestHandler(queue) { subprotocols, additionalHeaders in + opts.setClientRequestHandler(queue) { subprotocols, _ in let subproto = "foxglove.websocket.v1" if subprotocols.contains(subproto) { return NWProtocolWebSocket.Response(status: .accept, subprotocol: subproto) @@ -103,7 +106,7 @@ class FoxgloveServer: ObservableObject { self?.handleNewConnection(newConnection) } listener.start(queue: queue) - } catch let error { + } catch { print("Error \(error)") } } @@ -122,7 +125,7 @@ class FoxgloveServer: ObservableObject { self.handleClientMessage(info, data, context, isComplete, error) receive() } - //TODO: emit error + // TODO: emit error if let error { print("receive error: \(error)") } @@ -137,7 +140,7 @@ class FoxgloveServer: ObservableObject { let closed: Bool switch connection.state { - case .cancelled, .failed(_): + case .cancelled, .failed: closed = true default: closed = false @@ -174,10 +177,9 @@ class FoxgloveServer: ObservableObject { } receive() - connection.start(queue: self.queue) + connection.start(queue: queue) } - static let binaryMessage = NWConnection.ContentContext(identifier: "", metadata: [ NWProtocolWebSocket.Metadata(opcode: .binary), ]) @@ -226,7 +228,7 @@ class FoxgloveServer: ObservableObject { "schema": schema, ], ], to: conn) - } catch let error { + } catch { // TODO: emit error print("addchannel error: \(error)") } @@ -235,7 +237,6 @@ class FoxgloveServer: ObservableObject { return newID } - /** * Remove a previously advertised channel and inform any connected clients. */ @@ -253,13 +254,13 @@ class FoxgloveServer: ObservableObject { } do { - //TODO: serialize once + // TODO: serialize once try sendJson([ "op": "unadvertise", "channelIds": [channelID], ], to: conn) - } catch let error { - //TODO: emit error + } catch { + // TODO: emit error print("remove error \(error)") } } @@ -278,53 +279,74 @@ class FoxgloveServer: ObservableObject { } } - private func sendMessageData(on connection: NWConnection, subscriptionID: SubscriptionID, timestamp: UInt64, payload: Data) { + private func sendMessageData( + on connection: NWConnection, + subscriptionID: SubscriptionID, + timestamp: UInt64, + payload: Data + ) { var header = Data(count: 1 + 4 + 8) header[0] = BinaryOpcode.messageData.rawValue withUnsafeBytes(of: subscriptionID.littleEndian) { - header.replaceSubrange(1..<5, with: $0.baseAddress!, count: $0.count) + header.replaceSubrange(1 ..< 5, with: $0.baseAddress!, count: $0.count) } withUnsafeBytes(of: timestamp.littleEndian) { - header.replaceSubrange(5..<13, with: $0.baseAddress!, count: $0.count) + header.replaceSubrange(5 ..< 13, with: $0.baseAddress!, count: $0.count) } - connection.send(content: header, contentContext: Self.binaryMessage, isComplete: false, completion: .contentProcessed { error in - if let error = error { - print("error sending1: \(error)") + connection.send( + content: header, + contentContext: Self.binaryMessage, + isComplete: false, + completion: .contentProcessed { error in + if let error { + print("error sending1: \(error)") + } } - }) - connection.send(content: payload, contentContext: Self.binaryMessage, isComplete: true, completion: .contentProcessed { error in - if let error = error { - print("error sending2: \(error)") + ) + connection.send( + content: payload, + contentContext: Self.binaryMessage, + isComplete: true, + completion: .contentProcessed { error in + if let error { + print("error sending2: \(error)") + } } - }) + ) } private func sendJson(_ obj: Any, to connection: NWConnection) throws { let data = try JSONSerialization.data(withJSONObject: obj) - connection.send(content: data, contentContext: Self.jsonMessage, completion: .contentProcessed({ error in + connection.send(content: data, contentContext: Self.jsonMessage, completion: .contentProcessed { error in if let error { print("send error: \(error)") } - })) + }) } private func sendBinary(_ data: Data, to connection: NWConnection) throws { - connection.send(content: data, contentContext: Self.binaryMessage, completion: .contentProcessed({ error in + connection.send(content: data, contentContext: Self.binaryMessage, completion: .contentProcessed { error in if let error { print("send error: \(error)") } - })) + }) } - private func handleClientMessage(_ client: ClientInfo, _ data: Data, _ context: NWConnection.ContentContext, _ isComplete: Bool, _ error: NWError?) { + private func handleClientMessage( + _ client: ClientInfo, + _ data: Data, + _ context: NWConnection.ContentContext, + _: Bool, + _ error: NWError? + ) { do { let metadata = context.protocolMetadata(definition: NWProtocolWebSocket.definition) let isText = (metadata as? NWProtocolWebSocket.Metadata)?.opcode == .text if isText { let msg = try JSONDecoder().decode(ClientMessage.self, from: data) switch msg { - case .subscribe(let msg): + case let .subscribe(msg): Task { @MainActor in for sub in msg.subscriptions { // TODO: emit status messages for warnings (see TS impl) @@ -338,15 +360,15 @@ class FoxgloveServer: ObservableObject { } // TODO: emit subscribe } - case .unsubscribe(let msg): + case let .unsubscribe(msg): Task { @MainActor in for sub in msg.subscriptionIds { guard let chanID = client.subscriptions[sub] else { - //TODO: error + // TODO: error continue } client.subscriptions[sub] = nil - //TODO: cleanup index usage? + // TODO: cleanup index usage? client.subscriptionsByChannel[chanID]?.remove(sub) if client.subscriptionsByChannel[chanID]?.isEmpty == true { client.subscriptionsByChannel[chanID] = nil @@ -361,8 +383,8 @@ class FoxgloveServer: ObservableObject { } else { print("Got client message: data \(data)") } - } catch let error { - //TODO: emit error + } catch { + // TODO: emit error print("client msg error: \(error)") } } @@ -382,6 +404,7 @@ struct Subscribe: Decodable { let id: SubscriptionID let channelId: ChannelID } + let subscriptions: [Subscription] } @@ -401,8 +424,8 @@ enum ClientMessage: Decodable { init(from decoder: Decoder) throws { let op = try decoder.container(keyedBy: CodingKeys.self).decode(String.self, forKey: .op) switch ClientOp(rawValue: op) { - case .subscribe: self = .subscribe(try Subscribe(from: decoder)) - case .unsubscribe: self = .unsubscribe(try Unsubscribe(from: decoder)) + case .subscribe: self = try .subscribe(Subscribe(from: decoder)) + case .unsubscribe: self = try .unsubscribe(Unsubscribe(from: decoder)) case nil: throw FoxgloveServerError.unrecognizedOpcode(op) } diff --git a/WebSocketDemo-Shared/IPAddressRow.swift b/WebSocketDemo-Shared/IPAddressRow.swift index a5ffa29..ba37d23 100644 --- a/WebSocketDemo-Shared/IPAddressRow.swift +++ b/WebSocketDemo-Shared/IPAddressRow.swift @@ -1,9 +1,9 @@ -import SwiftUI import Network +import SwiftUI extension IPAddress { var withoutInterface: IPAddress { - return Self.init(rawValue, nil) ?? self + Self(rawValue, nil) ?? self } var urlString: String { @@ -63,7 +63,7 @@ struct IPAddressRow: View { var url = URL(string: "https://studio.foxglove.dev/")! url.append(queryItems: [ URLQueryItem(name: "ds", value: "foxglove-websocket"), - URLQueryItem(name: "ds.url", value: "ws://\(address.withoutInterface.urlString):\(String(port.rawValue))") + URLQueryItem(name: "ds.url", value: "ws://\(address.withoutInterface.urlString):\(String(port.rawValue))"), ]) return url }() diff --git a/WebSocketDemo-Shared/Onboarding/ConnectionView.swift b/WebSocketDemo-Shared/Onboarding/ConnectionView.swift index b7f63ca..90b43b1 100644 --- a/WebSocketDemo-Shared/Onboarding/ConnectionView.swift +++ b/WebSocketDemo-Shared/Onboarding/ConnectionView.swift @@ -42,7 +42,7 @@ struct ConnectionView: View { } } } - .disabled(!isConnected) + .disabled(!isConnected) } var body: some View { @@ -94,7 +94,10 @@ struct ConnectionView: View { .foregroundColor(.secondary) .font(.system(size: bulletSize)) VStack(alignment: .leading, spacing: 6) { - Text("Click the \(Image(systemName: "shield.lefthalf.filled")) icon in the address bar, then click “Load Unsafe Scripts”") + Text( + // swiftlint:disable:next line_length + "Click the \(Image(systemName: "shield.lefthalf.filled")) icon in the address bar, then click “Load Unsafe Scripts”" + ) Text("This setting allows a “`https://`” page to connect to a “`ws://`” URL") .font(.caption) .foregroundColor(.secondary) diff --git a/WebSocketDemo-Shared/Onboarding/ContinueButton.swift b/WebSocketDemo-Shared/Onboarding/ContinueButton.swift index 5bcb572..afd3d24 100644 --- a/WebSocketDemo-Shared/Onboarding/ContinueButton.swift +++ b/WebSocketDemo-Shared/Onboarding/ContinueButton.swift @@ -13,7 +13,7 @@ extension EnvironmentValues { extension View { func onContinue(_ action: @escaping () -> Void) -> some View { - self.environment(\.onContinue, action) + environment(\.onContinue, action) } } diff --git a/WebSocketDemo-Shared/Onboarding/IntroView.swift b/WebSocketDemo-Shared/Onboarding/IntroView.swift index f7e2ac0..e85a43c 100644 --- a/WebSocketDemo-Shared/Onboarding/IntroView.swift +++ b/WebSocketDemo-Shared/Onboarding/IntroView.swift @@ -1,11 +1,11 @@ import SwiftUI -fileprivate class Dummy {} +private class Dummy {} extension Text { // https://stackoverflow.com/a/64731044/23649 func fixedHeight() -> some View { - self.fixedSize(horizontal: false, vertical: true) + fixedSize(horizontal: false, vertical: true) } } @@ -63,8 +63,8 @@ struct IntroView: View { Spacer(minLength: 16).fixedSize() Text(""" -Stream data from your \(deviceModel)’s built-in sensors to Foxglove Studio for instant visualization. -""") + Stream data from your \(deviceModel)’s built-in sensors to Foxglove Studio for instant visualization. + """) .fixedHeight() .lineSpacing(4) diff --git a/WebSocketDemo-Shared/Onboarding/OnboardingView.swift b/WebSocketDemo-Shared/Onboarding/OnboardingView.swift index 542cf02..3aebce6 100644 --- a/WebSocketDemo-Shared/Onboarding/OnboardingView.swift +++ b/WebSocketDemo-Shared/Onboarding/OnboardingView.swift @@ -54,7 +54,7 @@ struct OnboardingStepWrapper: View { .multilineTextAlignment(.center) .padding(.horizontal, 32) .padding(.bottom, 32) - // Ensure the content takes up at least one full page in the scroll view + // Ensure the content takes up at least one full page in the scroll view .frame(maxWidth: .infinity, minHeight: proxy.size.height) } } diff --git a/WebSocketDemo-Shared/Onboarding/StudioUsageView.swift b/WebSocketDemo-Shared/Onboarding/StudioUsageView.swift index f154924..cfc4d77 100644 --- a/WebSocketDemo-Shared/Onboarding/StudioUsageView.swift +++ b/WebSocketDemo-Shared/Onboarding/StudioUsageView.swift @@ -1,10 +1,10 @@ -import SwiftUI import SafariServices +import SwiftUI struct SafariView: UIViewControllerRepresentable { let url: URL - func makeUIViewController(context: Context) -> SFSafariViewController { + func makeUIViewController(context _: Context) -> SFSafariViewController { let config = SFSafariViewController.Configuration() // Bar collapsing looks buggy when presented in a sheet. config.barCollapsingEnabled = false @@ -12,7 +12,7 @@ struct SafariView: UIViewControllerRepresentable { return SFSafariViewController(url: url, configuration: config) } - func updateUIViewController(_ uiViewController: SFSafariViewController, context: Context) { + func updateUIViewController(_: SFSafariViewController, context _: Context) { print("Warning: unable to update SFSafariViewController") } } @@ -92,8 +92,10 @@ struct StudioUsageView: View { Spacer(minLength: 32).fixedSize() Text(""" -Try adding the **3D**, **Image**, **Map**, and **Plot** panels to your layout to start exploring your \(deviceModel)’s sensor data. -""") + Try adding the **3D**, **Image**, **Map**, and **Plot** panels to your layout to start exploring your \( + deviceModel + )’s sensor data. + """) Spacer(minLength: 32).fixedSize() Button("View Docs") { showDocs = true diff --git a/WebSocketDemo-Shared/Server.swift b/WebSocketDemo-Shared/Server.swift index 5830174..b316831 100644 --- a/WebSocketDemo-Shared/Server.swift +++ b/WebSocketDemo-Shared/Server.swift @@ -1,11 +1,14 @@ -import SwiftUI -import CoreMotion import AVFoundation import Combine -import Network import CoreLocation +import CoreMotion +import Network +import SwiftUI import WatchConnectivity +// swiftlint:disable:next blanket_disable_command +// swiftlint:disable force_try + struct CPUUsage: Encodable, Identifiable { let usage: Double let date: Date @@ -30,6 +33,7 @@ struct Timestamp: Encodable { let sec: UInt32 let nsec: UInt32 } + extension Timestamp { init(_ date: Date) { let seconds = date.timeIntervalSince1970 @@ -43,6 +47,7 @@ extension Timestamp { nsec = intNsec } } + struct Health: Encodable { let heart_rate: Double let timestamp: Timestamp @@ -60,7 +65,7 @@ class Server: NSObject, ObservableObject, AVCaptureVideoDataOutputSampleBufferDe @Published var actualPort: NWEndpoint.Port? let server = FoxgloveServer() - + let cameraManager = CameraManager() let motionManager = CMMotionManager() let locationManager = CLLocationManager() @@ -79,7 +84,7 @@ class Server: NSObject, ObservableObject, AVCaptureVideoDataOutputSampleBufferDe let watchSession = WCSession.default @Published private(set) var droppedVideoFrames = 0 - + @Published private(set) var cameraError: Error? @Published var sendPose = true { @@ -101,11 +106,13 @@ class Server: NSObject, ObservableObject, AVCaptureVideoDataOutputSampleBufferDe } } } + @Published var useVideoCompression = true { didSet { cameraManager.useVideoCompression = useVideoCompression } } + var hasCameraPermission: Bool { AVCaptureDevice.authorizationStatus(for: .video) == .authorized } @@ -119,8 +126,10 @@ class Server: NSObject, ObservableObject, AVCaptureVideoDataOutputSampleBufferDe } } } + var hasLocationPermission: Bool { - locationManager.authorizationStatus == .authorizedWhenInUse || locationManager.authorizationStatus == .authorizedAlways + locationManager.authorizationStatus == .authorizedWhenInUse || locationManager + .authorizationStatus == .authorizedAlways } @Published var sendCPU = true { @@ -158,11 +167,9 @@ class Server: NSObject, ObservableObject, AVCaptureVideoDataOutputSampleBufferDe cameraManager.activeCamera = activeCamera } } - - var clientEndpointNames: [String] { - return server.clientEndpointNames + server.clientEndpointNames } var cpuTimer: Timer? @@ -182,57 +189,61 @@ class Server: NSObject, ObservableObject, AVCaptureVideoDataOutputSampleBufferDe topic: "camera_jpeg", encoding: "protobuf", schemaName: Foxglove_CompressedImage.protoMessageName, - schema: try! Data(contentsOf: Bundle(for: Self.self).url(forResource: "CompressedImage", withExtension: "bin")!).base64EncodedString() + schema: try! Data(contentsOf: Bundle(for: Self.self).url(forResource: "CompressedImage", withExtension: "bin")!) + .base64EncodedString() ) calibrationChannel = server.addChannel( topic: "calibration", encoding: "protobuf", schemaName: Foxglove_CameraCalibration.protoMessageName, - schema: try! Data(contentsOf: Bundle(for: Self.self).url(forResource: "CameraCalibration", withExtension: "bin")!).base64EncodedString() + schema: try! Data(contentsOf: Bundle(for: Self.self).url(forResource: "CameraCalibration", withExtension: "bin")!) + .base64EncodedString() ) h264Channel = server.addChannel( topic: "camera_h264", encoding: "protobuf", schemaName: Foxglove_CompressedVideo.protoMessageName, - schema: try! Data(contentsOf: Bundle(for: Self.self).url(forResource: "CompressedVideo", withExtension: "bin")!).base64EncodedString() + schema: try! Data(contentsOf: Bundle(for: Self.self).url(forResource: "CompressedVideo", withExtension: "bin")!) + .base64EncodedString() ) locationChannel = server.addChannel( topic: "gps", encoding: "protobuf", schemaName: Foxglove_LocationFix.protoMessageName, - schema: try! Data(contentsOf: Bundle(for: Self.self).url(forResource: "LocationFix", withExtension: "bin")!).base64EncodedString() + schema: try! Data(contentsOf: Bundle(for: Self.self).url(forResource: "LocationFix", withExtension: "bin")!) + .base64EncodedString() ) cpuChannel = server.addChannel(topic: "cpu", encoding: "json", schemaName: "CPU", schema: -#""" -{ - "type":"object", - "properties":{ - "usage":{"type":"number"} - } -} -"""#) + #""" + { + "type":"object", + "properties":{ + "usage":{"type":"number"} + } + } + """#) memChannel = server.addChannel(topic: "memory", encoding: "json", schemaName: "Memory", schema: -#""" -{ - "type":"object", - "properties":{ - "usage":{"type":"number"} - } -} -"""#) + #""" + { + "type":"object", + "properties":{ + "usage":{"type":"number"} + } + } + """#) healthChannel = server.addChannel(topic: "health", encoding: "json", schemaName: "Health", schema: -#""" -{ - "type":"object", - "properties":{ - "heart_rate":{"type":"number"}, - "timestamp":{ - "type":"object", - "properties":{"sec":{"type":"number"},"nsec":{"type":"number"}} - } - } -} -"""#) + #""" + { + "type":"object", + "properties":{ + "heart_rate":{"type":"number"}, + "timestamp":{ + "type":"object", + "properties":{"sec":{"type":"number"},"nsec":{"type":"number"}} + } + } + } + """#) super.init() server.start(preferredPort: preferredPort.flatMap { UInt16(exactly: $0) }) server.$port @@ -246,11 +257,11 @@ class Server: NSObject, ObservableObject, AVCaptureVideoDataOutputSampleBufferDe startCPUUpdates() startMemoryUpdates() watchSession.delegate = self - + cameraManager.$droppedFrames .assign(to: \.droppedVideoFrames, on: self) .store(in: &subscribers) - + cameraManager.$currentError .assign(to: \.cameraError, on: self) .store(in: &subscribers) @@ -264,7 +275,7 @@ class Server: NSObject, ObservableObject, AVCaptureVideoDataOutputSampleBufferDe msg.timestamp = .init(date: .now) msg.frameID = "camera" // Convert column-major to row-major - msg.k = (0..<3).flatMap { r in (0..<3).map { c in Double(calibration.intrinsicMatrix[c, r]) } } + msg.k = (0 ..< 3).flatMap { r in (0 ..< 3).map { c in Double(calibration.intrinsicMatrix[c, r]) } } msg.p = [ msg.k[0], msg.k[1], msg.k[2], 0, msg.k[3], msg.k[4], msg.k[5], 0, @@ -273,7 +284,11 @@ class Server: NSObject, ObservableObject, AVCaptureVideoDataOutputSampleBufferDe msg.width = UInt32(calibration.width) msg.height = UInt32(calibration.height) let data = try! msg.serializedData() - self.server.sendMessage(on: self.calibrationChannel, timestamp: DispatchTime.now().uptimeNanoseconds, payload: data) + self.server.sendMessage( + on: self.calibrationChannel, + timestamp: DispatchTime.now().uptimeNanoseconds, + payload: data + ) } .store(in: &subscribers) @@ -317,7 +332,7 @@ class Server: NSObject, ObservableObject, AVCaptureVideoDataOutputSampleBufferDe } func updateAddresses() { - self.addresses = getIPAddresses() + addresses = getIPAddresses() .filter { // Filter out some AirDrop interfaces that are not useful https://apple.stackexchange.com/q/394047/8318 if $0.interface?.name.hasPrefix("llw") == true || $0.interface?.name.hasPrefix("awdl") == true { @@ -389,7 +404,6 @@ class Server: NSObject, ObservableObject, AVCaptureVideoDataOutputSampleBufferDe memTimer = nil } - func startLocationUpdates() { locationManager.delegate = self locationManager.desiredAccuracy = kCLLocationAccuracyBest @@ -403,7 +417,7 @@ class Server: NSObject, ObservableObject, AVCaptureVideoDataOutputSampleBufferDe locationManager.stopUpdatingLocation() } - nonisolated func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + nonisolated func locationManager(_: CLLocationManager, didUpdateLocations locations: [CLLocation]) { guard let location: CLLocation = locations.last else { return } var msg = Foxglove_LocationFix() @@ -425,27 +439,28 @@ class Server: NSObject, ObservableObject, AVCaptureVideoDataOutputSampleBufferDe func startPoseUpdates() { motionManager.deviceMotionUpdateInterval = 0.02 - motionManager.startDeviceMotionUpdates(to: .main) { motion, error in + motionManager.startDeviceMotionUpdates(to: .main) { motion, _ in if let motion { self.sendPose(motion: motion) } } } + func stopPoseUpdates() { motionManager.stopDeviceMotionUpdates() } func sendPose(motion: CMDeviceMotion) { let data = try! JSONSerialization.data(withJSONObject: [ - "timestamp": ["sec":0,"nsec":0], + "timestamp": ["sec": 0, "nsec": 0], "frame_id": "root", "pose": [ - "position":["x":0,"y":0,"z":0], - "orientation":[ - "x":motion.attitude.quaternion.x, - "y":motion.attitude.quaternion.y, - "z":motion.attitude.quaternion.z, - "w":motion.attitude.quaternion.w, + "position": ["x": 0, "y": 0, "z": 0], + "orientation": [ + "x": motion.attitude.quaternion.x, + "y": motion.attitude.quaternion.y, + "z": motion.attitude.quaternion.z, + "w": motion.attitude.quaternion.w, ], ], ], options: .sortedKeys) @@ -454,28 +469,28 @@ class Server: NSObject, ObservableObject, AVCaptureVideoDataOutputSampleBufferDe } } - extension Server: WCSessionDelegate { - func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { + func session(_: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { print("watch activation completed: \(activationState), error: \(error)") } - func sessionDidBecomeInactive(_ session: WCSession) { + func sessionDidBecomeInactive(_: WCSession) { print("watch became inactive") } - func sessionDidDeactivate(_ session: WCSession) { + func sessionDidDeactivate(_: WCSession) { print("watch deactivated") } func startWatchUpdates() { watchSession.activate() } + func stopWatchUpdates() { print("stop watch updates?") } - func session(_ session: WCSession, didReceiveMessage message: [String : Any]) { + func session(_: WCSession, didReceiveMessage message: [String: Any]) { print("message from watch: \(message)") guard sendWatchData else { return diff --git a/WebSocketDemo-Shared/compareIPAddresses.swift b/WebSocketDemo-Shared/compareIPAddresses.swift index f03eba2..e06ba69 100644 --- a/WebSocketDemo-Shared/compareIPAddresses.swift +++ b/WebSocketDemo-Shared/compareIPAddresses.swift @@ -1,9 +1,9 @@ import Foundation import Network -fileprivate struct BytewiseLess: Comparable { +private struct BytewiseLess: Comparable { let data: Data - static func <(lhs: BytewiseLess, rhs: BytewiseLess) -> Bool { + static func < (lhs: BytewiseLess, rhs: BytewiseLess) -> Bool { if lhs.data.count < rhs.data.count { return true } @@ -18,10 +18,10 @@ fileprivate struct BytewiseLess: Comparable { } } -fileprivate struct TrueLess: Comparable { +private struct TrueLess: Comparable { let value: Bool - static func <(lhs: TrueLess, rhs: TrueLess) -> Bool { - return lhs.value && !rhs.value + static func < (lhs: TrueLess, rhs: TrueLess) -> Bool { + lhs.value && !rhs.value } } @@ -32,7 +32,7 @@ fileprivate struct TrueLess: Comparable { - fall back to comparing raw addresses */ func compareIPAddresses(_ lhs: IPAddress, _ rhs: IPAddress) -> Bool { - return ( + ( TrueLess(value: lhs is IPv4Address), TrueLess(value: lhs.interface?.type == .wifi), TrueLess(value: lhs.interface?.type == .wiredEthernet), diff --git a/WebSocketDemo-Shared/getCPUUsage.swift b/WebSocketDemo-Shared/getCPUUsage.swift index 51fd1b9..c5d062d 100644 --- a/WebSocketDemo-Shared/getCPUUsage.swift +++ b/WebSocketDemo-Shared/getCPUUsage.swift @@ -1,7 +1,7 @@ import Darwin.Mach.host_info //// https://stackoverflow.com/a/44744883 -//func getCPUUsage() -> host_cpu_load_info? { +// func getCPUUsage() -> host_cpu_load_info? { // let HOST_CPU_LOAD_INFO_COUNT = MemoryLayout.stride / MemoryLayout.stride // // var size = mach_msg_type_number_t(HOST_CPU_LOAD_INFO_COUNT) @@ -18,22 +18,23 @@ import Darwin.Mach.host_info // let data = hostInfo.move() // hostInfo.deallocate() // return data -//} +// } +// swiftlint:disable:next line_length // From https://github.com/dani-gavrilov/GDPerformanceView-Swift/blob/171a656040135d667f4228c3ec82f2384770d87d/GDPerformanceView-Swift/GDPerformanceMonitoring/PerformanceСalculator.swift#L94 // See also: https://developer.apple.com/forums/thread/655349 func getCPUUsage() -> Double { - var totalUsageOfCPU: Double = 0.0 + var totalUsageOfCPU = 0.0 var threadsList: thread_act_array_t? var threadsCount = mach_msg_type_number_t(0) let threadsResult = withUnsafeMutablePointer(to: &threadsList) { - return $0.withMemoryRebound(to: thread_act_array_t?.self, capacity: 1) { + $0.withMemoryRebound(to: thread_act_array_t?.self, capacity: 1) { task_threads(mach_task_self_, $0, &threadsCount) } } - if threadsResult == KERN_SUCCESS, let threadsList = threadsList { - for index in 0.. Double { } } - vm_deallocate(mach_task_self_, vm_address_t(UInt(bitPattern: threadsList)), vm_size_t(Int(threadsCount) * MemoryLayout.stride)) + vm_deallocate( + mach_task_self_, + vm_address_t(UInt(bitPattern: threadsList)), + vm_size_t(Int(threadsCount) * MemoryLayout.stride) + ) return totalUsageOfCPU } diff --git a/WebSocketDemo-Shared/getMemoryUsage.swift b/WebSocketDemo-Shared/getMemoryUsage.swift index 0521dc3..8ba0df9 100644 --- a/WebSocketDemo-Shared/getMemoryUsage.swift +++ b/WebSocketDemo-Shared/getMemoryUsage.swift @@ -1,6 +1,7 @@ -import Foundation import Darwin.Mach.task_info +import Foundation +// swiftlint:disable:next line_length // https://github.com/dani-gavrilov/GDPerformanceView-Swift/blob/171a656040135d667f4228c3ec82f2384770d87d/GDPerformanceView-Swift/GDPerformanceMonitoring/PerformanceСalculator.swift#L129 func getMemoryUsage() -> (used: UInt64, total: UInt64) { var taskInfo = task_vm_info_data_t() diff --git a/WebSocketDemo-Shared/schemas.swift b/WebSocketDemo-Shared/schemas.swift index 3b1195a..b997e48 100644 --- a/WebSocketDemo-Shared/schemas.swift +++ b/WebSocketDemo-Shared/schemas.swift @@ -1,3 +1,6 @@ +// swiftlint:disable:next blanket_disable_command +// swiftlint:disable line_length + let poseInFrameSchema = """ { "title": "foxglove.PoseInFrame", diff --git a/WebSocketDemo-Watch/ContentView.swift b/WebSocketDemo-Watch/ContentView.swift index b83e96a..fffaef5 100644 --- a/WebSocketDemo-Watch/ContentView.swift +++ b/WebSocketDemo-Watch/ContentView.swift @@ -1,6 +1,6 @@ +import HealthKit import SwiftUI import WatchConnectivity -import HealthKit @MainActor class SessionManager: NSObject, ObservableObject, WCSessionDelegate { @@ -34,7 +34,11 @@ class SessionManager: NSObject, ObservableObject, WCSessionDelegate { } } - nonisolated func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { + nonisolated func session( + _: WCSession, + activationDidCompleteWith activationState: WCSessionActivationState, + error: Error? + ) { guard activationState == .activated else { print("activation failed \(error)") return @@ -61,21 +65,26 @@ class SessionManager: NSObject, ObservableObject, WCSessionDelegate { session.pause() workoutSession = session print("started session") - } catch let error { + } catch { print("error creating workout: \(error)") } let type = HKQuantityType(.heartRate) - let query = HKAnchoredObjectQuery(type: type, predicate: HKQuery.predicateForSamples(withStart: .now, end: nil), anchor: nil, limit: HKObjectQueryNoLimit) { query, samples, deleted, anchor, error in + let query = HKAnchoredObjectQuery( + type: type, + predicate: HKQuery.predicateForSamples(withStart: .now, end: nil), + anchor: nil, + limit: HKObjectQueryNoLimit + ) { _, samples, deleted, anchor, error in print("results: \(samples), deleted \(deleted), anchor \(anchor), error \(error)") } - query.updateHandler = { [weak self] query, samples, deleted, anchor, error in + query.updateHandler = { [weak self] _, samples, deleted, anchor, error in guard let self else { return } print("update: \(samples), deleted \(deleted), anchor \(anchor), error \(error)") guard let samples else { return } for case let sample as HKQuantitySample in samples { let bpm = sample.quantity.doubleValue(for: HKUnit(from: "count/min")) - self.session.sendMessage(["heart_rate": bpm], replyHandler: nil) + session.sendMessage(["heart_rate": bpm], replyHandler: nil) Task { @MainActor in self.currentHeartRate = bpm }