ironOS native ios app
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: inital prototype

+1239
ios/.DS_Store

This is a binary file and will not be displayed.

+343
ios/PinecilTime.xcodeproj/project.pbxproj
··· 1 + // !$*UTF8*$! 2 + { 3 + archiveVersion = 1; 4 + classes = { 5 + }; 6 + objectVersion = 77; 7 + objects = { 8 + 9 + /* Begin PBXFileReference section */ 10 + 0B29219B2F3460FA009E14CB /* Pinecil Time.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Pinecil Time.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 11 + /* End PBXFileReference section */ 12 + 13 + /* Begin PBXFileSystemSynchronizedRootGroup section */ 14 + 0B29219D2F3460FA009E14CB /* PinecilTime */ = { 15 + isa = PBXFileSystemSynchronizedRootGroup; 16 + path = PinecilTime; 17 + sourceTree = "<group>"; 18 + }; 19 + /* End PBXFileSystemSynchronizedRootGroup section */ 20 + 21 + /* Begin PBXFrameworksBuildPhase section */ 22 + 0B2921982F3460FA009E14CB /* Frameworks */ = { 23 + isa = PBXFrameworksBuildPhase; 24 + buildActionMask = 2147483647; 25 + files = ( 26 + ); 27 + runOnlyForDeploymentPostprocessing = 0; 28 + }; 29 + /* End PBXFrameworksBuildPhase section */ 30 + 31 + /* Begin PBXGroup section */ 32 + 0B2921922F3460FA009E14CB = { 33 + isa = PBXGroup; 34 + children = ( 35 + 0B29219D2F3460FA009E14CB /* PinecilTime */, 36 + 0B29219C2F3460FA009E14CB /* Products */, 37 + ); 38 + sourceTree = "<group>"; 39 + }; 40 + 0B29219C2F3460FA009E14CB /* Products */ = { 41 + isa = PBXGroup; 42 + children = ( 43 + 0B29219B2F3460FA009E14CB /* Pinecil Time.app */, 44 + ); 45 + name = Products; 46 + sourceTree = "<group>"; 47 + }; 48 + /* End PBXGroup section */ 49 + 50 + /* Begin PBXNativeTarget section */ 51 + 0B29219A2F3460FA009E14CB /* Pinecil Time */ = { 52 + isa = PBXNativeTarget; 53 + buildConfigurationList = 0B2921A62F3460FB009E14CB /* Build configuration list for PBXNativeTarget "Pinecil Time" */; 54 + buildPhases = ( 55 + 0B2921972F3460FA009E14CB /* Sources */, 56 + 0B2921982F3460FA009E14CB /* Frameworks */, 57 + 0B2921992F3460FA009E14CB /* Resources */, 58 + ); 59 + buildRules = ( 60 + ); 61 + dependencies = ( 62 + ); 63 + fileSystemSynchronizedGroups = ( 64 + 0B29219D2F3460FA009E14CB /* PinecilTime */, 65 + ); 66 + name = "Pinecil Time"; 67 + packageProductDependencies = ( 68 + ); 69 + productName = "Pinecil Time"; 70 + productReference = 0B29219B2F3460FA009E14CB /* Pinecil Time.app */; 71 + productType = "com.apple.product-type.application"; 72 + }; 73 + /* End PBXNativeTarget section */ 74 + 75 + /* Begin PBXProject section */ 76 + 0B2921932F3460FA009E14CB /* Project object */ = { 77 + isa = PBXProject; 78 + attributes = { 79 + BuildIndependentTargetsInParallel = 1; 80 + LastSwiftUpdateCheck = 2620; 81 + LastUpgradeCheck = 2620; 82 + TargetAttributes = { 83 + 0B29219A2F3460FA009E14CB = { 84 + CreatedOnToolsVersion = 26.2; 85 + }; 86 + }; 87 + }; 88 + buildConfigurationList = 0B2921962F3460FA009E14CB /* Build configuration list for PBXProject "PinecilTime" */; 89 + developmentRegion = en; 90 + hasScannedForEncodings = 0; 91 + knownRegions = ( 92 + en, 93 + Base, 94 + ); 95 + mainGroup = 0B2921922F3460FA009E14CB; 96 + minimizedProjectReferenceProxies = 1; 97 + preferredProjectObjectVersion = 77; 98 + productRefGroup = 0B29219C2F3460FA009E14CB /* Products */; 99 + projectDirPath = ""; 100 + projectRoot = ""; 101 + targets = ( 102 + 0B29219A2F3460FA009E14CB /* Pinecil Time */, 103 + ); 104 + }; 105 + /* End PBXProject section */ 106 + 107 + /* Begin PBXResourcesBuildPhase section */ 108 + 0B2921992F3460FA009E14CB /* Resources */ = { 109 + isa = PBXResourcesBuildPhase; 110 + buildActionMask = 2147483647; 111 + files = ( 112 + ); 113 + runOnlyForDeploymentPostprocessing = 0; 114 + }; 115 + /* End PBXResourcesBuildPhase section */ 116 + 117 + /* Begin PBXSourcesBuildPhase section */ 118 + 0B2921972F3460FA009E14CB /* Sources */ = { 119 + isa = PBXSourcesBuildPhase; 120 + buildActionMask = 2147483647; 121 + files = ( 122 + ); 123 + runOnlyForDeploymentPostprocessing = 0; 124 + }; 125 + /* End PBXSourcesBuildPhase section */ 126 + 127 + /* Begin XCBuildConfiguration section */ 128 + 0B2921A42F3460FB009E14CB /* Debug */ = { 129 + isa = XCBuildConfiguration; 130 + buildSettings = { 131 + ALWAYS_SEARCH_USER_PATHS = NO; 132 + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 133 + CLANG_ANALYZER_NONNULL = YES; 134 + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 135 + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 136 + CLANG_ENABLE_MODULES = YES; 137 + CLANG_ENABLE_OBJC_ARC = YES; 138 + CLANG_ENABLE_OBJC_WEAK = YES; 139 + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 140 + CLANG_WARN_BOOL_CONVERSION = YES; 141 + CLANG_WARN_COMMA = YES; 142 + CLANG_WARN_CONSTANT_CONVERSION = YES; 143 + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 144 + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 145 + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 146 + CLANG_WARN_EMPTY_BODY = YES; 147 + CLANG_WARN_ENUM_CONVERSION = YES; 148 + CLANG_WARN_INFINITE_RECURSION = YES; 149 + CLANG_WARN_INT_CONVERSION = YES; 150 + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 151 + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 152 + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 153 + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 154 + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 155 + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 156 + CLANG_WARN_STRICT_PROTOTYPES = YES; 157 + CLANG_WARN_SUSPICIOUS_MOVE = YES; 158 + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 159 + CLANG_WARN_UNREACHABLE_CODE = YES; 160 + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 161 + COPY_PHASE_STRIP = NO; 162 + DEBUG_INFORMATION_FORMAT = dwarf; 163 + DEVELOPMENT_TEAM = M67B42LX8D; 164 + ENABLE_STRICT_OBJC_MSGSEND = YES; 165 + ENABLE_TESTABILITY = YES; 166 + ENABLE_USER_SCRIPT_SANDBOXING = YES; 167 + GCC_C_LANGUAGE_STANDARD = gnu17; 168 + GCC_DYNAMIC_NO_PIC = NO; 169 + GCC_NO_COMMON_BLOCKS = YES; 170 + GCC_OPTIMIZATION_LEVEL = 0; 171 + GCC_PREPROCESSOR_DEFINITIONS = ( 172 + "DEBUG=1", 173 + "$(inherited)", 174 + ); 175 + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 176 + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 177 + GCC_WARN_UNDECLARED_SELECTOR = YES; 178 + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 179 + GCC_WARN_UNUSED_FUNCTION = YES; 180 + GCC_WARN_UNUSED_VARIABLE = YES; 181 + IPHONEOS_DEPLOYMENT_TARGET = 26.2; 182 + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 183 + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 184 + MTL_FAST_MATH = YES; 185 + ONLY_ACTIVE_ARCH = YES; 186 + SDKROOT = iphoneos; 187 + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 188 + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 189 + }; 190 + name = Debug; 191 + }; 192 + 0B2921A52F3460FB009E14CB /* Release */ = { 193 + isa = XCBuildConfiguration; 194 + buildSettings = { 195 + ALWAYS_SEARCH_USER_PATHS = NO; 196 + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 197 + CLANG_ANALYZER_NONNULL = YES; 198 + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 199 + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 200 + CLANG_ENABLE_MODULES = YES; 201 + CLANG_ENABLE_OBJC_ARC = YES; 202 + CLANG_ENABLE_OBJC_WEAK = YES; 203 + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 204 + CLANG_WARN_BOOL_CONVERSION = YES; 205 + CLANG_WARN_COMMA = YES; 206 + CLANG_WARN_CONSTANT_CONVERSION = YES; 207 + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 208 + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 209 + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 210 + CLANG_WARN_EMPTY_BODY = YES; 211 + CLANG_WARN_ENUM_CONVERSION = YES; 212 + CLANG_WARN_INFINITE_RECURSION = YES; 213 + CLANG_WARN_INT_CONVERSION = YES; 214 + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 215 + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 216 + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 217 + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 218 + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 219 + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 220 + CLANG_WARN_STRICT_PROTOTYPES = YES; 221 + CLANG_WARN_SUSPICIOUS_MOVE = YES; 222 + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 223 + CLANG_WARN_UNREACHABLE_CODE = YES; 224 + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 225 + COPY_PHASE_STRIP = NO; 226 + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 227 + DEVELOPMENT_TEAM = M67B42LX8D; 228 + ENABLE_NS_ASSERTIONS = NO; 229 + ENABLE_STRICT_OBJC_MSGSEND = YES; 230 + ENABLE_USER_SCRIPT_SANDBOXING = YES; 231 + GCC_C_LANGUAGE_STANDARD = gnu17; 232 + GCC_NO_COMMON_BLOCKS = YES; 233 + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 234 + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 235 + GCC_WARN_UNDECLARED_SELECTOR = YES; 236 + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 237 + GCC_WARN_UNUSED_FUNCTION = YES; 238 + GCC_WARN_UNUSED_VARIABLE = YES; 239 + IPHONEOS_DEPLOYMENT_TARGET = 26.2; 240 + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 241 + MTL_ENABLE_DEBUG_INFO = NO; 242 + MTL_FAST_MATH = YES; 243 + SDKROOT = iphoneos; 244 + SWIFT_COMPILATION_MODE = wholemodule; 245 + VALIDATE_PRODUCT = YES; 246 + }; 247 + name = Release; 248 + }; 249 + 0B2921A72F3460FB009E14CB /* Debug */ = { 250 + isa = XCBuildConfiguration; 251 + buildSettings = { 252 + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 253 + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 254 + CODE_SIGN_STYLE = Automatic; 255 + CURRENT_PROJECT_VERSION = 1; 256 + DEVELOPMENT_TEAM = M67B42LX8D; 257 + ENABLE_PREVIEWS = YES; 258 + GENERATE_INFOPLIST_FILE = YES; 259 + INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "PinecilTime needs Bluetooth to connect to your Pinecil soldering iron."; 260 + INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "PinecilTime needs Bluetooth to connect to your Pinecil soldering iron."; 261 + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 262 + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 263 + INFOPLIST_KEY_UIBackgroundModes = "bluetooth-central"; 264 + INFOPLIST_KEY_UILaunchScreen_Generation = YES; 265 + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 266 + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 267 + LD_RUNPATH_SEARCH_PATHS = ( 268 + "$(inherited)", 269 + "@executable_path/Frameworks", 270 + ); 271 + MARKETING_VERSION = 1.0; 272 + PRODUCT_BUNDLE_IDENTIFIER = "sh.dunkirk.sh.Pinecil-Time"; 273 + PRODUCT_NAME = "$(TARGET_NAME)"; 274 + STRING_CATALOG_GENERATE_SYMBOLS = YES; 275 + SWIFT_APPROACHABLE_CONCURRENCY = YES; 276 + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; 277 + SWIFT_EMIT_LOC_STRINGS = YES; 278 + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; 279 + SWIFT_VERSION = 5.0; 280 + TARGETED_DEVICE_FAMILY = "1,2"; 281 + }; 282 + name = Debug; 283 + }; 284 + 0B2921A82F3460FB009E14CB /* Release */ = { 285 + isa = XCBuildConfiguration; 286 + buildSettings = { 287 + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 288 + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 289 + CODE_SIGN_STYLE = Automatic; 290 + CURRENT_PROJECT_VERSION = 1; 291 + DEVELOPMENT_TEAM = M67B42LX8D; 292 + ENABLE_PREVIEWS = YES; 293 + GENERATE_INFOPLIST_FILE = YES; 294 + INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "PinecilTime needs Bluetooth to connect to your Pinecil soldering iron."; 295 + INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "PinecilTime needs Bluetooth to connect to your Pinecil soldering iron."; 296 + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 297 + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 298 + INFOPLIST_KEY_UIBackgroundModes = "bluetooth-central"; 299 + INFOPLIST_KEY_UILaunchScreen_Generation = YES; 300 + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 301 + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 302 + LD_RUNPATH_SEARCH_PATHS = ( 303 + "$(inherited)", 304 + "@executable_path/Frameworks", 305 + ); 306 + MARKETING_VERSION = 1.0; 307 + PRODUCT_BUNDLE_IDENTIFIER = "sh.dunkirk.sh.Pinecil-Time"; 308 + PRODUCT_NAME = "$(TARGET_NAME)"; 309 + STRING_CATALOG_GENERATE_SYMBOLS = YES; 310 + SWIFT_APPROACHABLE_CONCURRENCY = YES; 311 + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; 312 + SWIFT_EMIT_LOC_STRINGS = YES; 313 + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; 314 + SWIFT_VERSION = 5.0; 315 + TARGETED_DEVICE_FAMILY = "1,2"; 316 + }; 317 + name = Release; 318 + }; 319 + /* End XCBuildConfiguration section */ 320 + 321 + /* Begin XCConfigurationList section */ 322 + 0B2921962F3460FA009E14CB /* Build configuration list for PBXProject "PinecilTime" */ = { 323 + isa = XCConfigurationList; 324 + buildConfigurations = ( 325 + 0B2921A42F3460FB009E14CB /* Debug */, 326 + 0B2921A52F3460FB009E14CB /* Release */, 327 + ); 328 + defaultConfigurationIsVisible = 0; 329 + defaultConfigurationName = Release; 330 + }; 331 + 0B2921A62F3460FB009E14CB /* Build configuration list for PBXNativeTarget "Pinecil Time" */ = { 332 + isa = XCConfigurationList; 333 + buildConfigurations = ( 334 + 0B2921A72F3460FB009E14CB /* Debug */, 335 + 0B2921A82F3460FB009E14CB /* Release */, 336 + ); 337 + defaultConfigurationIsVisible = 0; 338 + defaultConfigurationName = Release; 339 + }; 340 + /* End XCConfigurationList section */ 341 + }; 342 + rootObject = 0B2921932F3460FA009E14CB /* Project object */; 343 + }
+7
ios/PinecilTime.xcodeproj/project.xcworkspace/contents.xcworkspacedata
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <Workspace 3 + version = "1.0"> 4 + <FileRef 5 + location = "self:"> 6 + </FileRef> 7 + </Workspace>
ios/PinecilTime.xcodeproj/project.xcworkspace/xcuserdata/kierank.xcuserdatad/UserInterfaceState.xcuserstate

