commit f38bee223c3a4d5836050289a57b616b010b734c Author: Morgan Date: Thu Nov 27 17:42:54 2025 +0900 init diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..c5f60e0 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,69 @@ +name: Build iOS App + +on: + push: + branches: [ main, master, develop ] + pull_request: + branches: [ main, master, develop ] + workflow_dispatch: + +jobs: + build: + name: Build iOS App + runs-on: macos-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Select Xcode version + run: sudo xcode-select -s /Applications/Xcode_15.2.app/Contents/Developer + + - name: Show Xcode version + run: xcodebuild -version + + - name: Show available SDKs + run: xcodebuild -showsdks + + - name: Build iOS App + run: | + xcodebuild clean build \ + -project EnvSensorReader.xcodeproj \ + -scheme EnvSensorReader \ + -sdk iphoneos \ + -configuration Release \ + CODE_SIGN_IDENTITY="" \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGNING_ALLOWED=NO \ + ONLY_ACTIVE_ARCH=NO + + - name: Build for iOS Simulator + run: | + xcodebuild clean build \ + -project EnvSensorReader.xcodeproj \ + -scheme EnvSensorReader \ + -sdk iphonesimulator \ + -configuration Release \ + CODE_SIGN_IDENTITY="" \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGNING_ALLOWED=NO \ + ONLY_ACTIVE_ARCH=NO + + - name: Archive build products + if: success() + run: | + mkdir -p artifacts + if [ -d "build/Release-iphoneos" ]; then + cp -R build/Release-iphoneos artifacts/ || true + fi + if [ -d "build/Release-iphonesimulator" ]; then + cp -R build/Release-iphonesimulator artifacts/ || true + fi + + - name: Upload build artifacts + if: success() + uses: actions/upload-artifact@v4 + with: + name: ios-build + path: artifacts/ + retention-days: 30 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..25b5b45 --- /dev/null +++ b/.gitignore @@ -0,0 +1,93 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +build/ +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +# *.xcodeproj +# +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +# .swiftpm + +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ +# +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# Accio dependency management +Dependencies/ +.accio/ + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +# Code Injection +# +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ + +# Mac +.DS_Store diff --git a/EnvSensorReader.xcodeproj/project.pbxproj b/EnvSensorReader.xcodeproj/project.pbxproj new file mode 100644 index 0000000..3b070fb --- /dev/null +++ b/EnvSensorReader.xcodeproj/project.pbxproj @@ -0,0 +1,347 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + AA0000000000000000000001 /* EnvSensorReaderApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB0000000000000000000001 /* EnvSensorReaderApp.swift */; }; + AA0000000000000000000002 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB0000000000000000000002 /* ContentView.swift */; }; + AA0000000000000000000003 /* BLEManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB0000000000000000000003 /* BLEManager.swift */; }; + AA0000000000000000000004 /* SensorReading.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB0000000000000000000004 /* SensorReading.swift */; }; + AA0000000000000000000005 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BB0000000000000000000006 /* Assets.xcassets */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + BB0000000000000000000001 /* EnvSensorReaderApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvSensorReaderApp.swift; sourceTree = ""; }; + BB0000000000000000000002 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + BB0000000000000000000003 /* BLEManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLEManager.swift; sourceTree = ""; }; + BB0000000000000000000004 /* SensorReading.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SensorReading.swift; sourceTree = ""; }; + BB0000000000000000000005 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + BB0000000000000000000006 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + CC0000000000000000000001 /* EnvSensorReader.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = EnvSensorReader.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + DD0000000000000000000001 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + EE0000000000000000000001 = { + isa = PBXGroup; + children = ( + EE0000000000000000000002 /* EnvSensorReader */, + EE0000000000000000000003 /* Products */, + ); + sourceTree = ""; + }; + EE0000000000000000000002 /* EnvSensorReader */ = { + isa = PBXGroup; + children = ( + BB0000000000000000000001 /* EnvSensorReaderApp.swift */, + BB0000000000000000000002 /* ContentView.swift */, + BB0000000000000000000003 /* BLEManager.swift */, + BB0000000000000000000004 /* SensorReading.swift */, + BB0000000000000000000006 /* Assets.xcassets */, + BB0000000000000000000005 /* Info.plist */, + ); + path = EnvSensorReader; + sourceTree = ""; + }; + EE0000000000000000000003 /* Products */ = { + isa = PBXGroup; + children = ( + CC0000000000000000000001 /* EnvSensorReader.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + FF0000000000000000000001 /* EnvSensorReader */ = { + isa = PBXNativeTarget; + buildConfigurationList = FF0000000000000000000002 /* Build configuration list for PBXNativeTarget "EnvSensorReader" */; + buildPhases = ( + FF0000000000000000000003 /* Sources */, + DD0000000000000000000001 /* Frameworks */, + FF0000000000000000000004 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = EnvSensorReader; + productName = EnvSensorReader; + productReference = CC0000000000000000000001 /* EnvSensorReader.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 000000000000000000000001 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1500; + LastUpgradeCheck = 1500; + TargetAttributes = { + FF0000000000000000000001 = { + CreatedOnToolsVersion = 15.0; + }; + }; + }; + buildConfigurationList = 000000000000000000000002 /* Build configuration list for PBXProject "EnvSensorReader" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = EE0000000000000000000001; + productRefGroup = EE0000000000000000000003 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + FF0000000000000000000001 /* EnvSensorReader */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + FF0000000000000000000004 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AA0000000000000000000005 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + FF0000000000000000000003 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AA0000000000000000000001 /* EnvSensorReaderApp.swift in Sources */, + AA0000000000000000000002 /* ContentView.swift in Sources */, + AA0000000000000000000003 /* BLEManager.swift in Sources */, + AA0000000000000000000004 /* SensorReading.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 000000000000000000000003 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 000000000000000000000004 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + FF0000000000000000000005 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = EnvSensorReader/Info.plist; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.EnvSensorReader; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + FF0000000000000000000006 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = EnvSensorReader/Info.plist; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.EnvSensorReader; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 000000000000000000000002 /* Build configuration list for PBXProject "EnvSensorReader" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 000000000000000000000003 /* Debug */, + 000000000000000000000004 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + FF0000000000000000000002 /* Build configuration list for PBXNativeTarget "EnvSensorReader" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FF0000000000000000000005 /* Debug */, + FF0000000000000000000006 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 000000000000000000000001 /* Project object */; +} diff --git a/EnvSensorReader.xcodeproj/xcshareddata/xcschemes/EnvSensorReader.xcscheme b/EnvSensorReader.xcodeproj/xcshareddata/xcschemes/EnvSensorReader.xcscheme new file mode 100644 index 0000000..9534b92 --- /dev/null +++ b/EnvSensorReader.xcodeproj/xcshareddata/xcschemes/EnvSensorReader.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/EnvSensorReader/Assets.xcassets/AccentColor.colorset/Contents.json b/EnvSensorReader/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/EnvSensorReader/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/EnvSensorReader/Assets.xcassets/AppIcon.appiconset/Contents.json b/EnvSensorReader/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..13613e3 --- /dev/null +++ b/EnvSensorReader/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/EnvSensorReader/Assets.xcassets/Contents.json b/EnvSensorReader/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/EnvSensorReader/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/EnvSensorReader/BLEManager.swift b/EnvSensorReader/BLEManager.swift new file mode 100644 index 0000000..5e8fd3d --- /dev/null +++ b/EnvSensorReader/BLEManager.swift @@ -0,0 +1,152 @@ +import Foundation +import CoreBluetooth +import Combine + +class BLEManager: NSObject, ObservableObject { + @Published var readings: [SensorReading] = [] + @Published var isScanning = false + @Published var bluetoothState: CBManagerState = .unknown + + private var centralManager: CBCentralManager! + private var seenNonces: Set = [] + + private let deviceName = "EnvSensor" + private let companyID: UInt16 = 0xFFFF + + override init() { + super.init() + centralManager = CBCentralManager(delegate: self, queue: nil) + } + + func startScanning() { + guard centralManager.state == .poweredOn else { + print("Bluetooth not ready") + return + } + + seenNonces.removeAll() + centralManager.scanForPeripherals(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey: true]) + isScanning = true + print("Started scanning for EnvSensor devices...") + } + + func stopScanning() { + centralManager.stopScan() + isScanning = false + print("Stopped scanning") + } + + private func parseManufacturerData(_ data: Data) -> (nonce: UInt16, temp: Double, hum: Double, pres: Double, voltage: Double, current: Double)? { + guard data.count == 16 else { return nil } + + let bytes = [UInt8](data) + + // Parse according to struct format: '= 2 else { + return + } + + // Extract company ID (first 2 bytes, little-endian) + let companyIDBytes = manufacturerData.prefix(2) + let extractedCompanyID = UInt16(companyIDBytes[0]) | (UInt16(companyIDBytes[1]) << 8) + + guard extractedCompanyID == companyID else { + return + } + + // Parse the payload (skip the first 2 bytes which are the company ID) + let payload = manufacturerData.dropFirst(2) + + guard let parsed = parseManufacturerData(payload) else { + return + } + + // Create unique key for deduplication + let key = "\(peripheral.identifier.uuidString)-\(parsed.nonce)" + guard !seenNonces.contains(key) else { + return + } + seenNonces.insert(key) + + // Create reading + let reading = SensorReading( + timestamp: Date(), + deviceAddress: peripheral.identifier.uuidString, + nonce: parsed.nonce, + temperature: parsed.temp, + humidity: parsed.hum, + pressure: parsed.pres, + voltage: parsed.voltage, + current: parsed.current, + rssi: RSSI.intValue + ) + + DispatchQueue.main.async { + self.readings.insert(reading, at: 0) + + // Keep only last 100 readings + if self.readings.count > 100 { + self.readings = Array(self.readings.prefix(100)) + } + } + + print("[\(reading.timestampString)] (\(String(format: "%04X", parsed.nonce))) \(peripheral.identifier.uuidString)") + print(" T=\(String(format: "%5.1f", parsed.temp))°C H=\(String(format: "%5.1f", parsed.hum))% P=\(String(format: "%7.1f", parsed.pres))hPa") + print(" V=\(String(format: "%5.2f", parsed.voltage))V I=\(String(format: "%7.2f", parsed.current))mA P=\(String(format: "%7.2f", reading.power))mW") + print(" RSSI=\(String(format: "%3d", RSSI.intValue))dBm") + print() + } +} diff --git a/EnvSensorReader/ContentView.swift b/EnvSensorReader/ContentView.swift new file mode 100644 index 0000000..8b23dd5 --- /dev/null +++ b/EnvSensorReader/ContentView.swift @@ -0,0 +1,230 @@ +import SwiftUI + +struct ContentView: View { + @StateObject private var bleManager = BLEManager() + + var body: some View { + NavigationView { + VStack { + // Status bar + HStack { + Image(systemName: bluetoothIcon) + .foregroundColor(bluetoothColor) + Text(bluetoothStatusText) + .font(.subheadline) + Spacer() + if bleManager.isScanning { + ProgressView() + .scaleEffect(0.8) + Text("Scanning...") + .font(.subheadline) + .foregroundColor(.secondary) + } + } + .padding() + .background(Color(.systemGray6)) + + // Readings list + if bleManager.readings.isEmpty { + Spacer() + VStack(spacing: 16) { + Image(systemName: "antenna.radiowaves.left.and.right") + .font(.system(size: 60)) + .foregroundColor(.secondary) + Text("No readings yet") + .font(.title2) + .foregroundColor(.secondary) + if !bleManager.isScanning && bleManager.bluetoothState == .poweredOn { + Text("Tap Start to scan for EnvSensor devices") + .font(.subheadline) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + } + } + Spacer() + } else { + List(bleManager.readings) { reading in + SensorReadingRow(reading: reading) + } + .listStyle(.plain) + } + + // Start/Stop button + Button(action: toggleScanning) { + Text(bleManager.isScanning ? "Stop Scanning" : "Start Scanning") + .font(.headline) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .padding() + .background(bleManager.bluetoothState == .poweredOn ? Color.blue : Color.gray) + .cornerRadius(10) + } + .padding() + .disabled(bleManager.bluetoothState != .poweredOn) + } + .navigationTitle("EnvSensor Reader") + .navigationBarTitleDisplayMode(.inline) + } + } + + private var bluetoothIcon: String { + switch bleManager.bluetoothState { + case .poweredOn: + return "bluetooth" + case .poweredOff: + return "bluetooth.slash" + default: + return "bluetooth" + } + } + + private var bluetoothColor: Color { + bleManager.bluetoothState == .poweredOn ? .blue : .red + } + + private var bluetoothStatusText: String { + switch bleManager.bluetoothState { + case .poweredOn: + return "Bluetooth Ready" + case .poweredOff: + return "Bluetooth Off" + case .unauthorized: + return "Bluetooth Unauthorized" + case .unsupported: + return "Bluetooth Not Supported" + default: + return "Bluetooth Unknown" + } + } + + private func toggleScanning() { + if bleManager.isScanning { + bleManager.stopScanning() + } else { + bleManager.startScanning() + } + } +} + +struct SensorReadingRow: View { + let reading: SensorReading + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + // Header + HStack { + Text(reading.timestampString) + .font(.headline) + Text("(\(String(format: "%04X", reading.nonce)))") + .font(.subheadline) + .foregroundColor(.secondary) + Spacer() + Text("\(reading.rssi) dBm") + .font(.subheadline) + .foregroundColor(rssiColor(reading.rssi)) + } + + // Device address + Text(reading.deviceAddress) + .font(.caption) + .foregroundColor(.secondary) + + // Environmental readings + HStack(spacing: 20) { + VStack(alignment: .leading, spacing: 4) { + Text("Temperature") + .font(.caption) + .foregroundColor(.secondary) + HStack(spacing: 4) { + Image(systemName: "thermometer") + .foregroundColor(.red) + Text(String(format: "%.1f°C", reading.temperature)) + .font(.system(.body, design: .monospaced)) + } + } + + VStack(alignment: .leading, spacing: 4) { + Text("Humidity") + .font(.caption) + .foregroundColor(.secondary) + HStack(spacing: 4) { + Image(systemName: "humidity") + .foregroundColor(.blue) + Text(String(format: "%.1f%%", reading.humidity)) + .font(.system(.body, design: .monospaced)) + } + } + } + + HStack(spacing: 20) { + VStack(alignment: .leading, spacing: 4) { + Text("Pressure") + .font(.caption) + .foregroundColor(.secondary) + HStack(spacing: 4) { + Image(systemName: "gauge") + .foregroundColor(.purple) + Text(String(format: "%.1f hPa", reading.pressure)) + .font(.system(.body, design: .monospaced)) + } + } + } + + Divider() + + // Power readings + HStack(spacing: 20) { + VStack(alignment: .leading, spacing: 4) { + Text("Voltage") + .font(.caption) + .foregroundColor(.secondary) + HStack(spacing: 4) { + Image(systemName: "bolt") + .foregroundColor(.orange) + Text(String(format: "%.2f V", reading.voltage)) + .font(.system(.body, design: .monospaced)) + } + } + + VStack(alignment: .leading, spacing: 4) { + Text("Current") + .font(.caption) + .foregroundColor(.secondary) + HStack(spacing: 4) { + Image(systemName: "waveform.path") + .foregroundColor(.green) + Text(String(format: "%.2f mA", reading.current)) + .font(.system(.body, design: .monospaced)) + } + } + } + + HStack(spacing: 4) { + Image(systemName: "power") + .foregroundColor(.yellow) + Text("Power: ") + .font(.caption) + .foregroundColor(.secondary) + Text(String(format: "%.2f mW", reading.power)) + .font(.system(.body, design: .monospaced)) + } + } + .padding(.vertical, 8) + } + + private func rssiColor(_ rssi: Int) -> Color { + if rssi > -60 { + return .green + } else if rssi > -80 { + return .orange + } else { + return .red + } + } +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView() + } +} diff --git a/EnvSensorReader/EnvSensorReaderApp.swift b/EnvSensorReader/EnvSensorReaderApp.swift new file mode 100644 index 0000000..8b4e9f9 --- /dev/null +++ b/EnvSensorReader/EnvSensorReaderApp.swift @@ -0,0 +1,10 @@ +import SwiftUI + +@main +struct EnvSensorReaderApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/EnvSensorReader/Info.plist b/EnvSensorReader/Info.plist new file mode 100644 index 0000000..b9057eb --- /dev/null +++ b/EnvSensorReader/Info.plist @@ -0,0 +1,55 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + + UIApplicationSupportsIndirectInputEvents + + UILaunchScreen + + UIRequiredDeviceCapabilities + + armv7 + bluetooth-le + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + NSBluetoothAlwaysUsageDescription + This app needs Bluetooth access to scan for EnvSensor devices and read environmental data. + NSBluetoothPeripheralUsageDescription + This app needs Bluetooth access to scan for EnvSensor devices and read environmental data. + + diff --git a/EnvSensorReader/SensorReading.swift b/EnvSensorReader/SensorReading.swift new file mode 100644 index 0000000..95addf1 --- /dev/null +++ b/EnvSensorReader/SensorReading.swift @@ -0,0 +1,28 @@ +import Foundation + +struct SensorReading: Identifiable, Equatable { + let id = UUID() + let timestamp: Date + let deviceAddress: String + let nonce: UInt16 + let temperature: Double + let humidity: Double + let pressure: Double + let voltage: Double + let current: Double + let rssi: Int + + var power: Double { + voltage * current + } + + var timestampString: String { + let formatter = DateFormatter() + formatter.dateFormat = "HH:mm:ss" + return formatter.string(from: timestamp) + } + + static func == (lhs: SensorReading, rhs: SensorReading) -> Bool { + lhs.deviceAddress == rhs.deviceAddress && lhs.nonce == rhs.nonce + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..36e8399 --- /dev/null +++ b/README.md @@ -0,0 +1,109 @@ +# EnvSensor Reader - iOS App + +An iOS application for scanning and reading environmental sensor data from BLE devices named "EnvSensor". + +## Features + +- Real-time BLE scanning for EnvSensor devices +- Displays environmental readings: + - Temperature (°C) + - Humidity (%) + - Pressure (hPa) +- Displays power metrics: + - Voltage (V) + - Current (mA) + - Power (mW) +- Shows signal strength (RSSI in dBm) +- Automatic deduplication of readings based on nonce +- Clean, modern SwiftUI interface + +## Requirements + +- iOS 15.0 or later +- iPhone or iPad with Bluetooth LE support +- Xcode 15.0 or later (for building) + +## BLE Protocol + +The app scans for BLE devices with the following characteristics: + +- **Device Name**: `EnvSensor` +- **Company ID**: `0xFFFF` +- **Data Format** (16 bytes, little-endian): + - Bytes 0-1: Nonce (UInt16) + - Bytes 2-3: Temperature (Int16, divide by 100 for °C) + - Bytes 4-5: Humidity (UInt16, divide by 100 for %) + - Bytes 6-9: Pressure (UInt32, divide by 10 for hPa) + - Bytes 10-11: Voltage (UInt16, divide by 100 for V) + - Bytes 12-15: Current (Int32, divide by 100 for mA) + +## Building the App + +### Using Xcode + +1. Open `EnvSensorReader.xcodeproj` in Xcode +2. Select your target device or simulator +3. Press Cmd+R to build and run + +### Using xcodebuild (command line) + +For iOS device: +```bash +xcodebuild clean build \ + -project EnvSensorReader.xcodeproj \ + -scheme EnvSensorReader \ + -sdk iphoneos \ + -configuration Release +``` + +For iOS Simulator: +```bash +xcodebuild clean build \ + -project EnvSensorReader.xcodeproj \ + -scheme EnvSensorReader \ + -sdk iphonesimulator \ + -configuration Release +``` + +## GitHub Actions + +This project includes a GitHub Actions workflow that automatically builds the app on every push to main/master/develop branches. The workflow: + +- Builds for both iOS device and simulator +- Runs on macOS runners +- Archives build artifacts for download +- Supports manual triggering via workflow_dispatch + +## Permissions + +The app requires Bluetooth permissions to function. The following permissions are declared in Info.plist: + +- `NSBluetoothAlwaysUsageDescription`: For scanning BLE devices +- `NSBluetoothPeripheralUsageDescription`: For BLE peripheral access + +Users will be prompted to grant Bluetooth access when the app first launches. + +## Project Structure + +``` +EnvSensorReader/ +├── EnvSensorReader/ +│ ├── EnvSensorReaderApp.swift # App entry point +│ ├── ContentView.swift # Main UI +│ ├── BLEManager.swift # BLE scanning and parsing +│ ├── SensorReading.swift # Data model +│ ├── Assets.xcassets/ # App assets +│ └── Info.plist # App configuration +├── EnvSensorReader.xcodeproj/ # Xcode project +└── .github/ + └── workflows/ + └── build.yml # CI/CD workflow +``` + +## Python Version + +This iOS app is a port of the Python script `env_reader.py` which uses the Bleak library for BLE scanning. Both versions implement the same protocol and functionality. + +## License + +This project is provided as-is for educational and development purposes.