This is a binary file and will not be displayed.

+14
ios/PinecilTime.xcodeproj/xcuserdata/kierank.xcuserdatad/xcschemes/xcschememanagement.plist
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 3 + <plist version="1.0"> 4 + <dict> 5 + <key>SchemeUserState</key> 6 + <dict> 7 + <key>Pinecil Time.xcscheme_^#shared#^_</key> 8 + <dict> 9 + <key>orderHint</key> 10 + <integer>0</integer> 11 + </dict> 12 + </dict> 13 + </dict> 14 + </plist>
+11
ios/PinecilTime/Assets.xcassets/AccentColor.colorset/Contents.json
··· 1 + { 2 + "colors" : [ 3 + { 4 + "idiom" : "universal" 5 + } 6 + ], 7 + "info" : { 8 + "author" : "xcode", 9 + "version" : 1 10 + } 11 + }
+35
ios/PinecilTime/Assets.xcassets/AppIcon.appiconset/Contents.json
··· 1 + { 2 + "images" : [ 3 + { 4 + "idiom" : "universal", 5 + "platform" : "ios", 6 + "size" : "1024x1024" 7 + }, 8 + { 9 + "appearances" : [ 10 + { 11 + "appearance" : "luminosity", 12 + "value" : "dark" 13 + } 14 + ], 15 + "idiom" : "universal", 16 + "platform" : "ios", 17 + "size" : "1024x1024" 18 + }, 19 + { 20 + "appearances" : [ 21 + { 22 + "appearance" : "luminosity", 23 + "value" : "tinted" 24 + } 25 + ], 26 + "idiom" : "universal", 27 + "platform" : "ios", 28 + "size" : "1024x1024" 29 + } 30 + ], 31 + "info" : { 32 + "author" : "xcode", 33 + "version" : 1 34 + } 35 + }
+6
ios/PinecilTime/Assets.xcassets/Contents.json
··· 1 + { 2 + "info" : { 3 + "author" : "xcode", 4 + "version" : 1 5 + } 6 + }
+301
ios/PinecilTime/BLEManager.swift
··· 1 + // 2 + // BLEManager.swift 3 + // PinecilTime 4 + // 5 + 6 + import CoreBluetooth 7 + import Foundation 8 + 9 + @Observable 10 + class BLEManager: NSObject { 11 + 12 + // MARK: - State 13 + 14 + var isScanning = false 15 + var discoveredDevices: [CBPeripheral] = [] 16 + var connectedPeripheral: CBPeripheral? 17 + var connectionState: ConnectionState = .disconnected 18 + var liveData = IronOSLiveData() 19 + var deviceName: String = "" 20 + var firmwareVersion: String = "" 21 + 22 + // Temperature history for graph 23 + var temperatureHistory: [TemperaturePoint] = [] 24 + private let maxHistoryPoints = 60 25 + 26 + // MARK: - Private 27 + 28 + private var centralManager: CBCentralManager! 29 + private var discoveredCharacteristics: [CBUUID: CBCharacteristic] = [:] 30 + private var pollTimer: Timer? 31 + 32 + // MARK: - Init 33 + 34 + override init() { 35 + super.init() 36 + centralManager = CBCentralManager(delegate: self, queue: .main) 37 + } 38 + 39 + // MARK: - Connection State 40 + 41 + enum ConnectionState: Equatable { 42 + case disconnected 43 + case scanning 44 + case connecting 45 + case connected 46 + case error(String) 47 + 48 + var isConnected: Bool { 49 + self == .connected 50 + } 51 + } 52 + 53 + // MARK: - Public Methods 54 + 55 + func startScanning() { 56 + guard centralManager.state == .poweredOn else { return } 57 + 58 + discoveredDevices.removeAll() 59 + connectionState = .scanning 60 + 61 + centralManager.scanForPeripherals( 62 + withServices: [IronOSUUIDs.bulkDataService], 63 + options: [CBCentralManagerScanOptionAllowDuplicatesKey: false] 64 + ) 65 + 66 + isScanning = true 67 + } 68 + 69 + func stopScanning() { 70 + centralManager.stopScan() 71 + isScanning = false 72 + if connectionState == .scanning { 73 + connectionState = .disconnected 74 + } 75 + } 76 + 77 + func connect(to peripheral: CBPeripheral) { 78 + stopScanning() 79 + connectionState = .connecting 80 + connectedPeripheral = peripheral 81 + peripheral.delegate = self 82 + centralManager.connect(peripheral, options: nil) 83 + } 84 + 85 + func disconnect() { 86 + stopPolling() 87 + 88 + if let peripheral = connectedPeripheral { 89 + centralManager.cancelPeripheralConnection(peripheral) 90 + } 91 + 92 + connectionState = .disconnected 93 + connectedPeripheral = nil 94 + discoveredCharacteristics.removeAll() 95 + temperatureHistory.removeAll() 96 + } 97 + 98 + func setTemperature(_ temp: UInt32) { 99 + guard connectionState == .connected, 100 + let peripheral = connectedPeripheral, 101 + let characteristic = discoveredCharacteristics[IronOSUUIDs.setpointSetting] else { 102 + return 103 + } 104 + 105 + let value = UInt16(temp).data 106 + peripheral.writeValue(value, for: characteristic, type: .withResponse) 107 + } 108 + 109 + // MARK: - Polling 110 + 111 + private func startPolling() { 112 + stopPolling() 113 + 114 + pollTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in 115 + self?.readBulkData() 116 + } 117 + 118 + readBulkData() 119 + } 120 + 121 + private func stopPolling() { 122 + pollTimer?.invalidate() 123 + pollTimer = nil 124 + } 125 + 126 + private func readBulkData() { 127 + guard let characteristic = discoveredCharacteristics[IronOSUUIDs.bulkLiveData], 128 + let peripheral = connectedPeripheral else { return } 129 + 130 + peripheral.readValue(for: characteristic) 131 + } 132 + 133 + private func recordTemperature() { 134 + let point = TemperaturePoint( 135 + timestamp: Date(), 136 + actualTemp: liveData.liveTemp, 137 + setpoint: liveData.setpoint 138 + ) 139 + 140 + temperatureHistory.append(point) 141 + 142 + // Keep only last N points 143 + if temperatureHistory.count > maxHistoryPoints { 144 + temperatureHistory.removeFirst() 145 + } 146 + } 147 + 148 + private func handleCharacteristicValue(_ characteristic: CBCharacteristic) { 149 + guard let value = characteristic.value else { return } 150 + 151 + switch characteristic.uuid { 152 + case IronOSUUIDs.bulkLiveData: 153 + liveData.updateFromBulkData(value) 154 + recordTemperature() 155 + 156 + case IronOSUUIDs.liveTemp: 157 + liveData.liveTemp = value.toUInt32() ?? 0 158 + case IronOSUUIDs.setpointRead: 159 + liveData.setpoint = value.toUInt32() ?? 0 160 + case IronOSUUIDs.dcInput: 161 + liveData.dcInput = value.toUInt32() ?? 0 162 + case IronOSUUIDs.handleTemp: 163 + liveData.handleTemp = value.toUInt32() ?? 0 164 + case IronOSUUIDs.powerLevel: 165 + liveData.powerLevel = value.toUInt32() ?? 0 166 + case IronOSUUIDs.powerSource: 167 + liveData.powerSource = value.toUInt32() ?? 0 168 + case IronOSUUIDs.operatingMode: 169 + liveData.operatingMode = value.toUInt32() ?? 0 170 + case IronOSUUIDs.estimatedWatts: 171 + liveData.estimatedWatts = value.toUInt32() ?? 0 172 + case IronOSUUIDs.maxTemp: 173 + liveData.maxTemp = value.toUInt32() ?? 450 174 + 175 + case IronOSUUIDs.firmwareVersion: 176 + firmwareVersion = value.toString() ?? "" 177 + 178 + default: 179 + break 180 + } 181 + } 182 + } 183 + 184 + // MARK: - CBCentralManagerDelegate 185 + 186 + extension BLEManager: CBCentralManagerDelegate { 187 + 188 + func centralManagerDidUpdateState(_ central: CBCentralManager) { 189 + switch central.state { 190 + case .poweredOn: 191 + startScanning() 192 + case .poweredOff: 193 + connectionState = .error("Bluetooth is off") 194 + case .unauthorized: 195 + connectionState = .error("Bluetooth access denied") 196 + case .unsupported: 197 + connectionState = .error("Bluetooth not supported") 198 + default: 199 + break 200 + } 201 + } 202 + 203 + func centralManager(_ central: CBCentralManager, 204 + didDiscover peripheral: CBPeripheral, 205 + advertisementData: [String: Any], 206 + rssi RSSI: NSNumber) { 207 + // Auto-connect to first discovered Pinecil 208 + if connectedPeripheral == nil { 209 + if peripheral.name?.hasPrefix("PrattlePin-") == true || 210 + advertisementData[CBAdvertisementDataServiceUUIDsKey] != nil { 211 + connect(to: peripheral) 212 + return 213 + } 214 + } 215 + 216 + if !discoveredDevices.contains(where: { $0.identifier == peripheral.identifier }) { 217 + discoveredDevices.append(peripheral) 218 + } 219 + } 220 + 221 + func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { 222 + connectionState = .connected 223 + deviceName = peripheral.name ?? "Pinecil" 224 + peripheral.discoverServices(nil) 225 + } 226 + 227 + func centralManager(_ central: CBCentralManager, 228 + didFailToConnect peripheral: CBPeripheral, 229 + error: Error?) { 230 + connectionState = .error(error?.localizedDescription ?? "Connection failed") 231 + connectedPeripheral = nil 232 + 233 + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in 234 + self?.startScanning() 235 + } 236 + } 237 + 238 + func centralManager(_ central: CBCentralManager, 239 + didDisconnectPeripheral peripheral: CBPeripheral, 240 + error: Error?) { 241 + stopPolling() 242 + connectionState = .disconnected 243 + connectedPeripheral = nil 244 + discoveredCharacteristics.removeAll() 245 + 246 + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in 247 + self?.startScanning() 248 + } 249 + } 250 + } 251 + 252 + // MARK: - CBPeripheralDelegate 253 + 254 + extension BLEManager: CBPeripheralDelegate { 255 + 256 + func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { 257 + guard let services = peripheral.services else { return } 258 + 259 + for service in services { 260 + peripheral.discoverCharacteristics(nil, for: service) 261 + } 262 + } 263 + 264 + func peripheral(_ peripheral: CBPeripheral, 265 + didDiscoverCharacteristicsFor service: CBService, 266 + error: Error?) { 267 + guard let characteristics = service.characteristics else { return } 268 + 269 + for characteristic in characteristics { 270 + discoveredCharacteristics[characteristic.uuid] = characteristic 271 + 272 + // Read bulk data and firmware version on connect 273 + if characteristic.uuid == IronOSUUIDs.bulkLiveData || 274 + characteristic.uuid == IronOSUUIDs.firmwareVersion { 275 + peripheral.readValue(for: characteristic) 276 + } 277 + 278 + // Enable notifications for operating mode 279 + if characteristic.uuid == IronOSUUIDs.operatingMode { 280 + peripheral.setNotifyValue(true, for: characteristic) 281 + } 282 + } 283 + 284 + // Start polling once we have the live data service 285 + if service.uuid == IronOSUUIDs.liveDataService || service.uuid == IronOSUUIDs.bulkDataService { 286 + startPolling() 287 + } 288 + } 289 + 290 + func peripheral(_ peripheral: CBPeripheral, 291 + didUpdateValueFor characteristic: CBCharacteristic, 292 + error: Error?) { 293 + if error != nil { return } 294 + handleCharacteristicValue(characteristic) 295 + } 296 + 297 + func peripheral(_ peripheral: CBPeripheral, 298 + didWriteValueFor characteristic: CBCharacteristic, 299 + error: Error?) { 300 + } 301 + }
+240
ios/PinecilTime/ContentView.swift
··· 1 + // 2 + // ContentView.swift 3 + // PinecilTime 4 + // 5 + 6 + import SwiftUI 7 + 8 + struct ContentView: View { 9 + @State private var bleManager = BLEManager() 10 + @State private var targetTemp: Double = 300 11 + @State private var isEditingSlider = false 12 + @State private var lastSentTemp: Double = 0 13 + @State private var lastSendTime: Date = .distantPast 14 + 15 + private var isHeating: Bool { 16 + bleManager.liveData.mode?.isActive ?? false 17 + } 18 + 19 + var body: some View { 20 + ZStack { 21 + // Background graph 22 + if !bleManager.temperatureHistory.isEmpty { 23 + TemperatureGraph( 24 + history: bleManager.temperatureHistory, 25 + maxTemp: bleManager.liveData.maxTemp 26 + ) 27 + .padding(.horizontal, 20) 28 + .padding(.vertical, 120) 29 + } 30 + 31 + // Main content 32 + if bleManager.connectionState.isConnected { 33 + connectedView 34 + } else { 35 + scanningView 36 + } 37 + } 38 + .background(Color(.systemBackground)) 39 + .onChange(of: bleManager.liveData.setpoint) { _, newValue in 40 + if !isEditingSlider && newValue > 0 { 41 + targetTemp = Double(newValue) 42 + } 43 + } 44 + } 45 + 46 + // MARK: - Connected View 47 + 48 + private var connectedView: some View { 49 + VStack(spacing: 0) { 50 + // Top bar with stats 51 + topBar 52 + .padding(.top, 8) 53 + 54 + Spacer() 55 + 56 + // Big temperature number 57 + temperatureDisplay 58 + 59 + // Target indicator 60 + if isHeating { 61 + HStack(spacing: 4) { 62 + Image(systemName: "arrow.right") 63 + .font(.caption) 64 + Text("\(bleManager.liveData.setpoint)°") 65 + .font(.title3.monospacedDigit()) 66 + } 67 + .foregroundStyle(.secondary) 68 + .padding(.top, 8) 69 + } 70 + 71 + Spacer() 72 + 73 + // Bottom slider panel 74 + sliderPanel 75 + .padding(.horizontal, 20) 76 + .padding(.bottom, 16) 77 + } 78 + } 79 + 80 + // MARK: - Top Bar 81 + 82 + private var topBar: some View { 83 + HStack(spacing: 16) { 84 + // Device name 85 + Text(bleManager.deviceName) 86 + .font(.subheadline.bold()) 87 + 88 + Spacer() 89 + 90 + // Stats 91 + HStack(spacing: 12) { 92 + statItem(value: String(format: "%.1f", bleManager.liveData.watts), unit: "W") 93 + statItem(value: String(format: "%.1f", bleManager.liveData.voltage), unit: "V") 94 + statItem(value: "\(bleManager.liveData.powerPercent)", unit: "%") 95 + } 96 + 97 + // Mode indicator 98 + if let mode = bleManager.liveData.mode { 99 + Image(systemName: mode.icon) 100 + .foregroundStyle(mode.isActive ? .orange : .secondary) 101 + } 102 + } 103 + .padding(.horizontal, 20) 104 + .padding(.vertical, 12) 105 + .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16)) 106 + .padding(.horizontal, 16) 107 + } 108 + 109 + // MARK: - Temperature Display 110 + 111 + private var temperatureDisplay: some View { 112 + let currentTemp = Double(bleManager.liveData.liveTemp) 113 + let maxTemp = Double(bleManager.liveData.maxTemp) 114 + 115 + return HStack(alignment: .firstTextBaseline, spacing: 0) { 116 + Text("\(bleManager.liveData.liveTemp)") 117 + .font(.system(size: 120, weight: .thin, design: .rounded)) 118 + .contentTransition(.numericText()) 119 + Text("°") 120 + .font(.system(size: 60, weight: .ultraLight)) 121 + .foregroundStyle(.secondary) 122 + } 123 + .foregroundStyle(colorForTemp(currentTemp, maxTemp: maxTemp)) 124 + } 125 + 126 + // MARK: - Slider Panel 127 + 128 + private var sliderPanel: some View { 129 + VStack(spacing: 12) { 130 + HStack { 131 + Text("Target") 132 + .font(.subheadline) 133 + .foregroundStyle(.secondary) 134 + Spacer() 135 + Text("\(Int(targetTemp))°C") 136 + .font(.subheadline.monospacedDigit().bold()) 137 + .foregroundStyle(colorForTemp(targetTemp, maxTemp: 450)) 138 + } 139 + 140 + Slider( 141 + value: $targetTemp, 142 + in: 10...450, 143 + step: 5, 144 + onEditingChanged: { editing in 145 + isEditingSlider = editing 146 + if !editing { 147 + bleManager.setTemperature(UInt32(targetTemp)) 148 + lastSentTemp = targetTemp 149 + } 150 + } 151 + ) 152 + .tint(colorForTemp(targetTemp, maxTemp: 450)) 153 + .onChange(of: targetTemp) { _, newValue in 154 + guard isEditingSlider else { return } 155 + let now = Date() 156 + if now.timeIntervalSince(lastSendTime) > 0.15 && abs(newValue - lastSentTemp) >= 5 { 157 + bleManager.setTemperature(UInt32(newValue)) 158 + lastSentTemp = newValue 159 + lastSendTime = now 160 + } 161 + } 162 + } 163 + .padding(.horizontal, 20) 164 + .padding(.vertical, 16) 165 + .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 20)) 166 + } 167 + 168 + // MARK: - Helpers 169 + 170 + private func statItem(value: String, unit: String) -> some View { 171 + HStack(alignment: .firstTextBaseline, spacing: 1) { 172 + Text(value) 173 + .font(.caption.monospacedDigit().bold()) 174 + Text(unit) 175 + .font(.caption2) 176 + .foregroundStyle(.secondary) 177 + } 178 + } 179 + 180 + private func colorForTemp(_ temp: Double, maxTemp: Double) -> Color { 181 + let progress = Swift.min(Swift.max(temp / maxTemp, 0), 1) 182 + 183 + if progress < 0.33 { 184 + let t = progress / 0.33 185 + return Color( 186 + red: 0 + t * 0, 187 + green: 0.5 + t * 0.5, 188 + blue: 1 - t * 0 189 + ) 190 + } else if progress < 0.66 { 191 + let t = (progress - 0.33) / 0.33 192 + return Color( 193 + red: 0 + t * 1, 194 + green: 1 - t * 0.35, 195 + blue: 1 - t * 1 196 + ) 197 + } else { 198 + let t = (progress - 0.66) / 0.34 199 + return Color( 200 + red: 1, 201 + green: 0.65 - t * 0.65, 202 + blue: 0 203 + ) 204 + } 205 + } 206 + 207 + // MARK: - Scanning View 208 + 209 + private var scanningView: some View { 210 + VStack(spacing: 24) { 211 + Spacer() 212 + 213 + if bleManager.isScanning || bleManager.connectionState == .connecting { 214 + ProgressView() 215 + .scaleEffect(1.5) 216 + 217 + Text(bleManager.connectionState == .connecting ? "Connecting..." : "Scanning...") 218 + .font(.headline) 219 + 220 + Text("Looking for your Pinecil") 221 + .font(.subheadline) 222 + .foregroundStyle(.secondary) 223 + } else { 224 + Image(systemName: "antenna.radiowaves.left.and.right.slash") 225 + .font(.system(size: 48)) 226 + .foregroundStyle(.secondary) 227 + 228 + Text("No Device Found") 229 + .font(.headline) 230 + 231 + Button("Scan Again") { 232 + bleManager.startScanning() 233 + } 234 + .buttonStyle(.borderedProminent) 235 + } 236 + 237 + Spacer() 238 + } 239 + } 240 + }
+40
ios/PinecilTime/IronOSUUIDs.swift
··· 1 + // 2 + // IronOSUUIDs.swift 3 + // PinecilTime 4 + // 5 + 6 + import CoreBluetooth 7 + import Foundation 8 + 9 + enum IronOSUUIDs { 10 + 11 + // MARK: - Bulk Data Service (for discovery) 12 + static let bulkDataService = CBUUID(string: "9EAE1000-9D0D-48C5-AA55-33E27F9BC533") 13 + static let bulkLiveData = CBUUID(string: "9EAE1001-9D0D-48C5-AA55-33E27F9BC533") 14 + static let firmwareVersion = CBUUID(string: "9EAE1003-9D0D-48C5-AA55-33E27F9BC533") 15 + static let deviceSerial = CBUUID(string: "9EAE1004-9D0D-48C5-AA55-33E27F9BC533") 16 + 17 + // MARK: - Live Data Service 18 + static let liveDataService = CBUUID(string: "D85EF000-168E-4A71-AA55-33E27F9BC533") 19 + static let liveTemp = CBUUID(string: "D85EF001-168E-4A71-AA55-33E27F9BC533") 20 + static let setpointRead = CBUUID(string: "D85EF002-168E-4A71-AA55-33E27F9BC533") 21 + static let dcInput = CBUUID(string: "D85EF003-168E-4A71-AA55-33E27F9BC533") 22 + static let handleTemp = CBUUID(string: "D85EF004-168E-4A71-AA55-33E27F9BC533") 23 + static let powerLevel = CBUUID(string: "D85EF005-168E-4A71-AA55-33E27F9BC533") 24 + static let powerSource = CBUUID(string: "D85EF006-168E-4A71-AA55-33E27F9BC533") 25 + static let tipResistance = CBUUID(string: "D85EF007-168E-4A71-AA55-33E27F9BC533") 26 + static let uptime = CBUUID(string: "D85EF008-168E-4A71-AA55-33E27F9BC533") 27 + static let lastMovement = CBUUID(string: "D85EF009-168E-4A71-AA55-33E27F9BC533") 28 + static let maxTemp = CBUUID(string: "D85EF00A-168E-4A71-AA55-33E27F9BC533") 29 + static let rawTip = CBUUID(string: "D85EF00B-168E-4A71-AA55-33E27F9BC533") 30 + static let hallSensor = CBUUID(string: "D85EF00C-168E-4A71-AA55-33E27F9BC533") 31 + static let operatingMode = CBUUID(string: "D85EF00D-168E-4A71-AA55-33E27F9BC533") 32 + static let estimatedWatts = CBUUID(string: "D85EF00E-168E-4A71-AA55-33E27F9BC533") 33 + 34 + // MARK: - Settings Service 35 + static let settingsService = CBUUID(string: "F6D80000-5A10-4EBA-AA55-33E27F9BC533") 36 + // Setting 0 = Setpoint temperature (writable) 37 + static let setpointSetting = CBUUID(string: "F6D70000-5A10-4EBA-AA55-33E27F9BC533") 38 + static let saveSettings = CBUUID(string: "F6D7FFFF-5A10-4EBA-AA55-33E27F9BC533") 39 + static let resetSettings = CBUUID(string: "F6D7FFFE-5A10-4EBA-AA55-33E27F9BC533") 40 + }
+170
ios/PinecilTime/Models.swift
··· 1 + // 2 + // Models.swift 3 + // PinecilTime 4 + // 5 + 6 + import Foundation 7 + import SwiftUI 8 + 9 + // MARK: - Operating Mode 10 + 11 + enum OperatingMode: UInt32 { 12 + case homeScreen = 0 13 + case soldering = 1 14 + case sleeping = 3 15 + case settingsMenu = 4 16 + case solderingProfile = 6 17 + case thermalRunaway = 9 18 + case hibernating = 14 19 + 20 + var isActive: Bool { 21 + self == .soldering || self == .solderingProfile 22 + } 23 + 24 + var displayName: String { 25 + switch self { 26 + case .homeScreen: return "Idle" 27 + case .soldering: return "Heating" 28 + case .sleeping: return "Sleep" 29 + case .settingsMenu: return "Settings" 30 + case .solderingProfile: return "Profile" 31 + case .thermalRunaway: return "ERROR" 32 + case .hibernating: return "Hibernate" 33 + } 34 + } 35 + 36 + var icon: String { 37 + switch self { 38 + case .homeScreen: return "house" 39 + case .soldering: return "flame.fill" 40 + case .sleeping: return "moon.zzz.fill" 41 + case .settingsMenu: return "gear" 42 + case .solderingProfile: return "flame" 43 + case .thermalRunaway: return "exclamationmark.triangle.fill" 44 + case .hibernating: return "snowflake" 45 + } 46 + } 47 + } 48 + 49 + // MARK: - Power Source 50 + 51 + enum PowerSource: UInt32 { 52 + case dc = 0 53 + case quickCharge = 1 54 + case pdType1 = 2 55 + case pdType2 = 3 56 + 57 + var displayName: String { 58 + switch self { 59 + case .dc: return "DC" 60 + case .quickCharge: return "QC" 61 + case .pdType1, .pdType2: return "PD" 62 + } 63 + } 64 + } 65 + 66 + // MARK: - Temperature Point for Graph 67 + 68 + struct TemperaturePoint: Identifiable { 69 + let id = UUID() 70 + let timestamp: Date 71 + let actualTemp: UInt32 72 + let setpoint: UInt32 73 + } 74 + 75 + // MARK: - Live Data 76 + 77 + @Observable 78 + class IronOSLiveData { 79 + var liveTemp: UInt32 = 0 80 + var setpoint: UInt32 = 0 81 + var dcInput: UInt32 = 0 82 + var handleTemp: UInt32 = 0 83 + var powerLevel: UInt32 = 0 84 + var powerSource: UInt32 = 0 85 + var tipResistance: UInt32 = 0 86 + var uptime: UInt32 = 0 87 + var lastMovement: UInt32 = 0 88 + var maxTemp: UInt32 = 450 89 + var rawTip: UInt32 = 0 90 + var hallSensor: UInt32 = 0 91 + var operatingMode: UInt32 = 0 92 + var estimatedWatts: UInt32 = 0 93 + 94 + var voltage: Double { Double(dcInput) / 10.0 } 95 + var watts: Double { Double(estimatedWatts) / 10.0 } 96 + var resistance: Double { Double(tipResistance) / 100.0 } 97 + var handleTempC: Double { Double(handleTemp) / 10.0 } 98 + var powerPercent: Int { Int(Double(powerLevel) / 255.0 * 100) } 99 + 100 + var mode: OperatingMode? { OperatingMode(rawValue: operatingMode) } 101 + var power: PowerSource? { PowerSource(rawValue: powerSource) } 102 + 103 + var temperatureProgress: Double { 104 + guard maxTemp > 0 else { return 0 } 105 + return min(Double(liveTemp) / Double(maxTemp), 1.0) 106 + } 107 + 108 + var temperatureColor: Color { 109 + let progress = temperatureProgress 110 + if progress < 0.3 { 111 + return .blue 112 + } else if progress < 0.6 { 113 + return Color(red: 1.0, green: 0.6, blue: 0.0) // Orange 114 + } else { 115 + return .red 116 + } 117 + } 118 + 119 + func updateFromBulkData(_ data: Data) { 120 + guard data.count >= 56 else { return } 121 + 122 + let values = data.withUnsafeBytes { buffer -> [UInt32] in 123 + guard let baseAddress = buffer.baseAddress else { return [] } 124 + return (0..<14).map { index in 125 + baseAddress.load(fromByteOffset: index * 4, as: UInt32.self) 126 + } 127 + } 128 + 129 + guard values.count == 14 else { return } 130 + 131 + liveTemp = values[0] 132 + setpoint = values[1] 133 + dcInput = values[2] 134 + handleTemp = values[3] 135 + powerLevel = values[4] 136 + powerSource = values[5] 137 + tipResistance = values[6] 138 + uptime = values[7] 139 + lastMovement = values[8] 140 + maxTemp = values[9] 141 + rawTip = values[10] 142 + hallSensor = values[11] 143 + operatingMode = values[12] 144 + estimatedWatts = values[13] 145 + } 146 + } 147 + 148 + // MARK: - Data Extensions 149 + 150 + extension Data { 151 + func toUInt32() -> UInt32? { 152 + guard count >= 4 else { return nil } 153 + return withUnsafeBytes { $0.load(as: UInt32.self) } 154 + } 155 + 156 + func toUInt64() -> UInt64? { 157 + guard count >= 8 else { return nil } 158 + return withUnsafeBytes { $0.load(as: UInt64.self) } 159 + } 160 + 161 + func toString() -> String? { 162 + String(data: self, encoding: .utf8) 163 + } 164 + } 165 + 166 + extension UInt16 { 167 + var data: Data { 168 + withUnsafeBytes(of: self) { Data($0) } 169 + } 170 + }
+15
ios/PinecilTime/Pinecil_TimeApp.swift
··· 1 + // 2 + // Pinecil_TimeApp.swift 3 + // PinecilTime 4 + // 5 + 6 + import SwiftUI 7 + 8 + @main 9 + struct PinecilTimeApp: App { 10 + var body: some Scene { 11 + WindowGroup { 12 + ContentView() 13 + } 14 + } 15 + }
+57
ios/PinecilTime/TemperatureGraph.swift
··· 1 + // 2 + // TemperatureGraph.swift 3 + // PinecilTime 4 + // 5 + 6 + import Charts 7 + import SwiftUI 8 + 9 + struct TemperatureGraph: View { 10 + let history: [TemperaturePoint] 11 + let maxTemp: UInt32 12 + 13 + var body: some View { 14 + Chart { 15 + setpointLine 16 + actualTempLine 17 + } 18 + .chartXAxis(.hidden) 19 + .chartYAxis(.hidden) 20 + .chartLegend(.hidden) 21 + .chartYScale(domain: 0...500) 22 + } 23 + 24 + @ChartContentBuilder 25 + private var setpointLine: some ChartContent { 26 + ForEach(history) { point in 27 + LineMark( 28 + x: .value("T", point.timestamp), 29 + y: .value("S", Int(point.setpoint)), 30 + series: .value("L", "S") 31 + ) 32 + .foregroundStyle(Color.gray.opacity(0.4)) 33 + .lineStyle(StrokeStyle(lineWidth: 1.5)) 34 + } 35 + } 36 + 37 + @ChartContentBuilder 38 + private var actualTempLine: some ChartContent { 39 + ForEach(history) { point in 40 + LineMark( 41 + x: .value("T", point.timestamp), 42 + y: .value("A", Int(point.actualTemp)), 43 + series: .value("L", "A") 44 + ) 45 + .foregroundStyle(lineColor) 46 + .lineStyle(StrokeStyle(lineWidth: 2.5, lineCap: .round)) 47 + } 48 + } 49 + 50 + private var lineColor: Color { 51 + guard let last = history.last else { return .blue } 52 + let temp = last.actualTemp 53 + if temp < 150 { return .blue } 54 + if temp < 300 { return .orange } 55 + return .red 56 + } 57 + }