├── .gitattributes ├── .gitignore ├── BLEHelper.cs ├── BSManager.csproj ├── BSManager.sln ├── BSManager ├── AutoUpdaterBSManager.json └── BSManager_tray.png ├── BSManagerMain.Designer.cs ├── BSManagerMain.cs ├── BSManagerMain.resx ├── BSManagerRes.Designer.cs ├── BSManagerRes.resx ├── GlobalSuppressions.cs ├── LICENSE ├── LightHouse.cs ├── Program.cs ├── Properties ├── Settings.Designer.cs └── Settings.settings ├── README.md ├── Resources ├── bsmanager_on.ico ├── error.png └── warning.png ├── bsmanager.ico └── bsmanager.png /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd -------------------------------------------------------------------------------- /BLEHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Text; 5 | using System.Linq; 6 | using Windows.Devices.Bluetooth.GenericAttributeProfile; 7 | using Windows.Devices.Enumeration; 8 | using Windows.Security.Cryptography; 9 | using Windows.Storage.Streams; 10 | using System.Threading.Tasks; 11 | using System.Threading; 12 | 13 | namespace BSManager 14 | { 15 | /// 16 | /// Represents the display of an attribute - both characteristics and services. 17 | /// 18 | public class BluetoothLEAttributeDisplay 19 | { 20 | public GattCharacteristic characteristic; 21 | public GattDescriptor descriptor; 22 | 23 | public GattDeviceService service; 24 | 25 | public BluetoothLEAttributeDisplay(GattDeviceService service) 26 | { 27 | this.service = service; 28 | AttributeDisplayType = AttributeType.Service; 29 | } 30 | 31 | public BluetoothLEAttributeDisplay(GattCharacteristic characteristic) 32 | { 33 | this.characteristic = characteristic; 34 | AttributeDisplayType = AttributeType.Characteristic; 35 | } 36 | 37 | public string Chars => (CanRead ? "R" : " ") + (CanWrite ? "W" : " ") + (CanNotify ? "N" : " "); 38 | 39 | public bool CanRead 40 | { 41 | get 42 | { 43 | return this.characteristic != null ? this.characteristic.CharacteristicProperties.HasFlag(GattCharacteristicProperties.Read) : false; 44 | } 45 | } 46 | 47 | public bool CanWrite 48 | { 49 | get 50 | { 51 | return this.characteristic != null ? 52 | (this.characteristic.CharacteristicProperties.HasFlag(GattCharacteristicProperties.Write) || 53 | this.characteristic.CharacteristicProperties.HasFlag(GattCharacteristicProperties.WriteWithoutResponse) || 54 | this.characteristic.CharacteristicProperties.HasFlag(GattCharacteristicProperties.ReliableWrites) || 55 | this.characteristic.CharacteristicProperties.HasFlag(GattCharacteristicProperties.WritableAuxiliaries)) 56 | : false; 57 | } 58 | } 59 | 60 | public bool CanNotify 61 | { 62 | get 63 | { 64 | return this.characteristic != null ? this.characteristic.CharacteristicProperties.HasFlag(GattCharacteristicProperties.Notify) : false; 65 | } 66 | } 67 | 68 | 69 | public string Name 70 | { 71 | get 72 | { 73 | switch (AttributeDisplayType) 74 | { 75 | case AttributeType.Service: 76 | if (IsSigDefinedUuid(service.Uuid)) 77 | { 78 | GattNativeServiceUuid serviceName; 79 | if (Enum.TryParse(Utilities.ConvertUuidToShortId(service.Uuid).ToString(), out serviceName)) 80 | { 81 | return serviceName.ToString(); 82 | } 83 | } 84 | else 85 | { 86 | return "Custom Service: " + service.Uuid; 87 | } 88 | break; 89 | case AttributeType.Characteristic: 90 | if (IsSigDefinedUuid(characteristic.Uuid)) 91 | { 92 | GattNativeCharacteristicUuid characteristicName; 93 | if (Enum.TryParse(Utilities.ConvertUuidToShortId(characteristic.Uuid).ToString(), 94 | out characteristicName)) 95 | { 96 | return characteristicName.ToString(); 97 | } 98 | } 99 | else 100 | { 101 | if (!string.IsNullOrEmpty(characteristic.UserDescription)) 102 | { 103 | return characteristic.UserDescription; 104 | } 105 | 106 | else 107 | { 108 | return "Custom Characteristic: " + characteristic.Uuid; 109 | } 110 | } 111 | break; 112 | default: 113 | break; 114 | } 115 | return "Invalid"; 116 | } 117 | } 118 | 119 | public AttributeType AttributeDisplayType { get; } 120 | 121 | /// 122 | /// The SIG has a standard base value for Assigned UUIDs. In order to determine if a UUID is SIG defined, 123 | /// zero out the unique section and compare the base sections. 124 | /// 125 | /// The UUID to determine if SIG assigned 126 | /// 127 | private static bool IsSigDefinedUuid(Guid uuid) 128 | { 129 | var bluetoothBaseUuid = new Guid("00000000-0000-1000-8000-00805F9B34FB"); 130 | 131 | var bytes = uuid.ToByteArray(); 132 | // Zero out the first and second bytes 133 | // Note how each byte gets flipped in a section - 1234 becomes 34 12 134 | // Example Guid: 35918bc9-1234-40ea-9779-889d79b753f0 135 | // ^^^^ 136 | // bytes output = C9 8B 91 35 34 12 EA 40 97 79 88 9D 79 B7 53 F0 137 | // ^^ ^^ 138 | bytes[0] = 0; 139 | bytes[1] = 0; 140 | var baseUuid = new Guid(bytes); 141 | return baseUuid == bluetoothBaseUuid; 142 | } 143 | } 144 | 145 | public enum AttributeType 146 | { 147 | Service = 0, 148 | Characteristic = 1, 149 | Descriptor = 2 150 | } 151 | 152 | /// 153 | /// Display class used to represent a BluetoothLEDevice in the Device list 154 | /// 155 | public class BluetoothLEDeviceDisplay : INotifyPropertyChanged 156 | { 157 | public BluetoothLEDeviceDisplay(DeviceInformation deviceInfoIn) 158 | { 159 | DeviceInformation = deviceInfoIn; 160 | } 161 | 162 | public DeviceInformation DeviceInformation { get; private set; } 163 | 164 | public string Id => DeviceInformation.Id; 165 | public string Name => DeviceInformation.Name; 166 | public bool IsPaired => DeviceInformation.Pairing.IsPaired; 167 | public bool IsConnected => (bool?)DeviceInformation.Properties["System.Devices.Aep.IsConnected"] == true; 168 | public bool IsConnectable => (bool?)DeviceInformation.Properties["System.Devices.Aep.Bluetooth.Le.IsConnectable"] == true; 169 | 170 | public IReadOnlyDictionary Properties => DeviceInformation.Properties; 171 | 172 | public event PropertyChangedEventHandler PropertyChanged; 173 | 174 | public void Update(DeviceInformationUpdate deviceInfoUpdate) 175 | { 176 | DeviceInformation.Update(deviceInfoUpdate); 177 | 178 | OnPropertyChanged("Id"); 179 | OnPropertyChanged("Name"); 180 | OnPropertyChanged("DeviceInformation"); 181 | OnPropertyChanged("IsPaired"); 182 | OnPropertyChanged("IsConnected"); 183 | OnPropertyChanged("Properties"); 184 | OnPropertyChanged("IsConnectable"); 185 | } 186 | 187 | protected void OnPropertyChanged(string name) 188 | { 189 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); 190 | } 191 | } 192 | 193 | /// 194 | /// This enum assists in finding a string representation of a BT SIG assigned value for Service UUIDS 195 | /// Reference: https://developer.bluetooth.org/gatt/services/Pages/ServicesHome.aspx 196 | /// 197 | public enum GattNativeServiceUuid : ushort 198 | { 199 | None = 0, 200 | AlertNotification = 0x1811, 201 | Battery = 0x180F, 202 | BloodPressure = 0x1810, 203 | CurrentTimeService = 0x1805, 204 | CyclingSpeedandCadence = 0x1816, 205 | DeviceInformation = 0x180A, 206 | GenericAccess = 0x1800, 207 | GenericAttribute = 0x1801, 208 | Glucose = 0x1808, 209 | HealthThermometer = 0x1809, 210 | HeartRate = 0x180D, 211 | HumanInterfaceDevice = 0x1812, 212 | ImmediateAlert = 0x1802, 213 | LinkLoss = 0x1803, 214 | NextDSTChange = 0x1807, 215 | PhoneAlertStatus = 0x180E, 216 | ReferenceTimeUpdateService = 0x1806, 217 | RunningSpeedandCadence = 0x1814, 218 | ScanParameters = 0x1813, 219 | TxPower = 0x1804, 220 | SimpleKeyService = 0xFFE0 221 | } 222 | 223 | /// 224 | /// This enum is nice for finding a string representation of a BT SIG assigned value for Characteristic UUIDs 225 | /// Reference: https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicsHome.aspx 226 | /// 227 | public enum GattNativeCharacteristicUuid : ushort 228 | { 229 | None = 0, 230 | AlertCategoryID = 0x2A43, 231 | AlertCategoryIDBitMask = 0x2A42, 232 | AlertLevel = 0x2A06, 233 | AlertNotificationControlPoint = 0x2A44, 234 | AlertStatus = 0x2A3F, 235 | Appearance = 0x2A01, 236 | BatteryLevel = 0x2A19, 237 | BloodPressureFeature = 0x2A49, 238 | BloodPressureMeasurement = 0x2A35, 239 | BodySensorLocation = 0x2A38, 240 | BootKeyboardInputReport = 0x2A22, 241 | BootKeyboardOutputReport = 0x2A32, 242 | BootMouseInputReport = 0x2A33, 243 | CSCFeature = 0x2A5C, 244 | CSCMeasurement = 0x2A5B, 245 | CurrentTime = 0x2A2B, 246 | DateTime = 0x2A08, 247 | DayDateTime = 0x2A0A, 248 | DayofWeek = 0x2A09, 249 | DeviceName = 0x2A00, 250 | DSTOffset = 0x2A0D, 251 | ExactTime256 = 0x2A0C, 252 | FirmwareRevisionString = 0x2A26, 253 | GlucoseFeature = 0x2A51, 254 | GlucoseMeasurement = 0x2A18, 255 | GlucoseMeasurementContext = 0x2A34, 256 | HardwareRevisionString = 0x2A27, 257 | HeartRateControlPoint = 0x2A39, 258 | HeartRateMeasurement = 0x2A37, 259 | HIDControlPoint = 0x2A4C, 260 | HIDInformation = 0x2A4A, 261 | IEEE11073_20601RegulatoryCertificationDataList = 0x2A2A, 262 | IntermediateCuffPressure = 0x2A36, 263 | IntermediateTemperature = 0x2A1E, 264 | LocalTimeInformation = 0x2A0F, 265 | ManufacturerNameString = 0x2A29, 266 | MeasurementInterval = 0x2A21, 267 | ModelNumberString = 0x2A24, 268 | NewAlert = 0x2A46, 269 | PeripheralPreferredConnectionParameters = 0x2A04, 270 | PeripheralPrivacyFlag = 0x2A02, 271 | PnPID = 0x2A50, 272 | ProtocolMode = 0x2A4E, 273 | ReconnectionAddress = 0x2A03, 274 | RecordAccessControlPoint = 0x2A52, 275 | ReferenceTimeInformation = 0x2A14, 276 | Report = 0x2A4D, 277 | ReportMap = 0x2A4B, 278 | RingerControlPoint = 0x2A40, 279 | RingerSetting = 0x2A41, 280 | RSCFeature = 0x2A54, 281 | RSCMeasurement = 0x2A53, 282 | SCControlPoint = 0x2A55, 283 | ScanIntervalWindow = 0x2A4F, 284 | ScanRefresh = 0x2A31, 285 | SensorLocation = 0x2A5D, 286 | SerialNumberString = 0x2A25, 287 | ServiceChanged = 0x2A05, 288 | SoftwareRevisionString = 0x2A28, 289 | SupportedNewAlertCategory = 0x2A47, 290 | SupportedUnreadAlertCategory = 0x2A48, 291 | SystemID = 0x2A23, 292 | TemperatureMeasurement = 0x2A1C, 293 | TemperatureType = 0x2A1D, 294 | TimeAccuracy = 0x2A12, 295 | TimeSource = 0x2A13, 296 | TimeUpdateControlPoint = 0x2A16, 297 | TimeUpdateState = 0x2A17, 298 | TimewithDST = 0x2A11, 299 | TimeZone = 0x2A0E, 300 | TxPowerLevel = 0x2A07, 301 | UnreadAlertStatus = 0x2A45, 302 | AggregateInput = 0x2A5A, 303 | AnalogInput = 0x2A58, 304 | AnalogOutput = 0x2A59, 305 | CyclingPowerControlPoint = 0x2A66, 306 | CyclingPowerFeature = 0x2A65, 307 | CyclingPowerMeasurement = 0x2A63, 308 | CyclingPowerVector = 0x2A64, 309 | DigitalInput = 0x2A56, 310 | DigitalOutput = 0x2A57, 311 | ExactTime100 = 0x2A0B, 312 | LNControlPoint = 0x2A6B, 313 | LNFeature = 0x2A6A, 314 | LocationandSpeed = 0x2A67, 315 | Navigation = 0x2A68, 316 | NetworkAvailability = 0x2A3E, 317 | PositionQuality = 0x2A69, 318 | ScientificTemperatureinCelsius = 0x2A3C, 319 | SecondaryTimeZone = 0x2A10, 320 | String = 0x2A3D, 321 | TemperatureinCelsius = 0x2A1F, 322 | TemperatureinFahrenheit = 0x2A20, 323 | TimeBroadcast = 0x2A15, 324 | BatteryLevelState = 0x2A1B, 325 | BatteryPowerState = 0x2A1A, 326 | PulseOximetryContinuousMeasurement = 0x2A5F, 327 | PulseOximetryControlPoint = 0x2A62, 328 | PulseOximetryFeatures = 0x2A61, 329 | PulseOximetryPulsatileEvent = 0x2A60, 330 | SimpleKeyState = 0xFFE1 331 | } 332 | 333 | /// 334 | /// This enum assists in finding a string representation of a BT SIG assigned value for Descriptor UUIDs 335 | /// Reference: https://developer.bluetooth.org/gatt/descriptors/Pages/DescriptorsHomePage.aspx 336 | /// 337 | public enum GattNativeDescriptorUuid : ushort 338 | { 339 | CharacteristicExtendedProperties = 0x2900, 340 | CharacteristicUserDescription = 0x2901, 341 | ClientCharacteristicConfiguration = 0x2902, 342 | ServerCharacteristicConfiguration = 0x2903, 343 | CharacteristicPresentationFormat = 0x2904, 344 | CharacteristicAggregateFormat = 0x2905, 345 | ValidRange = 0x2906, 346 | ExternalReportReference = 0x2907, 347 | ReportReference = 0x2908 348 | } 349 | 350 | public enum DataFormat 351 | { 352 | ASCII = 0, 353 | UTF8, 354 | Dec, 355 | Hex, 356 | Bin, 357 | } 358 | 359 | public static class Utilities 360 | { 361 | /// 362 | /// Converts from standard 128bit UUID to the assigned 32bit UUIDs. Makes it easy to compare services 363 | /// that devices expose to the standard list. 364 | /// 365 | /// UUID to convert to 32 bit 366 | /// 367 | public static ushort ConvertUuidToShortId(Guid uuid) 368 | { 369 | // Get the short Uuid 370 | var bytes = uuid.ToByteArray(); 371 | var shortUuid = (ushort)(bytes[0] | (bytes[1] << 8)); 372 | return shortUuid; 373 | } 374 | 375 | /// 376 | /// Converts from a buffer to a properly sized byte array 377 | /// 378 | /// 379 | /// 380 | public static byte[] ReadBufferToBytes(IBuffer buffer) 381 | { 382 | var dataLength = buffer.Length; 383 | var data = new byte[dataLength]; 384 | using (var reader = DataReader.FromBuffer(buffer)) 385 | { 386 | reader.ReadBytes(data); 387 | } 388 | return data; 389 | } 390 | 391 | /// 392 | /// This function converts IBuffer data to string by specified format 393 | /// 394 | /// 395 | /// 396 | /// 397 | public static string FormatValue(IBuffer buffer, DataFormat format) 398 | { 399 | byte[] data; 400 | CryptographicBuffer.CopyToByteArray(buffer, out data); 401 | 402 | switch (format) 403 | { 404 | case DataFormat.ASCII: 405 | return Encoding.ASCII.GetString(data); 406 | 407 | case DataFormat.UTF8: 408 | return Encoding.UTF8.GetString(data); 409 | 410 | case DataFormat.Dec: 411 | return string.Join(" ", data.Select(b => b.ToString("00"))); 412 | 413 | case DataFormat.Hex: 414 | return BitConverter.ToString(data).Replace("-", " "); 415 | 416 | case DataFormat.Bin: 417 | var s = string.Empty; 418 | foreach (var b in data) s += Convert.ToString(b, 2).PadLeft(8, '0') + " "; 419 | return s; 420 | 421 | default: 422 | return Encoding.ASCII.GetString(data); 423 | } 424 | } 425 | 426 | /// 427 | /// Format data for writing by specific format 428 | /// 429 | /// 430 | /// 431 | /// 432 | public static IBuffer FormatData(string data, DataFormat format) 433 | { 434 | try 435 | { 436 | // For text formats, use CryptographicBuffer 437 | if (format == DataFormat.ASCII || format == DataFormat.UTF8) 438 | { 439 | return CryptographicBuffer.ConvertStringToBinary(data, BinaryStringEncoding.Utf8); 440 | } 441 | else 442 | { 443 | string[] values = data.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); 444 | byte[] bytes = new byte[values.Length]; 445 | 446 | for (int i = 0; i < values.Length; i++) 447 | bytes[i] = Convert.ToByte(values[i], (format == DataFormat.Dec ? 10 : (format == DataFormat.Hex ? 16 : 2))); 448 | 449 | var writer = new DataWriter(); 450 | writer.ByteOrder = ByteOrder.LittleEndian; 451 | writer.WriteBytes(bytes); 452 | 453 | return writer.DetachBuffer(); 454 | } 455 | } 456 | catch (Exception error) 457 | { 458 | Console.WriteLine(error.Message); 459 | return null; 460 | } 461 | } 462 | 463 | /// 464 | /// This function is trying to find device or service or attribute by name or number 465 | /// 466 | /// source collection 467 | /// name or number to find 468 | /// ID for device, Name for services or attributes 469 | public static string GetIdByNameOrNumber(object collection, string name) 470 | { 471 | string result = string.Empty; 472 | 473 | // If number is specified, try to open BLE device by specific number 474 | if (name[0] == '#') 475 | { 476 | int devNumber = -1; 477 | if (int.TryParse(name.Substring(1), out devNumber)) 478 | { 479 | // Try to find device ID by number 480 | if (collection is List) 481 | { 482 | if (0 <= devNumber && devNumber < (collection as List).Count) 483 | { 484 | result = (collection as List)[devNumber].Id; 485 | } 486 | else 487 | if (Console.IsOutputRedirected) 488 | Console.WriteLine("Device number {0:00} is not in device list range", devNumber); 489 | } 490 | // for services or attributes 491 | else 492 | { 493 | if (0 <= devNumber && devNumber < (collection as List).Count) 494 | { 495 | result = (collection as List)[devNumber].Name; 496 | } 497 | } 498 | } 499 | else 500 | if (!Console.IsOutputRedirected) 501 | Console.WriteLine("Invalid device number {0}", name.Substring(1)); 502 | } 503 | // else try to find name 504 | else 505 | { 506 | // ... for devices 507 | if (collection is List) 508 | { 509 | var foundDevices = (collection as List).Where(d => d.Name.ToLower().StartsWith(name.ToLower())).ToList(); 510 | if (foundDevices.Count == 0) 511 | { 512 | if (!Console.IsOutputRedirected) 513 | Console.WriteLine("Can't connect to {0}.", name); 514 | } 515 | else if (foundDevices.Count == 1) 516 | { 517 | result = foundDevices.First().Id; 518 | } 519 | else 520 | { 521 | if (!Console.IsOutputRedirected) 522 | Console.WriteLine("Found multiple devices with names started from {0}. Please provide an exact name.", name); 523 | } 524 | } 525 | // for services or attributes 526 | else 527 | { 528 | var foundDispAttrs = (collection as List).Where(d => d.Name.ToLower().StartsWith(name.ToLower())).ToList(); 529 | if (foundDispAttrs.Count == 0) 530 | { 531 | if (Console.IsOutputRedirected) 532 | Console.WriteLine("No service/characteristic found by name {0}.", name); 533 | } 534 | else if (foundDispAttrs.Count == 1) 535 | { 536 | result = foundDispAttrs.First().Name; 537 | } 538 | else 539 | { 540 | if (Console.IsOutputRedirected) 541 | Console.WriteLine("Found multiple services/characteristic with names started from {0}. Please provide an exact name.", name); 542 | } 543 | } 544 | } 545 | return result; 546 | } 547 | } 548 | 549 | public static class TaskExtensions 550 | { 551 | public static async Task TimeoutAfter(this Task task, TimeSpan timeout) 552 | { 553 | using (var timeoutCancellationTokenSource = new CancellationTokenSource()) 554 | { 555 | var completedTask = await Task.WhenAny(task, Task.Delay(timeout, timeoutCancellationTokenSource.Token)); 556 | if (completedTask == task) 557 | { 558 | timeoutCancellationTokenSource.Cancel(); 559 | return await task; // Very important in order to propagate exceptions 560 | } 561 | else 562 | { 563 | throw new TimeoutException("The operation has timed out."); 564 | } 565 | } 566 | } 567 | } 568 | } -------------------------------------------------------------------------------- /BSManager.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | WinExe 5 | netcoreapp3.1 6 | win10-x64 7 | true 8 | bsmanager.ico 9 | BSManager.Program 10 | 0 11 | 0.20 12 | ManniX 13 | LICENSE 14 | bsmanager.png 15 | 16 | Debug;Release;Release Profiler 17 | AnyCPU;x64 18 | 19 | 20 | 21 | TRACE 22 | 23 | 24 | 25 | TRACE 26 | 27 | 28 | 29 | TRACE 30 | 31 | 32 | 33 | TRACE 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | tlbimp 49 | 0 50 | 1 51 | f935dc20-1cf0-11d0-adb9-00c04fd58a0b 52 | 0 53 | false 54 | true 55 | true 56 | 57 | 58 | True 59 | 60 | 61 | 62 | True 63 | 64 | 65 | 66 | 67 | 68 | 69 | Never 70 | 71 | 72 | Always 73 | 74 | 75 | Always 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | True 92 | True 93 | Settings.settings 94 | 95 | 96 | True 97 | True 98 | BSManagerRes.resx 99 | 100 | 101 | 102 | 103 | 104 | ResXFileCodeGenerator 105 | BSManagerRes.Designer.cs 106 | 107 | 108 | 109 | 110 | 111 | SettingsSingleFileGenerator 112 | Settings.Designer.cs 113 | 114 | 115 | 116 | 117 | WinExe 118 | 119 | 120 | 121 | true 122 | 2.4.1 123 | ManniX 124 | VR Base Stations Manager for Pimax and Vive Pro HeadSets 125 | https://github.com/mann1x/BSManager 126 | https://github.com/mann1x/BSManager 127 | GitHub 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /BSManager.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31624.102 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BSManager", "BSManager.csproj", "{147B18D2-8162-45EA-9A9C-1078723706F4}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Debug|x64 = Debug|x64 12 | Release Profiler|Any CPU = Release Profiler|Any CPU 13 | Release Profiler|x64 = Release Profiler|x64 14 | Release|Any CPU = Release|Any CPU 15 | Release|x64 = Release|x64 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {147B18D2-8162-45EA-9A9C-1078723706F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {147B18D2-8162-45EA-9A9C-1078723706F4}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {147B18D2-8162-45EA-9A9C-1078723706F4}.Debug|x64.ActiveCfg = Debug|x64 21 | {147B18D2-8162-45EA-9A9C-1078723706F4}.Debug|x64.Build.0 = Debug|x64 22 | {147B18D2-8162-45EA-9A9C-1078723706F4}.Release Profiler|Any CPU.ActiveCfg = Release Profiler|Any CPU 23 | {147B18D2-8162-45EA-9A9C-1078723706F4}.Release Profiler|Any CPU.Build.0 = Release Profiler|Any CPU 24 | {147B18D2-8162-45EA-9A9C-1078723706F4}.Release Profiler|x64.ActiveCfg = Release Profiler|x64 25 | {147B18D2-8162-45EA-9A9C-1078723706F4}.Release Profiler|x64.Build.0 = Release Profiler|x64 26 | {147B18D2-8162-45EA-9A9C-1078723706F4}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {147B18D2-8162-45EA-9A9C-1078723706F4}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {147B18D2-8162-45EA-9A9C-1078723706F4}.Release|x64.ActiveCfg = Release|x64 29 | {147B18D2-8162-45EA-9A9C-1078723706F4}.Release|x64.Build.0 = Release|x64 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {2BC42F4E-3E83-481E-800F-4C9E72E3BEDE} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /BSManager/AutoUpdaterBSManager.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.4.0", 3 | "url": "https://github.com/mann1x/BSManager/releases/download/v2.4.1/BSManager.zip", 4 | "changelog": "https://github.com/mann1x/BSManager/releases", 5 | "mandatory": { 6 | "value": false, 7 | "minVersion": "2.4.1", 8 | "mode": 1 9 | }, 10 | "checksum": { 11 | "value": "6DB2C26D548FCBE294CC55177485D56178351266EC969AFE42981DEFDA943190", 12 | "hashingAlgorithm": "SHA256" 13 | } 14 | } -------------------------------------------------------------------------------- /BSManager/BSManager_tray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mann1x/BSManager/b5a7bdbc44c8cca84d997a20c392483751b9f666/BSManager/BSManager_tray.png -------------------------------------------------------------------------------- /BSManagerMain.Designer.cs: -------------------------------------------------------------------------------- 1 |  2 | using System.Reflection; 3 | using System.Windows.Forms; 4 | 5 | namespace BSManager 6 | { 7 | partial class Form1 8 | { 9 | /// 10 | /// Required designer variable. 11 | /// 12 | private System.ComponentModel.IContainer components = null; 13 | 14 | /// 15 | /// Clean up any resources being used. 16 | /// 17 | /// true if managed resources should be disposed; otherwise, false. 18 | protected override void Dispose(bool disposing) 19 | { 20 | if (disposing && (components != null)) 21 | { 22 | components.Dispose(); 23 | } 24 | base.Dispose(disposing); 25 | } 26 | 27 | #region Windows Form Designer generated code 28 | 29 | /// 30 | /// Required method for Designer support - do not modify 31 | /// the contents of this method with the code editor. 32 | /// 33 | private void InitializeComponent() 34 | { 35 | this.components = new System.ComponentModel.Container(); 36 | this.notifyIcon1 = new System.Windows.Forms.NotifyIcon(this.components); 37 | this.contextMenuStrip1 = new System.Windows.Forms.ContextMenuStrip(this.components); 38 | this.toolStripMenuItemBS = new System.Windows.Forms.ToolStripMenuItem(); 39 | this.ToolStripMenuItemDisco = new System.Windows.Forms.ToolStripMenuItem(); 40 | this.hMDToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); 41 | this.ToolStripMenuItemHmd = new System.Windows.Forms.ToolStripMenuItem(); 42 | this.toolStripRunAtStartup = new System.Windows.Forms.ToolStripMenuItem(); 43 | this.RuntimeToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); 44 | this.toolStripMenuItem4 = new System.Windows.Forms.ToolStripMenuItem(); 45 | this.bSManagerVersionToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); 46 | this.toolStripDebugLog = new System.Windows.Forms.ToolStripMenuItem(); 47 | this.SteamVR_LH_ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); 48 | this.SteamVR_DB_ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); 49 | this.Pimax_LH_ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); 50 | this.Pimax_DB_ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); 51 | this.documentationToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); 52 | this.licenseToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); 53 | this.createDesktopShortcutToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); 54 | this.quitToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); 55 | this.disableProgressToastToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); 56 | this.contextMenuStrip1.SuspendLayout(); 57 | this.SuspendLayout(); 58 | // 59 | // notifyIcon1 60 | // 61 | this.notifyIcon1.BalloonTipText = "BSManager"; 62 | this.notifyIcon1.BalloonTipTitle = "BSManager"; 63 | this.notifyIcon1.ContextMenuStrip = this.contextMenuStrip1; 64 | this.notifyIcon1.Icon = global::BSManager.BSManagerRes.bsmanager_off; 65 | this.notifyIcon1.Text = "BSManager"; 66 | this.notifyIcon1.Visible = true; 67 | // 68 | // contextMenuStrip1 69 | // 70 | this.contextMenuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { 71 | this.toolStripMenuItemBS, 72 | this.hMDToolStripMenuItem, 73 | this.toolStripRunAtStartup, 74 | this.RuntimeToolStripMenuItem, 75 | this.toolStripMenuItem4, 76 | this.quitToolStripMenuItem}); 77 | this.contextMenuStrip1.Name = "contextMenuStrip1"; 78 | this.contextMenuStrip1.Size = new System.Drawing.Size(181, 158); 79 | // 80 | // toolStripMenuItemBS 81 | // 82 | this.toolStripMenuItemBS.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { 83 | this.ToolStripMenuItemDisco}); 84 | this.toolStripMenuItemBS.Name = "toolStripMenuItemBS"; 85 | this.toolStripMenuItemBS.Size = new System.Drawing.Size(180, 22); 86 | this.toolStripMenuItemBS.Text = "Base Stations"; 87 | // 88 | // ToolStripMenuItemDisco 89 | // 90 | this.ToolStripMenuItemDisco.Name = "ToolStripMenuItemDisco"; 91 | this.ToolStripMenuItemDisco.Size = new System.Drawing.Size(144, 22); 92 | this.ToolStripMenuItemDisco.Text = "Discovered: 0"; 93 | // 94 | // hMDToolStripMenuItem 95 | // 96 | this.hMDToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { 97 | this.ToolStripMenuItemHmd}); 98 | this.hMDToolStripMenuItem.Name = "hMDToolStripMenuItem"; 99 | this.hMDToolStripMenuItem.Size = new System.Drawing.Size(180, 22); 100 | this.hMDToolStripMenuItem.Text = "HMD"; 101 | // 102 | // ToolStripMenuItemHmd 103 | // 104 | this.ToolStripMenuItemHmd.Name = "ToolStripMenuItemHmd"; 105 | this.ToolStripMenuItemHmd.Size = new System.Drawing.Size(95, 22); 106 | this.ToolStripMenuItemHmd.Text = "OFF"; 107 | // 108 | // toolStripRunAtStartup 109 | // 110 | this.toolStripRunAtStartup.Name = "toolStripRunAtStartup"; 111 | this.toolStripRunAtStartup.Size = new System.Drawing.Size(180, 22); 112 | this.toolStripRunAtStartup.Text = "Run at Startup"; 113 | this.toolStripRunAtStartup.Click += new System.EventHandler(this.toolStripRunAtStartup_Click); 114 | // 115 | // RuntimeToolStripMenuItem 116 | // 117 | this.RuntimeToolStripMenuItem.Name = "RuntimeToolStripMenuItem"; 118 | this.RuntimeToolStripMenuItem.Size = new System.Drawing.Size(180, 22); 119 | this.RuntimeToolStripMenuItem.Text = "Manage Runtime"; 120 | this.RuntimeToolStripMenuItem.Click += new System.EventHandler(this.RuntimeToolStripMenuItem_Click); 121 | // 122 | // toolStripMenuItem4 123 | // 124 | this.toolStripMenuItem4.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { 125 | this.bSManagerVersionToolStripMenuItem, 126 | this.disableProgressToastToolStripMenuItem, 127 | this.toolStripDebugLog, 128 | this.SteamVR_LH_ToolStripMenuItem, 129 | this.Pimax_LH_ToolStripMenuItem, 130 | this.documentationToolStripMenuItem, 131 | this.licenseToolStripMenuItem, 132 | this.createDesktopShortcutToolStripMenuItem}); 133 | this.toolStripMenuItem4.Name = "toolStripMenuItem4"; 134 | this.toolStripMenuItem4.Size = new System.Drawing.Size(180, 22); 135 | this.toolStripMenuItem4.Text = "Help and Info"; 136 | // 137 | // bSManagerVersionToolStripMenuItem 138 | // 139 | this.bSManagerVersionToolStripMenuItem.Name = "bSManagerVersionToolStripMenuItem"; 140 | this.bSManagerVersionToolStripMenuItem.Size = new System.Drawing.Size(200, 22); 141 | this.bSManagerVersionToolStripMenuItem.Text = "BSManager Version"; 142 | // 143 | // toolStripDebugLog 144 | // 145 | this.toolStripDebugLog.Name = "toolStripDebugLog"; 146 | this.toolStripDebugLog.Size = new System.Drawing.Size(200, 22); 147 | this.toolStripDebugLog.Text = "Debug Log"; 148 | this.toolStripDebugLog.Click += new System.EventHandler(this.toolStripDebugLog_Click); 149 | // 150 | // SteamVR_LH_ToolStripMenuItem 151 | // 152 | this.SteamVR_LH_ToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { 153 | this.SteamVR_DB_ToolStripMenuItem}); 154 | this.SteamVR_LH_ToolStripMenuItem.Name = "SteamVR_LH_ToolStripMenuItem"; 155 | this.SteamVR_LH_ToolStripMenuItem.Size = new System.Drawing.Size(200, 22); 156 | this.SteamVR_LH_ToolStripMenuItem.Text = "SteamVR LH DB"; 157 | // 158 | // SteamVR_DB_ToolStripMenuItem 159 | // 160 | this.SteamVR_DB_ToolStripMenuItem.Name = "SteamVR_DB_ToolStripMenuItem"; 161 | this.SteamVR_DB_ToolStripMenuItem.Size = new System.Drawing.Size(96, 22); 162 | this.SteamVR_DB_ToolStripMenuItem.Text = "N/D"; 163 | // 164 | // Pimax_LH_ToolStripMenuItem 165 | // 166 | this.Pimax_LH_ToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { 167 | this.Pimax_DB_ToolStripMenuItem}); 168 | this.Pimax_LH_ToolStripMenuItem.Name = "Pimax_LH_ToolStripMenuItem"; 169 | this.Pimax_LH_ToolStripMenuItem.Size = new System.Drawing.Size(200, 22); 170 | this.Pimax_LH_ToolStripMenuItem.Text = "Pimax LH DB"; 171 | // 172 | // Pimax_DB_ToolStripMenuItem 173 | // 174 | this.Pimax_DB_ToolStripMenuItem.Name = "Pimax_DB_ToolStripMenuItem"; 175 | this.Pimax_DB_ToolStripMenuItem.Size = new System.Drawing.Size(96, 22); 176 | this.Pimax_DB_ToolStripMenuItem.Text = "N/D"; 177 | // 178 | // documentationToolStripMenuItem 179 | // 180 | this.documentationToolStripMenuItem.Name = "documentationToolStripMenuItem"; 181 | this.documentationToolStripMenuItem.Size = new System.Drawing.Size(200, 22); 182 | this.documentationToolStripMenuItem.Text = "Documentation"; 183 | this.documentationToolStripMenuItem.Click += new System.EventHandler(this.documentationToolStripMenuItem_Click); 184 | // 185 | // licenseToolStripMenuItem 186 | // 187 | this.licenseToolStripMenuItem.Name = "licenseToolStripMenuItem"; 188 | this.licenseToolStripMenuItem.Size = new System.Drawing.Size(200, 22); 189 | this.licenseToolStripMenuItem.Text = "License"; 190 | this.licenseToolStripMenuItem.Click += new System.EventHandler(this.licenseToolStripMenuItem_Click); 191 | // 192 | // createDesktopShortcutToolStripMenuItem 193 | // 194 | this.createDesktopShortcutToolStripMenuItem.Name = "createDesktopShortcutToolStripMenuItem"; 195 | this.createDesktopShortcutToolStripMenuItem.Size = new System.Drawing.Size(200, 22); 196 | this.createDesktopShortcutToolStripMenuItem.Text = "Create desktop shortcut"; 197 | this.createDesktopShortcutToolStripMenuItem.Click += new System.EventHandler(this.createDesktopShortcutToolStripMenuItem_Click); 198 | // 199 | // quitToolStripMenuItem 200 | // 201 | this.quitToolStripMenuItem.Name = "quitToolStripMenuItem"; 202 | this.quitToolStripMenuItem.Size = new System.Drawing.Size(180, 22); 203 | this.quitToolStripMenuItem.Text = "Quit"; 204 | this.quitToolStripMenuItem.Click += new System.EventHandler(this.quitToolStripMenuItem_Click); 205 | // 206 | // disableProgressToastToolStripMenuItem 207 | // 208 | this.disableProgressToastToolStripMenuItem.Name = "disableProgressToastToolStripMenuItem"; 209 | this.disableProgressToastToolStripMenuItem.Size = new System.Drawing.Size(200, 22); 210 | this.disableProgressToastToolStripMenuItem.Text = "Disable Progress Toast"; 211 | this.disableProgressToastToolStripMenuItem.Click += new System.EventHandler(this.disableProgressToastToolStripMenuItem_Click); 212 | // 213 | // Form1 214 | // 215 | this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); 216 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 217 | this.ClientSize = new System.Drawing.Size(800, 450); 218 | this.Icon = global::BSManager.BSManagerRes.bsmanager_off; 219 | this.Name = "Form1"; 220 | this.Text = "BSManager"; 221 | this.WindowState = System.Windows.Forms.FormWindowState.Minimized; 222 | this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.Form1_FormClosing); 223 | this.FormClosed += new System.Windows.Forms.FormClosedEventHandler(this.Form1_FormClosed); 224 | this.Load += new System.EventHandler(this.Form1_Load); 225 | this.contextMenuStrip1.ResumeLayout(false); 226 | this.ResumeLayout(false); 227 | 228 | } 229 | 230 | #endregion 231 | 232 | private System.Windows.Forms.NotifyIcon notifyIcon1; 233 | private System.Windows.Forms.ContextMenuStrip contextMenuStrip1; 234 | private ToolStripMenuItem toolStripMenuItemBS; 235 | private ToolStripMenuItem ToolStripMenuItemDisco; 236 | private ToolStripMenuItem hMDToolStripMenuItem; 237 | private ToolStripMenuItem ToolStripMenuItemHmd; 238 | private ToolStripMenuItem toolStripRunAtStartup; 239 | private ToolStripMenuItem toolStripMenuItem4; 240 | private ToolStripMenuItem bSManagerVersionToolStripMenuItem; 241 | private ToolStripMenuItem Pimax_LH_ToolStripMenuItem; 242 | private ToolStripMenuItem Pimax_DB_ToolStripMenuItem; 243 | private ToolStripMenuItem documentationToolStripMenuItem; 244 | private ToolStripMenuItem licenseToolStripMenuItem; 245 | private ToolStripMenuItem createDesktopShortcutToolStripMenuItem; 246 | private ToolStripMenuItem toolStripDebugLog; 247 | private ToolStripMenuItem quitToolStripMenuItem; 248 | private ToolStripMenuItem RuntimeToolStripMenuItem; 249 | private ToolStripMenuItem SteamVR_LH_ToolStripMenuItem; 250 | private ToolStripMenuItem SteamVR_DB_ToolStripMenuItem; 251 | private ToolStripMenuItem disableProgressToastToolStripMenuItem; 252 | } 253 | } 254 | 255 | -------------------------------------------------------------------------------- /BSManagerMain.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using System.Windows.Forms; 7 | using System.Management; 8 | using System.Diagnostics; 9 | using Windows.Devices.Bluetooth; 10 | using Windows.Devices.Bluetooth.GenericAttributeProfile; 11 | using Windows.Devices.Bluetooth.Advertisement; 12 | using Windows.Storage.Streams; 13 | using System.Reflection; 14 | using System.Threading; 15 | using Microsoft.Win32; 16 | using System.IO; 17 | using Newtonsoft.Json; 18 | using Newtonsoft.Json.Linq; 19 | using IWshRuntimeLibrary; 20 | using AutoUpdaterDotNET; 21 | using System.Runtime.Serialization; 22 | using System.Timers; 23 | using System.ServiceProcess; 24 | using File = System.IO.File; 25 | using System.Text; 26 | using Microsoft.Toolkit.Uwp.Notifications; 27 | using System.IO.Packaging; 28 | using NUnit.Framework; 29 | using System.Globalization; 30 | 31 | namespace BSManager 32 | { 33 | public enum MsgSeverity 34 | { 35 | INFO, 36 | WARNING, 37 | ERROR 38 | } 39 | 40 | public partial class Form1 : Form 41 | 42 | { 43 | readonly ComponentResourceManager resources = new ComponentResourceManager(typeof(Form1)); 44 | 45 | static int bsCount = 0; 46 | 47 | static List bsSerials = new List(); 48 | static List sbsSerials = new List(); 49 | static List pbsSerials = new List(); 50 | 51 | static IEnumerable bsTokens; 52 | 53 | // Current data format 54 | static DataFormat _dataFormat = DataFormat.Hex; 55 | 56 | static string _versionInfo; 57 | 58 | static TimeSpan _timeout = TimeSpan.FromSeconds(5); 59 | 60 | static string steamvr_lhjson; 61 | static string pimax_lhjson; 62 | 63 | static bool slhfound = false; 64 | static bool plhfound = false; 65 | 66 | private HashSet _lighthouses = new HashSet(); 67 | private BluetoothLEAdvertisementWatcher watcher; 68 | private ManagementEventWatcher insertWatcher; 69 | private ManagementEventWatcher removeWatcher; 70 | 71 | private int _delayCmd = 500; 72 | 73 | private const string v2_ON = "01"; 74 | private const string v2_OFF = "00"; 75 | 76 | private readonly Guid v2_powerGuid = Guid.Parse("00001523-1212-efde-1523-785feabcd124"); 77 | private readonly Guid v2_powerCharacteristic = Guid.Parse("00001525-1212-efde-1523-785feabcd124"); 78 | 79 | private const string v1_ON = "12 00 00 28 FF FF FF FF 00 00 00 00 00 00 00 00 00 00 00 00"; 80 | private const string v1_OFF = "12 01 00 28 FF FF FF FF 00 00 00 00 00 00 00 00 00 00 00 00"; 81 | 82 | private readonly Guid v1_powerGuid = Guid.Parse("0000cb00-0000-1000-8000-00805f9b34fb"); 83 | private readonly Guid v1_powerCharacteristic = Guid.Parse("0000cb01-0000-1000-8000-00805f9b34fb"); 84 | 85 | private int _V2DoubleCheckMin = 5; 86 | private bool V2BaseStations = false; 87 | private bool V2BaseStationsVive = false; 88 | 89 | public bool HeadSetState = false; 90 | 91 | private static int processingCmdSync = 0; 92 | private static int processingLHSync = 0; 93 | 94 | private int ProcessLHtimerCycle = 1000; 95 | 96 | public Thread thrUSBDiscovery; 97 | public Thread thrProcessLH; 98 | 99 | private DateTime LastCmdStamp; 100 | private LastCmd LastCmdSent; 101 | 102 | System.Timers.Timer ProcessLHtimer = new System.Timers.Timer(); 103 | 104 | private static TextWriterTraceListener traceEx = new TextWriterTraceListener("BSManager_exceptions.log", "BSManagerEx"); 105 | private static TextWriterTraceListener traceDbg = new TextWriterTraceListener("BSManager.log", "BSManagerDbg"); 106 | 107 | private readonly string fnKillList = "BSManager.kill.txt"; 108 | private readonly string fnGraceList = "BSManager.grace.txt"; 109 | 110 | private string[] kill_list = new string[] { }; 111 | private string[] graceful_list = new string[] { "vrmonitor", "vrdashboard", "ReviveOverlay", "vrmonitor" }; 112 | private string[] cleanup_pilist = new string[] { "pi_server", "piservice", "pitool" }; 113 | 114 | private static bool debugLog = false; 115 | private static bool ManageRuntime = false; 116 | private static string RuntimePath = ""; 117 | private static bool LastManage = false; 118 | private static bool ShowProgressToast = true; 119 | private static bool SetProgressToast = true; 120 | 121 | protected List ptoastNotificationList = new List(); 122 | 123 | [System.Runtime.InteropServices.DllImportAttribute("user32.dll")] 124 | public static extern bool PostMessage(IntPtr handleWnd, UInt32 Msg, Int32 wParam, UInt32 lParam); 125 | 126 | const int WM_QUERYENDSESSION = 0x0011, 127 | WM_ENDSESSION = 0x0016, 128 | WM_TRUE = 0x1, 129 | WM_FALSE = 0x0; 130 | 131 | 132 | [System.Runtime.InteropServices.DllImportAttribute("user32.dll", EntryPoint = "FindWindowEx")] 133 | public static extern int FindWindowEx(int hwndParent, int hwndEnfant, int lpClasse, string lpTitre); 134 | 135 | [System.Runtime.InteropServices.DllImportAttribute("user32.dll")] 136 | static extern int GetWindowThreadProcessId(IntPtr hWnd, out int processId); 137 | 138 | [System.Runtime.InteropServices.DllImportAttribute("user32.dll")] 139 | public static extern IntPtr FindWindowEx(IntPtr parentWindow, IntPtr previousChildWindow, string windowClass, string windowTitle); 140 | 141 | public Form1() 142 | { 143 | LogLine($"[BSMANAGER] FORM INIT "); 144 | 145 | InitializeComponent(); 146 | 147 | Application.ApplicationExit += delegate { notifyIcon1.Dispose(); }; 148 | } 149 | 150 | private void Form1_Load(object sender, EventArgs e) 151 | { 152 | 153 | try 154 | { 155 | Trace.AutoFlush = true; 156 | 157 | this.Hide(); 158 | 159 | var name = Assembly.GetExecutingAssembly().GetName(); 160 | _versionInfo = string.Format($"{name.Version.Major:0}.{name.Version.Minor:0}.{name.Version.Build:0}"); 161 | 162 | LogLine($"[BSMANAGER] STARTED "); 163 | LogLine($"[BSMANAGER] Version: {_versionInfo}"); 164 | 165 | FindRuntime(); 166 | 167 | using (RegistryKey registrySettingsCheck = Registry.CurrentUser.OpenSubKey("SOFTWARE\\ManniX\\BSManager", true)) 168 | { 169 | 170 | RegistryKey registrySettings; 171 | 172 | if (registrySettingsCheck == null) 173 | { 174 | registrySettings = Registry.CurrentUser.CreateSubKey 175 | ("SOFTWARE\\ManniX\\BSManager"); 176 | } 177 | 178 | registrySettings = Registry.CurrentUser.OpenSubKey("SOFTWARE\\ManniX\\BSManager", true); 179 | 180 | if (registrySettings.GetValue("DebugLog") == null) 181 | { 182 | toolStripDebugLog.Checked = false; 183 | debugLog = false; 184 | LogLine($"[BSMANAGER] Debug Log disabled"); 185 | } 186 | else 187 | { 188 | toolStripDebugLog.Checked = true; 189 | debugLog = true; 190 | LogLine($"[BSMANAGER] Debug Log enabled"); 191 | } 192 | 193 | if (registrySettings.GetValue("ManageRuntime") == null) 194 | { 195 | RuntimeToolStripMenuItem.Checked = false; 196 | ManageRuntime = false; 197 | LogLine($"[BSMANAGER] Manage Runtime disabled"); 198 | } 199 | else 200 | { 201 | RuntimeToolStripMenuItem.Checked = true; 202 | ManageRuntime = true; 203 | LogLine($"[BSMANAGER] Manage Runtime enabled"); 204 | } 205 | 206 | if (registrySettings.GetValue("ShowProgressToast") == null) 207 | { 208 | disableProgressToastToolStripMenuItem.Checked = true; 209 | SetProgressToast = false; 210 | ShowProgressToast = false; 211 | LogLine($"[BSMANAGER] Progress Toast disabled"); 212 | } 213 | else 214 | { 215 | disableProgressToastToolStripMenuItem.Checked = false; 216 | SetProgressToast = true; 217 | ShowProgressToast = true; 218 | LogLine($"[BSMANAGER] Progress Toast enabled"); 219 | } 220 | } 221 | 222 | AutoUpdater.ReportErrors = false; 223 | AutoUpdater.InstalledVersion = new Version(_versionInfo); 224 | AutoUpdater.DownloadPath = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName); 225 | AutoUpdater.RunUpdateAsAdmin = false; 226 | AutoUpdater.Synchronous = true; 227 | AutoUpdater.ParseUpdateInfoEvent += AutoUpdaterOnParseUpdateInfoEvent; 228 | AutoUpdater.Start("https://raw.githubusercontent.com/mann1x/BSManager/master/BSManager/AutoUpdaterBSManager.json"); 229 | 230 | bSManagerVersionToolStripMenuItem.Text = "BSManager Version " + _versionInfo; 231 | 232 | using (RegistryKey registryStart = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", true)) 233 | { 234 | string _curpath = registryStart.GetValue("BSManager").ToString(); 235 | if (_curpath == null) 236 | { 237 | toolStripRunAtStartup.Checked = false; 238 | } 239 | else 240 | { 241 | if (_curpath != MyExecutableWithPath) registryStart.SetValue("BSManager", MyExecutableWithPath); 242 | toolStripRunAtStartup.Checked = true; 243 | } 244 | } 245 | 246 | string [] _glist = null; 247 | string [] _klist = null; 248 | 249 | _glist = ProcListLoad(fnGraceList, "graceful"); 250 | _klist = ProcListLoad(fnKillList, "immediate"); 251 | 252 | if (_glist != null) graceful_list = _glist; 253 | if (_klist != null) kill_list = _klist; 254 | 255 | _glist = null; _klist = null; 256 | 257 | slhfound = Read_SteamVR_config(); 258 | if (!slhfound) 259 | { 260 | SteamVR_DB_ToolStripMenuItem.Text = "SteamVR DB not found in registry"; 261 | } 262 | else 263 | { 264 | slhfound = Load_LH_DB("SteamVR" ); 265 | if (!slhfound) { SteamVR_DB_ToolStripMenuItem.Text = "SteamVR DB file parse error"; } 266 | else 267 | { 268 | SteamVR_DB_ToolStripMenuItem.Text = "Serials:"; 269 | foreach (string bs in sbsSerials) 270 | { 271 | SteamVR_LH_ToolStripMenuItem.DropDownItems.Add(bs); 272 | } 273 | } 274 | } 275 | 276 | plhfound = Read_Pimax_config(); 277 | if (!plhfound) 278 | { 279 | Pimax_DB_ToolStripMenuItem.Text = "Pimax DB not found"; 280 | } 281 | else 282 | { 283 | plhfound = Load_LH_DB("Pimax"); 284 | if (!plhfound) { Pimax_DB_ToolStripMenuItem.Text = "Pimax DB file parse error"; } 285 | else 286 | { 287 | Pimax_DB_ToolStripMenuItem.Text = "Serials:"; 288 | foreach (string bs in pbsSerials) 289 | { 290 | Pimax_LH_ToolStripMenuItem.DropDownItems.Add(bs); 291 | } 292 | } 293 | } 294 | 295 | WqlEventQuery insertQuery = new WqlEventQuery("SELECT * FROM __InstanceCreationEvent WITHIN 2 WHERE TargetInstance ISA 'Win32_USBHub'"); 296 | 297 | insertWatcher = new ManagementEventWatcher(insertQuery); 298 | insertWatcher.EventArrived += new EventArrivedEventHandler(DeviceInsertedEvent); 299 | insertWatcher.Start(); 300 | 301 | WqlEventQuery removeQuery = new WqlEventQuery("SELECT * FROM __InstanceDeletionEvent WITHIN 2 WHERE TargetInstance ISA 'Win32_USBHub'"); 302 | removeWatcher = new ManagementEventWatcher(removeQuery); 303 | removeWatcher.EventArrived += new EventArrivedEventHandler(DeviceRemovedEvent); 304 | removeWatcher.Start(); 305 | 306 | watcher = new BluetoothLEAdvertisementWatcher(); 307 | watcher.Received += AdvertisementWatcher_Received; 308 | 309 | thrUSBDiscovery = new Thread(RunUSBDiscovery); 310 | thrUSBDiscovery.Start(); 311 | 312 | thrProcessLH = new Thread(RunProcessLH); 313 | 314 | while (true) 315 | { 316 | if (!thrUSBDiscovery.IsAlive) 317 | { 318 | LogLine("[LightHouse] Starting LightHouse Thread"); 319 | thrProcessLH.Start(); 320 | break; 321 | } 322 | } 323 | 324 | const string scheme = "pack"; 325 | if (!UriParser.IsKnownScheme(scheme)) 326 | { 327 | Assert.That(PackUriHelper.UriSchemePack, Is.EqualTo(scheme)); 328 | } 329 | 330 | // Listen to notification activation 331 | ToastNotificationManagerCompat.OnActivated += toastArgs => 332 | { 333 | // Obtain the arguments from the notification 334 | ToastArguments args = ToastArguments.Parse(toastArgs.Argument); 335 | // Clear the Toast Progress List 336 | if (args["conversationId"] == "9113") ptoastNotificationList.Clear(); 337 | }; 338 | 339 | } 340 | catch (Exception ex) 341 | { 342 | HandleEx(ex); 343 | } 344 | 345 | } 346 | 347 | private void HandleEx(Exception ex) 348 | { 349 | try { 350 | string _msg = ex.Message; 351 | if (ex.Source != string.Empty && ex.Source != null) _msg = $"{_msg} Source: {ex.Source}"; 352 | new ToastContentBuilder() 353 | .AddHeader("6789", "Exception raised", "") 354 | .AddText(_msg) 355 | .AddText(ex.StackTrace) 356 | .Show(toast => 357 | { 358 | toast.ExpirationTime = DateTime.Now.AddSeconds(360); 359 | }); 360 | LogLine($"{ex}"); 361 | traceEx.WriteLine($"[{DateTime.Now}] {ex}"); 362 | traceEx.Flush(); 363 | } 364 | catch (Exception e) 365 | { 366 | LogLine($"[HANDLEEX] Exception: {e}"); 367 | } 368 | 369 | } 370 | public static void LogLine(string msg) 371 | { 372 | Trace.WriteLine($"{msg}"); 373 | if (debugLog) { 374 | traceDbg.WriteLine($"[{DateTime.Now}] {msg}"); 375 | traceDbg.Flush(); 376 | } 377 | } 378 | 379 | public void BalloonMsg(string msg, string header = "BSManager") 380 | { 381 | try 382 | { 383 | new ToastContentBuilder() 384 | .AddText(header) 385 | .AddText(msg) 386 | .Show(toast => 387 | { 388 | toast.ExpirationTime = DateTime.Now.AddSeconds(120); 389 | }); 390 | Trace.WriteLine($"{msg}"); 391 | if (debugLog) 392 | { 393 | traceDbg.WriteLine($"[{DateTime.Now}] {msg}"); 394 | traceDbg.Flush(); 395 | } 396 | } 397 | catch (Exception e) 398 | { 399 | LogLine($"[BALLOONMSG] Exception: {e}"); 400 | } 401 | } 402 | 403 | private void timerManageRuntime() 404 | { 405 | Task.Delay(TimeSpan.FromMilliseconds(15000)) 406 | .ContinueWith(task => doManageRuntime()); 407 | } 408 | 409 | private IntPtr[] GetProcessWindows(int process) 410 | { 411 | IntPtr[] apRet = (new IntPtr[256]); 412 | int iCount = 0; 413 | IntPtr pLast = IntPtr.Zero; 414 | do 415 | { 416 | pLast = FindWindowEx(IntPtr.Zero, pLast, null, null); 417 | int iProcess_; 418 | GetWindowThreadProcessId(pLast, out iProcess_); 419 | if (iProcess_ == process) apRet[iCount++] = pLast; 420 | } while (pLast != IntPtr.Zero); 421 | System.Array.Resize(ref apRet, iCount); 422 | return apRet; 423 | } 424 | 425 | 426 | private void doManageRuntime() 427 | { 428 | try 429 | { 430 | 431 | void _pKill(Process _p2kill) 432 | { 433 | try 434 | { 435 | _p2kill.Kill(); 436 | } 437 | catch (InvalidOperationException) 438 | { 439 | LogLine($"[Manage Runtime] {_p2kill.ProcessName} has probably already exited"); 440 | } 441 | catch (AggregateException) 442 | { 443 | LogLine($"[Manage Runtime] {_p2kill.ProcessName} can't be killed: not all processes in the tree can be killed"); 444 | } 445 | catch (NotSupportedException) 446 | { 447 | LogLine($"[Manage Runtime] {_p2kill.ProcessName} can't be killed: operation not supported"); 448 | } 449 | catch (Win32Exception) 450 | { 451 | LogLine($"[Manage Runtime] {_p2kill.ProcessName} can't be killed: not enogh privileges or already exiting"); 452 | } 453 | } 454 | 455 | void _pClose(Process _p2close) 456 | { 457 | try 458 | { 459 | _p2close.CloseMainWindow(); 460 | } 461 | catch (InvalidOperationException) 462 | { 463 | string ProcessName = _p2close.ProcessName; 464 | LogLine($"[Manage Runtime] {ProcessName} has probably already exited"); 465 | } 466 | } 467 | 468 | 469 | void loopKill(string[] procnames, bool graceful) 470 | { 471 | foreach (string procname in procnames) 472 | { 473 | Process[] ProcsArray = Process.GetProcessesByName(procname); 474 | if (ProcsArray.Count() > 0) 475 | { 476 | foreach (Process Proc2Kill in ProcsArray) 477 | { 478 | string ProcessName = Proc2Kill.ProcessName; 479 | LogLine($"[Manage Runtime] Closing {ProcessName} with PID={Proc2Kill.Id}"); 480 | if (graceful) 481 | { 482 | _pClose(Proc2Kill); 483 | } 484 | else 485 | { 486 | _pKill(Proc2Kill); 487 | } 488 | for (int i = 0; i < 20; i++) 489 | { 490 | if (!Proc2Kill.HasExited) 491 | { 492 | Thread.Sleep(250); 493 | Proc2Kill.Refresh(); 494 | Thread.Sleep(250); 495 | } 496 | else 497 | { 498 | break; 499 | } 500 | } 501 | if (!Proc2Kill.HasExited) 502 | { 503 | _pKill(Proc2Kill); 504 | Thread.Sleep(250); 505 | Proc2Kill.Refresh(); 506 | Thread.Sleep(250); 507 | if (!Proc2Kill.HasExited) 508 | { 509 | IntPtr[] wnd = GetProcessWindows(Int32.Parse((Proc2Kill.Id).ToString())); 510 | var wm_ret = PostMessage(wnd[0], WM_ENDSESSION, WM_TRUE, 0x80000000); 511 | Thread.Sleep(1000); 512 | if (!Proc2Kill.HasExited) 513 | { 514 | LogLine($"[Manage Runtime] {ProcessName} can't be killed, still running"); 515 | } 516 | } 517 | else 518 | { 519 | LogLine($"[Manage Runtime] {ProcessName} killed"); 520 | Proc2Kill.Close(); 521 | Proc2Kill.Dispose(); 522 | } 523 | } 524 | else 525 | { 526 | LogLine($"[Manage Runtime] {ProcessName} killed"); 527 | Proc2Kill.Close(); 528 | Proc2Kill.Dispose(); 529 | } 530 | } 531 | } 532 | else 533 | { 534 | LogLine($"[Manage Runtime] {procname} can't be killed: not found"); 535 | } 536 | ProcsArray = null; 537 | } 538 | } 539 | 540 | 541 | if (ManageRuntime) { 542 | if (!HeadSetState && LastManage) { 543 | 544 | #if DEBUG 545 | 546 | Process[] localAll = Process.GetProcesses(); 547 | foreach (Process processo in localAll) 548 | { 549 | LogLine($"[PROCESSES] Active: {processo.ProcessName} PID={processo.Id}"); 550 | } 551 | 552 | #endif 553 | ServiceController sc = new ServiceController("PiServiceLauncher"); 554 | LogLine($"[Manage Runtime] PiService is currently: {sc.Status}"); 555 | 556 | if ((sc.Status.Equals(ServiceControllerStatus.Running)) || 557 | (sc.Status.Equals(ServiceControllerStatus.StartPending))) 558 | { 559 | LogLine($"[Manage Runtime] Stopping PiService"); 560 | sc.Stop(); 561 | sc.Refresh(); 562 | LogLine($"[Manage Runtime] PiService is now: {sc.Status}"); 563 | } 564 | 565 | loopKill(cleanup_pilist, false); 566 | 567 | if (graceful_list.Length > 0) 568 | loopKill(graceful_list, true); 569 | 570 | if (kill_list.Length > 0) 571 | loopKill(kill_list, false); 572 | 573 | LastManage = false; 574 | 575 | } 576 | else if (HeadSetState && !LastManage) 577 | { 578 | Process[] PiToolArray = Process.GetProcessesByName("Pitool"); 579 | LogLine($"[Manage Runtime] Found {PiToolArray.Count()} PiTool running"); 580 | 581 | if (PiToolArray.Count() == 0) 582 | { 583 | ProcessStartInfo startInfo = new ProcessStartInfo(RuntimePath + "\\Pitool.exe", "hide"); 584 | startInfo.WindowStyle = ProcessWindowStyle.Minimized; 585 | Process PiTool = Process.Start(startInfo); 586 | LogLine($"[Manage Runtime] Started PiTool ({RuntimePath + "\\Pitool.exe"}) with PID={PiTool.Id}"); 587 | } 588 | 589 | LastManage = true; 590 | } 591 | } 592 | } 593 | catch (Exception e) when (e is Win32Exception || e is FileNotFoundException) 594 | { 595 | LogLine($"[Manage Runtime] The following exception was raised: {e}"); 596 | } 597 | } 598 | 599 | private void USBDiscovery() 600 | { 601 | try 602 | { 603 | ManagementObjectCollection collection; 604 | using (var searcher = new ManagementObjectSearcher(@"Select * From Win32_USBHub")) 605 | collection = searcher.Get(); 606 | 607 | foreach (var device in collection) 608 | { 609 | string did = (string)device.GetPropertyValue("DeviceID"); 610 | 611 | LogLine($"[USB Discovery] DID={did}"); 612 | 613 | CheckHMDOn(did); 614 | 615 | } 616 | 617 | collection.Dispose(); 618 | 619 | return; 620 | } 621 | catch (Exception ex) 622 | { 623 | HandleEx(ex); 624 | } 625 | } 626 | 627 | private void CheckHMDOn(string did) 628 | { 629 | try 630 | { 631 | string _hmd = ""; 632 | string action = "ON"; 633 | 634 | if (did.Contains("VID_0483&PID_0101")) _hmd = "PIMAX HMD"; 635 | if (did.Contains("VID_2996&PID_0309")) _hmd = "VIVE PRO HMD"; 636 | 637 | if (_hmd.Length > 0) 638 | { 639 | if (SetProgressToast) ShowProgressToast = true; 640 | LogLine($"[HMD] ## {_hmd} {action} "); 641 | ChangeHMDStrip($" {_hmd} {action} ", true); 642 | this.notifyIcon1.Icon = BSManagerRes.bsmanager_on; 643 | HeadSetState = true; 644 | Task.Delay(TimeSpan.FromMilliseconds(5000)) 645 | .ContinueWith(task => checkLHState(lh => !lh.PoweredOn, true)); 646 | LogLine($"[HMD] Runtime {action}: ManageRuntime is {ManageRuntime}"); 647 | timerManageRuntime(); 648 | } 649 | } 650 | catch (Exception ex) 651 | { 652 | HandleEx(ex); 653 | } 654 | } 655 | 656 | private void checkLHState(Func lighthousePredicate, bool hs_state) 657 | { 658 | if (HeadSetState == hs_state) { 659 | var results = _lighthouses.Where(lighthousePredicate); 660 | if (results.Any()) 661 | { 662 | foreach (Lighthouse lh in _lighthouses) 663 | { 664 | lh.ProcessDone = false; 665 | } 666 | } 667 | } 668 | } 669 | 670 | 671 | private void CheckHMDOff(string did) 672 | { 673 | try 674 | { 675 | string _hmd = ""; 676 | string action = "OFF"; 677 | 678 | if (did.Contains("VID_0483&PID_0101")) _hmd = "PIMAX HMD"; 679 | if (did.Contains("VID_2996&PID_0309")) _hmd = "VIVE PRO HMD"; 680 | 681 | if (_hmd.Length > 0) 682 | { 683 | if (SetProgressToast) ShowProgressToast = true; 684 | LogLine($"[HMD] ## {_hmd} {action} "); 685 | ChangeHMDStrip($" {_hmd} {action} ", false); 686 | this.notifyIcon1.Icon = BSManagerRes.bsmanager_off; 687 | HeadSetState = false; 688 | Task.Delay(TimeSpan.FromMilliseconds(5000)) 689 | .ContinueWith(task => checkLHState(lh => lh.PoweredOn, false)); 690 | LogLine($"[HMD] Runtime {action}: ManageRuntime is {ManageRuntime}"); 691 | timerManageRuntime(); 692 | } 693 | } 694 | catch (Exception ex) 695 | { 696 | HandleEx(ex); 697 | } 698 | 699 | } 700 | 701 | 702 | private void DeviceInsertedEvent(object sender, EventArrivedEventArgs e) 703 | { 704 | try 705 | { 706 | ManagementBaseObject instance = (ManagementBaseObject)e.NewEvent["TargetInstance"]; 707 | 708 | foreach (var property in instance.Properties) 709 | { 710 | if (property.Name == "PNPDeviceID") 711 | { 712 | CheckHMDOn(property.Value.ToString()); 713 | } 714 | //LogLine($" INSERTED " + property.Name + " = " + property.Value); 715 | } 716 | e.NewEvent.Dispose(); 717 | } 718 | catch (Exception ex) 719 | { 720 | HandleEx(ex); 721 | } 722 | } 723 | 724 | private void DeviceRemovedEvent(object sender, EventArrivedEventArgs e) 725 | { 726 | try { 727 | ManagementBaseObject instance = (ManagementBaseObject)e.NewEvent["TargetInstance"]; 728 | foreach (var property in instance.Properties) 729 | { 730 | if (property.Name == "PNPDeviceID") 731 | { 732 | CheckHMDOff(property.Value.ToString()); 733 | } 734 | //LogLine($" REMOVED " + property.Name + " = " + property.Value); 735 | } 736 | e.NewEvent.Dispose(); 737 | } 738 | catch (Exception ex) 739 | { 740 | HandleEx(ex); 741 | } 742 | } 743 | 744 | private void ProcessLH_ElapsedEventHandler(object sender, ElapsedEventArgs e) 745 | { 746 | int sync = Interlocked.CompareExchange(ref processingLHSync, 1, 0); 747 | if (sync == 0) 748 | { 749 | OnProcessLH(sender, e); 750 | processingLHSync = 0; 751 | } 752 | } 753 | 754 | public void ProcessWatcher(bool start) 755 | { 756 | if (start) 757 | { 758 | if (watcher.Status == BluetoothLEAdvertisementWatcherStatus.Stopped || watcher.Status == BluetoothLEAdvertisementWatcherStatus.Created) 759 | { 760 | ptoastNotificationList.Clear(); 761 | LogLine($"[LightHouse] Starting BLE Watcher Status: {watcher.Status}"); 762 | watcher.Start(); 763 | Thread.Sleep(250); 764 | LogLine($"[LightHouse] Started BLE Watcher Status: {watcher.Status}"); 765 | } 766 | } 767 | else 768 | { 769 | if (watcher.Status == BluetoothLEAdvertisementWatcherStatus.Started && watcher.Status != BluetoothLEAdvertisementWatcherStatus.Stopping) 770 | { 771 | LogLine($"[LightHouse] Stopping BLE Watcher Status: {watcher.Status}"); 772 | watcher.Stop(); 773 | Thread.Sleep(250); 774 | LogLine($"[LightHouse] Stopped BLE Watcher Status: {watcher.Status}"); 775 | ptoastNotificationList.Clear(); 776 | } 777 | } 778 | } 779 | 780 | public void OnProcessLH(object sender, ElapsedEventArgs args) 781 | { 782 | try 783 | { 784 | bool _done = true; 785 | 786 | if (V2BaseStationsVive && LastCmdSent == LastCmd.SLEEP && !HeadSetState) 787 | { 788 | TimeSpan _delta = DateTime.Now - LastCmdStamp; 789 | 790 | //LogLine($"LastCmdSent {LastCmdSent} _delta {_delta}"); 791 | 792 | if (_delta.Minutes >= _V2DoubleCheckMin) 793 | { 794 | ShowProgressToast = false; 795 | foreach (Lighthouse lh in _lighthouses) 796 | { 797 | lh.ProcessDone = false; 798 | } 799 | } 800 | } 801 | 802 | foreach (Lighthouse _lh in _lighthouses) 803 | { 804 | if (_lh.ProcessDone == false) _done = false; 805 | } 806 | 807 | if (_lighthouses.Count == 0 || _lighthouses.Count < bsCount) 808 | { 809 | ProcessWatcher(true); 810 | } 811 | else if (_done) 812 | { 813 | ProcessWatcher(false); 814 | } 815 | else 816 | { 817 | ProcessWatcher(true); 818 | } 819 | Thread.Sleep(ProcessLHtimerCycle); 820 | } 821 | catch (Exception ex) 822 | { 823 | HandleEx(ex); 824 | } 825 | } 826 | 827 | 828 | void RunUSBDiscovery() 829 | { 830 | USBDiscovery(); 831 | } 832 | 833 | void RunProcessLH() 834 | { 835 | ProcessLHtimer.Interval = ProcessLHtimerCycle; 836 | ProcessLHtimer.Elapsed += new ElapsedEventHandler(ProcessLH_ElapsedEventHandler); 837 | ProcessLHtimer.Start(); 838 | } 839 | 840 | private void AutoUpdaterOnParseUpdateInfoEvent(ParseUpdateInfoEventArgs args) 841 | { 842 | dynamic json = JsonConvert.DeserializeObject(args.RemoteData); 843 | args.UpdateInfo = new UpdateInfoEventArgs 844 | { 845 | CurrentVersion = json.version, 846 | ChangelogURL = json.changelog, 847 | DownloadURL = json.url, 848 | Mandatory = new Mandatory 849 | { 850 | Value = json.mandatory.value, 851 | UpdateMode = json.mandatory.mode, 852 | MinimumVersion = json.mandatory.minVersion 853 | }, 854 | CheckSum = new CheckSum 855 | { 856 | Value = json.checksum.value, 857 | HashingAlgorithm = json.checksum.hashingAlgorithm 858 | } 859 | }; 860 | } 861 | private void ChangeHMDStrip(string label, bool _checked) 862 | { 863 | try 864 | { 865 | BeginInvoke((MethodInvoker)delegate { 866 | ToolStripMenuItemHmd.Text = label; 867 | ToolStripMenuItemHmd.Checked = _checked; 868 | }); 869 | } 870 | catch (Exception ex) 871 | { 872 | HandleEx(ex); 873 | } 874 | } 875 | 876 | private void ChangeDiscoMsg(string count, string nameBS) 877 | { 878 | try 879 | { 880 | BeginInvoke((MethodInvoker)delegate { 881 | ToolStripMenuItemDisco.Text = $"Discovered: {count}/{bsCount}"; 882 | toolStripMenuItemBS.DropDownItems.Add(nameBS); 883 | }); 884 | 885 | 886 | 887 | } 888 | catch (Exception ex) 889 | { 890 | HandleEx(ex); 891 | } 892 | } 893 | private void ChangeBSMsg(string _name, bool _poweredOn, LastCmd _lastCmd, Action _action) 894 | { 895 | try 896 | { 897 | string _cmdStatus = ""; 898 | string _actionStatus = ""; 899 | switch (_lastCmd) 900 | { 901 | case LastCmd.ERROR: 902 | _cmdStatus = "[ERROR] "; 903 | break; 904 | default: 905 | _cmdStatus = ""; 906 | break; 907 | } 908 | switch (_action) 909 | { 910 | case Action.WAKEUP: 911 | _actionStatus = " - Going to Wakeup"; 912 | break; 913 | case Action.SLEEP: 914 | _actionStatus = " - Going to Standby"; 915 | break; 916 | default: 917 | _actionStatus = ""; 918 | break; 919 | } 920 | 921 | BeginInvoke((MethodInvoker)delegate { 922 | foreach (ToolStripMenuItem item in toolStripMenuItemBS.DropDownItems) 923 | { 924 | if (item.Text.StartsWith(_name)) 925 | { 926 | if (_poweredOn) item.Image = BSManagerRes.bsmanager_on.ToBitmap(); 927 | if (!_poweredOn) item.Image = null; 928 | item.Text = $"{_name} {_cmdStatus}{_actionStatus}"; 929 | } 930 | } 931 | 932 | }); 933 | 934 | 935 | 936 | } 937 | catch (Exception ex) 938 | { 939 | HandleEx(ex); 940 | } 941 | } 942 | 943 | private bool Read_SteamVR_config() 944 | { 945 | try 946 | { 947 | steamvr_lhjson = string.Empty; 948 | using (RegistryKey key = Registry.LocalMachine.OpenSubKey("Software\\WOW6432Node\\Valve\\Steam")) 949 | { 950 | if (key != null) 951 | { 952 | Object o = key.GetValue("InstallPath"); 953 | if (o != null) 954 | { 955 | 956 | steamvr_lhjson = o.ToString() + "\\config\\lighthouse\\lighthousedb.json"; 957 | if (File.Exists(steamvr_lhjson)) 958 | { 959 | LogLine($"[CONFIG] Found SteamVR LH DB at Path={steamvr_lhjson}"); 960 | return true; 961 | } 962 | else 963 | { 964 | LogLine($"[CONFIG] Not found SteamVR LH DB at Path={steamvr_lhjson}"); 965 | return false; 966 | } 967 | } 968 | } 969 | return false; 970 | } 971 | } 972 | catch (Exception ex) 973 | { 974 | HandleEx(ex); 975 | return false; 976 | } 977 | } 978 | 979 | private bool Read_Pimax_config() 980 | { 981 | try 982 | { 983 | pimax_lhjson = string.Empty; 984 | string ProgramDataFolder = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData); 985 | pimax_lhjson = ProgramDataFolder + "\\pimax\\runtime\\config\\lighthouse\\lighthousedb.json"; 986 | if (File.Exists(pimax_lhjson)) 987 | { 988 | LogLine($"[CONFIG] Found Pimax LH DB at Path={pimax_lhjson}"); 989 | return true; 990 | } 991 | else 992 | { 993 | LogLine($"[CONFIG] Not found Pimax LH DB at Path={pimax_lhjson}"); 994 | return false; 995 | } 996 | } 997 | catch (Exception ex) 998 | { 999 | HandleEx(ex); 1000 | return false; 1001 | } 1002 | } 1003 | 1004 | private bool Load_LH_DB(string db_name) 1005 | { 1006 | try 1007 | { 1008 | string _lhjson = string.Empty; 1009 | if (db_name == "Pimax") 1010 | { 1011 | _lhjson = pimax_lhjson; 1012 | } 1013 | else 1014 | { 1015 | _lhjson = steamvr_lhjson; 1016 | } 1017 | using (StreamReader r = new StreamReader(_lhjson)) 1018 | { 1019 | string json = r.ReadToEnd(); 1020 | LogLine($"[CONFIG] SteamDB JSON Length={json.Length}"); 1021 | JObject o = JObject.Parse(json); 1022 | LogLine($"[CONFIG] SteamDB JSON Parsed"); 1023 | 1024 | bsTokens = o.SelectTokens("$..base_serial_number"); 1025 | 1026 | int _maxbs = 6; 1027 | int _curbs = 1; 1028 | int _bsCount = 0; 1029 | 1030 | foreach (JToken bsitem in bsTokens) 1031 | { 1032 | if (!bsSerials.Contains(bsitem.ToString())) bsSerials.Add(bsitem.ToString()); 1033 | if (db_name == "Pimax") 1034 | { 1035 | if (!pbsSerials.Contains(bsitem.ToString())) pbsSerials.Add(bsitem.ToString()); 1036 | _bsCount = pbsSerials.Count; 1037 | } 1038 | else 1039 | { 1040 | if (!sbsSerials.Contains(bsitem.ToString())) sbsSerials.Add(bsitem.ToString()); 1041 | _bsCount = sbsSerials.Count; 1042 | } 1043 | 1044 | LogLine($"[CONFIG] {db_name} DB Base Station Serial={bsitem}"); 1045 | _curbs++; 1046 | if (_curbs > _maxbs) break; 1047 | } 1048 | 1049 | LogLine($"[CONFIG] {db_name} DB Base Stations List=" + string.Join(", ", bsSerials)); 1050 | 1051 | bsCount = bsSerials.Count(); 1052 | 1053 | LogLine($"[CONFIG] {db_name} DB Base Stations count: {_bsCount}"); 1054 | 1055 | return true; 1056 | 1057 | } 1058 | } 1059 | catch (Exception ex) 1060 | { 1061 | HandleEx(ex); 1062 | return false; 1063 | } 1064 | } 1065 | 1066 | private void AdvertisementWatcher_Received(BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementReceivedEventArgs args) 1067 | { 1068 | try { 1069 | 1070 | //Trace.WriteLine($"Advertisment: {args.Advertisement.LocalName}"); 1071 | 1072 | if (!args.Advertisement.LocalName.StartsWith("LHB-") && !args.Advertisement.LocalName.StartsWith("HTC BS ")) 1073 | { 1074 | return; 1075 | } 1076 | 1077 | //Trace.WriteLine($"Advertisment: {args.Advertisement.LocalName}"); 1078 | 1079 | var existing = _lighthouses.SingleOrDefault(lh => lh.Address == args.BluetoothAddress); 1080 | 1081 | if (existing == null) 1082 | { 1083 | LogLine($"[LightHouse] Found lighthouse {args.Advertisement.LocalName}"); 1084 | 1085 | existing = new Lighthouse(args.Advertisement.LocalName, args.BluetoothAddress); 1086 | _lighthouses.Add(existing); 1087 | ChangeDiscoMsg(_lighthouses.Count.ToString(), existing.Name); 1088 | } 1089 | 1090 | int intpstate = 0; 1091 | 1092 | if (args.Advertisement.LocalName.StartsWith("LHB-")) 1093 | { 1094 | var valveData = args.Advertisement.GetManufacturerDataByCompanyId(0x055D); 1095 | var htcData = args.Advertisement.GetManufacturerDataByCompanyId(0x02ED); 1096 | 1097 | if (valveData.Count > 0) { 1098 | 1099 | existing.Manufacturer = BSManufacturer.VIVE; 1100 | 1101 | var valveDataSingle = valveData.Single(); 1102 | var data = new byte[valveDataSingle.Data.Length]; 1103 | 1104 | using (var reader = DataReader.FromBuffer(valveDataSingle.Data)) 1105 | { 1106 | reader.ReadBytes(data); 1107 | } 1108 | 1109 | if (!string.IsNullOrEmpty(data[4].ToString())) 1110 | { 1111 | intpstate = Int32.Parse(data[4].ToString()); 1112 | existing.V2PoweredOn = intpstate > 0; 1113 | 1114 | //existing.PoweredOn = data[4] == 0x03; 1115 | //LogLine($"{existing.Name} power status {intpstate} last {existing.lastPowerState} PoweredOn={existing.PoweredOn}"); 1116 | } 1117 | 1118 | V2BaseStationsVive = true; 1119 | } 1120 | else if (htcData.Count > 0) 1121 | { 1122 | var htcDataSingle = htcData.Single(); 1123 | var data = new byte[htcDataSingle.Data.Length]; 1124 | 1125 | 1126 | using (var reader = DataReader.FromBuffer(htcDataSingle.Data)) 1127 | { 1128 | reader.ReadBytes(data); 1129 | } 1130 | 1131 | 1132 | if (!string.IsNullOrEmpty(data[4].ToString())) 1133 | { 1134 | 1135 | intpstate = Int32.Parse(data[4].ToString()); 1136 | existing.V2PoweredOn = intpstate > 0; 1137 | 1138 | //existing.PoweredOn = data[4] == 0x03; 1139 | //LogLine($"{existing.Name} power status {intpstate} last {existing.lastPowerState} PoweredOn={existing.PoweredOn}"); 1140 | } 1141 | } 1142 | 1143 | V2BaseStations = true; 1144 | existing.V2 = true; 1145 | 1146 | if (existing.V2PoweredOn && existing.LastCmd == LastCmd.SLEEP && !HeadSetState && existing.Manufacturer == BSManufacturer.VIVE) 1147 | { 1148 | TimeSpan _delta = DateTime.Now - existing.LastCmdStamp; 1149 | if (_delta.Minutes >= _V2DoubleCheckMin) 1150 | { 1151 | if (0 == Interlocked.Exchange(ref processingCmdSync, 1)) 1152 | { 1153 | ShowProgressToast = false; 1154 | LogLine($"[LightHouse] Processing SLEEP check {_V2DoubleCheckMin} minutes still ON for: {existing.Name}"); 1155 | ProcessLighthouseAsync(existing, "SLEEP"); 1156 | existing.ProcessDone = true; 1157 | } 1158 | } 1159 | else 1160 | { 1161 | existing.ProcessDone = true; 1162 | } 1163 | } 1164 | 1165 | } 1166 | else 1167 | { 1168 | existing.V2 = false; 1169 | } 1170 | 1171 | 1172 | if (HeadSetState) 1173 | { 1174 | if (existing.V2 && (existing.LastCmd == LastCmd.NONE) && existing.PoweredOn) 1175 | { 1176 | ChangeBSMsg(existing.Name, true, LastCmd.WAKEUP, Action.NONE); 1177 | return; 1178 | } 1179 | if (existing.LastCmd != LastCmd.WAKEUP) 1180 | { 1181 | if (0 == Interlocked.Exchange(ref processingCmdSync, 1)) 1182 | { 1183 | ProcessLighthouseAsync(existing, "WAKEUP"); 1184 | } 1185 | } 1186 | } 1187 | else 1188 | { 1189 | if (existing.V2 && (existing.LastCmd == LastCmd.NONE) && !existing.PoweredOn) 1190 | { 1191 | ChangeBSMsg(existing.Name, false, LastCmd.SLEEP, Action.NONE); 1192 | return; 1193 | } 1194 | if (existing.LastCmd != LastCmd.SLEEP) 1195 | { 1196 | if (0 == Interlocked.Exchange(ref processingCmdSync, 1)) 1197 | { 1198 | ProcessLighthouseAsync(existing, "SLEEP"); 1199 | } 1200 | } 1201 | } 1202 | 1203 | Thread.Sleep(100); 1204 | } 1205 | catch (Exception ex) 1206 | { 1207 | HandleEx(ex); 1208 | } 1209 | 1210 | } 1211 | 1212 | private void ProcessLighthouseAsync(Lighthouse lh, string command) 1213 | { 1214 | try 1215 | { 1216 | void exitProcess(string msg) 1217 | { 1218 | throw new ProcessError($"{msg}"); 1219 | } 1220 | 1221 | var progressdec = CultureInfo.InvariantCulture.Clone() as CultureInfo; 1222 | progressdec.NumberFormat.NumberDecimalSeparator = "."; 1223 | string _toastAction = (command == "WAKEUP") ? "Waking up" : "Set to sleep"; 1224 | uint pidx = 1; 1225 | string ptag = "LHProcess"; 1226 | string pgroup = "LHProcess"; 1227 | 1228 | int _leftDone = 0; 1229 | foreach (Lighthouse lhDone in _lighthouses) { 1230 | if (lhDone.ProcessDone) _leftDone++; 1231 | } 1232 | 1233 | lh.OpsTotal++; 1234 | 1235 | int _doneCount=_leftDone+1; 1236 | 1237 | var pcontent = new ToastContentBuilder() 1238 | .AddArgument("action", "viewConversation") 1239 | .AddArgument("conversationId", 9113) 1240 | .AddText("Commandeering the Base Stations...") 1241 | .AddAudio(null,null,true) 1242 | .AddVisualChild(new AdaptiveProgressBar() 1243 | { 1244 | Title = new BindableString("title"), 1245 | Value = new BindableProgressBarValue("progressValue"), 1246 | ValueStringOverride = new BindableString("pregressCount"), 1247 | Status = new BindableString("progressStatus") 1248 | }).GetToastContent(); 1249 | 1250 | int eRate = (int)Math.Round((double)(100 * lh.ErrorTotal) / lh.OpsTotal); 1251 | bool showeRate = (eRate > 10) ? true : false; 1252 | string bsmanuf = ""; 1253 | if (V2BaseStations) 1254 | bsmanuf = (lh.Manufacturer == BSManufacturer.HTC) ? " (by HTC)" : " (by VIVE)"; 1255 | 1256 | var ptoast = new Windows.UI.Notifications.ToastNotification(pcontent.GetXml()); 1257 | 1258 | if (ptoastNotificationList.Count == 0) 1259 | { 1260 | ptoast.Tag = ptag; 1261 | ptoast.Group = pgroup; 1262 | ptoast.Data = new Windows.UI.Notifications.NotificationData(); 1263 | ptoast.Data.Values["title"] = $"BS {lh.Name}{bsmanuf}"; 1264 | ptoast.Data.Values["progressValue"] = "0"; 1265 | ptoast.Data.Values["pregressCount"] = $"{_doneCount}/{bsCount}"; 1266 | ptoast.Data.Values["progressStatus"] = $"{_toastAction}... (0%)"; 1267 | ptoast.Data.SequenceNumber = pidx; 1268 | ptoast.ExpirationTime = DateTime.Now.AddSeconds(120); 1269 | 1270 | ptoastNotificationList.Add(ptoast); 1271 | if (ShowProgressToast) ToastNotificationManagerCompat.CreateToastNotifier().Show(ptoast); 1272 | } 1273 | else 1274 | { 1275 | ptoast = ptoastNotificationList[0]; 1276 | var initdata = new Windows.UI.Notifications.NotificationData 1277 | { 1278 | SequenceNumber = pidx++ 1279 | }; 1280 | initdata.Values["title"] = $"BS {lh.Name}{bsmanuf}"; 1281 | initdata.Values["progressValue"] = $"0"; 1282 | initdata.Values["pregressCount"] = $"{_doneCount}/{bsCount}"; 1283 | initdata.Values["progressStatus"] = $"{_toastAction}... (0%)"; 1284 | if (ShowProgressToast) ToastNotificationManagerCompat.CreateToastNotifier().Update(initdata, ptag, pgroup); 1285 | } 1286 | 1287 | void updateProgress(double percentage, string _msg = "Commandeering") 1288 | { 1289 | string _ptag = "LHProcess"; 1290 | string _pgroup = "LHProcess"; 1291 | var data = new Windows.UI.Notifications.NotificationData 1292 | { 1293 | SequenceNumber = pidx++ 1294 | }; 1295 | double _p = percentage / 100; 1296 | string _status = $"{_toastAction}... ({percentage}%)"; 1297 | if (showeRate) _status += $" [Errors {eRate}%]"; 1298 | data.Values["title"] = $"BS {lh.Name}{bsmanuf}"; 1299 | data.Values["progressValue"] = $"{_p.ToString(progressdec)}"; 1300 | data.Values["pregressCount"] = $"{_doneCount}/{bsCount}"; 1301 | data.Values["progressStatus"] = _status; 1302 | if (ShowProgressToast) ToastNotificationManagerCompat.CreateToastNotifier().Update(data, _ptag, _pgroup); 1303 | } 1304 | 1305 | 1306 | LogLine($"[{lh.Name}] START Processing command: {command}"); 1307 | 1308 | lh.Action = (command == "WAKEUP") ? Action.WAKEUP : Action.SLEEP; 1309 | 1310 | ChangeBSMsg(lh.Name, lh.PoweredOn, lh.LastCmd, lh.Action); 1311 | 1312 | Guid _powerServGuid = v1_powerGuid; 1313 | Guid _powerCharGuid = v1_powerCharacteristic; 1314 | 1315 | if (lh.V2) 1316 | { 1317 | _powerServGuid = v2_powerGuid; 1318 | _powerCharGuid = v2_powerCharacteristic; 1319 | } 1320 | //https://docs.microsoft.com/en-us/windows/uwp/devices-sensors/gatt-client 1321 | var potentialLighthouseTask = BluetoothLEDevice.FromBluetoothAddressAsync(lh.Address).AsTask(); 1322 | potentialLighthouseTask.Wait(); 1323 | 1324 | if (ShowProgressToast) updateProgress(10); 1325 | 1326 | Thread.Sleep(_delayCmd); 1327 | 1328 | if (!potentialLighthouseTask.IsCompletedSuccessfully || potentialLighthouseTask.Result == null) exitProcess($"Could not connect to lighthouse"); 1329 | 1330 | using var btDevice = potentialLighthouseTask.Result; 1331 | 1332 | if (ShowProgressToast) updateProgress(20); 1333 | 1334 | Thread.Sleep(_delayCmd); 1335 | 1336 | var gattServicesTask = btDevice.GetGattServicesAsync(BluetoothCacheMode.Uncached).AsTask(); 1337 | gattServicesTask.Wait(); 1338 | 1339 | if (ShowProgressToast) updateProgress(30); 1340 | 1341 | Thread.Sleep(_delayCmd); 1342 | 1343 | if (!gattServicesTask.IsCompletedSuccessfully || gattServicesTask.Result.Status != GattCommunicationStatus.Success) exitProcess($"Failed to get services"); 1344 | 1345 | LogLine($"[{lh.Name}] Got services: {gattServicesTask.Result.Services.Count}"); 1346 | 1347 | foreach (var _serv in gattServicesTask.Result.Services.ToArray()) 1348 | { 1349 | LogLine($"[{lh.Name}] Service Attr: {_serv.AttributeHandle} Uuid: {_serv.Uuid}"); 1350 | } 1351 | 1352 | using var service = gattServicesTask.Result.Services.SingleOrDefault(s => s.Uuid == _powerServGuid); 1353 | 1354 | if (ShowProgressToast) updateProgress(40); 1355 | 1356 | Thread.Sleep(_delayCmd); 1357 | 1358 | if (service == null) exitProcess($"Could not find power service"); 1359 | 1360 | LogLine($"[{lh.Name}] Found power service"); 1361 | 1362 | var powerCharacteristicsTask = service.GetCharacteristicsAsync(BluetoothCacheMode.Uncached).AsTask(); 1363 | powerCharacteristicsTask.Wait(); 1364 | 1365 | if (ShowProgressToast) updateProgress(50); 1366 | 1367 | Thread.Sleep(_delayCmd); 1368 | 1369 | if (!powerCharacteristicsTask.IsCompletedSuccessfully || powerCharacteristicsTask.Result.Status != GattCommunicationStatus.Success) 1370 | exitProcess($"Could not get power service characteristics"); 1371 | 1372 | var powerChar = powerCharacteristicsTask.Result.Characteristics.SingleOrDefault(c => c.Uuid == _powerCharGuid); 1373 | 1374 | if (ShowProgressToast) updateProgress(60); 1375 | 1376 | Thread.Sleep(_delayCmd); 1377 | 1378 | if (powerChar == null) exitProcess($"Could not get power characteristic"); 1379 | 1380 | if (ShowProgressToast) updateProgress(70); 1381 | 1382 | Thread.Sleep(_delayCmd); 1383 | 1384 | LogLine($"[{lh.Name}] Found power characteristic"); 1385 | 1386 | if (ShowProgressToast) updateProgress(80); 1387 | 1388 | string data = v1_OFF; 1389 | if (command == "WAKEUP") data = v1_ON; 1390 | 1391 | if (lh.V2) 1392 | { 1393 | data = v2_OFF; 1394 | if (command == "WAKEUP") data = v2_ON; 1395 | } 1396 | 1397 | string[] values = data.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); 1398 | byte[] bytes = new byte[values.Length]; 1399 | 1400 | for (int i = 0; i < values.Length; i++) 1401 | bytes[i] = Convert.ToByte(values[i], (_dataFormat == DataFormat.Dec ? 10 : (_dataFormat == DataFormat.Hex ? 16 : 2))); 1402 | 1403 | var writer = new DataWriter(); 1404 | writer.ByteOrder = ByteOrder.LittleEndian; 1405 | writer.WriteBytes(bytes); 1406 | 1407 | var buff = writer.DetachBuffer(); 1408 | 1409 | LogLine($"[{lh.Name}] Sending {command} command to {lh.Name}"); 1410 | var writeResultTask = powerChar.WriteValueAsync(buff).AsTask(); 1411 | writeResultTask.Wait(); 1412 | 1413 | if (ShowProgressToast) updateProgress(95); 1414 | 1415 | Thread.Sleep(_delayCmd); 1416 | 1417 | if (!writeResultTask.IsCompletedSuccessfully || writeResultTask.Result != GattCommunicationStatus.Success) exitProcess($"Failed to write {command} command"); 1418 | 1419 | lh.LastCmd = (command == "WAKEUP") ? LastCmd.WAKEUP : LastCmd.SLEEP; 1420 | lh.PoweredOn = (command == "WAKEUP") ? true : false; 1421 | 1422 | btDevice.Dispose(); 1423 | 1424 | LogLine($"[{lh.Name}] SUCCESS command {command}"); 1425 | 1426 | if (ShowProgressToast) updateProgress(100, "Command received!"); 1427 | 1428 | Thread.Sleep(_delayCmd); 1429 | 1430 | LastCmdSent = lh.LastCmd; 1431 | LastCmdStamp = DateTime.Now; 1432 | 1433 | lh.Action = Action.NONE; 1434 | 1435 | lh.ProcessDone = true; 1436 | 1437 | ChangeBSMsg(lh.Name, lh.PoweredOn, lh.LastCmd, lh.Action); 1438 | 1439 | lh.TooManyErrors = true; 1440 | 1441 | Interlocked.Exchange(ref processingCmdSync, 0); 1442 | 1443 | LogLine($"[{lh.Name}] END Processing"); 1444 | } 1445 | catch (ProcessError ex) 1446 | { 1447 | LogLine($"[{lh.Name}] ERROR Processing ({lh.HowManyErrors}): {ex}"); 1448 | lh.ErrorStrings = lh.ErrorStrings.Insert(0, $"{ex.Message}\n"); 1449 | if (lh.TooManyErrors) BalloonMsg($"{lh.ErrorStrings}", $"[{lh.Name}] LAST ERRORS:"); 1450 | lh.LastCmd = LastCmd.ERROR; 1451 | ChangeBSMsg(lh.Name, lh.PoweredOn, lh.LastCmd, lh.Action); 1452 | Interlocked.Exchange(ref processingCmdSync, 0); 1453 | } 1454 | catch (Exception ex) 1455 | { 1456 | LogLine($"[{lh.Name}] ERROR Exception Processing ({lh.HowManyErrors}): {ex}"); 1457 | lh.LastCmd = LastCmd.ERROR; 1458 | lh.ErrorStrings = lh.ErrorStrings.Insert(0, $"{ex.Message}\n"); 1459 | if (lh.TooManyErrors) BalloonMsg($"{lh.ErrorStrings}", $"[{lh.Name}] LAST ERRORS:"); 1460 | ChangeBSMsg(lh.Name, lh.PoweredOn, lh.LastCmd, lh.Action); 1461 | Interlocked.Exchange(ref processingCmdSync, 0); 1462 | } 1463 | } 1464 | 1465 | private void Form1_FormClosing(object sender, FormClosingEventArgs e) 1466 | { 1467 | while (true) 1468 | { 1469 | if (0 == Interlocked.Exchange(ref processingCmdSync, 1)) break; 1470 | Thread.Sleep(100); 1471 | } 1472 | 1473 | } 1474 | 1475 | private void Form1_FormClosed(object sender, FormClosedEventArgs e) 1476 | { 1477 | traceDbg.Close(); 1478 | traceEx.Close(); 1479 | } 1480 | 1481 | private void quitToolStripMenuItem_Click(object sender, EventArgs e) 1482 | { 1483 | Application.Exit(); 1484 | } 1485 | 1486 | private void createDesktopShortcutToolStripMenuItem_Click(object sender, EventArgs e) 1487 | { 1488 | try { 1489 | object shDesktop = (object)"Desktop"; 1490 | WshShell shell = new WshShell(); 1491 | string shortcutAddress = (string)shell.SpecialFolders.Item(ref shDesktop) + @"\BSManager.lnk"; 1492 | IWshShortcut shortcut = (IWshShortcut)shell.CreateShortcut(shortcutAddress); 1493 | shortcut.Description = "Open BSManager"; 1494 | shortcut.Hotkey = ""; 1495 | shortcut.TargetPath = MyExecutableWithPath; 1496 | shortcut.Save(); 1497 | } 1498 | catch (Exception ex) 1499 | { 1500 | HandleEx(ex); 1501 | } 1502 | } 1503 | 1504 | private void licenseToolStripMenuItem_Click(object sender, EventArgs e) 1505 | { 1506 | openlink("https://github.com/mann1x/BSManager/LICENSE"); 1507 | } 1508 | 1509 | private void documentationToolStripMenuItem_Click(object sender, EventArgs e) 1510 | { 1511 | openlink("https://github.com/mann1x/BSManager/"); 1512 | } 1513 | 1514 | private void toolStripRunAtStartup_Click(object sender, EventArgs e) 1515 | { 1516 | 1517 | using (RegistryKey registryStart = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", true)) 1518 | { 1519 | if (!toolStripRunAtStartup.Checked) 1520 | { 1521 | registryStart.SetValue("BSManager", MyExecutableWithPath); 1522 | toolStripRunAtStartup.Checked = true; 1523 | } 1524 | else 1525 | { 1526 | registryStart.DeleteValue("BSManager", false); 1527 | toolStripRunAtStartup.Checked = false; 1528 | } 1529 | } 1530 | 1531 | } 1532 | 1533 | private void openlink(string uri) 1534 | { 1535 | var psi = new ProcessStartInfo(); 1536 | psi.UseShellExecute = true; 1537 | psi.FileName = uri; 1538 | Process.Start(psi); 1539 | } 1540 | 1541 | private string MyExecutableWithPath 1542 | { 1543 | get 1544 | { 1545 | string filepath = Process.GetCurrentProcess().MainModule.FileName; 1546 | string extension = Path.GetExtension(filepath).ToLower(); 1547 | if (String.Equals(extension, ".dll")) 1548 | { 1549 | string folder = Path.GetDirectoryName(filepath); 1550 | string fileName = Path.GetFileNameWithoutExtension(filepath); 1551 | fileName = String.Concat(fileName, ".exe"); 1552 | filepath = Path.Combine(folder, fileName); 1553 | } 1554 | return filepath; 1555 | } 1556 | } 1557 | 1558 | private string[] ProcListLoad(string _filename, string friendlyListName) 1559 | { 1560 | try 1561 | { 1562 | string[] _list = null; 1563 | if (File.Exists(_filename)) 1564 | { 1565 | _list = File.ReadLines(_filename).ToArray(); 1566 | LogLine($"[CONFIG] Loaded custom processes list for {friendlyListName} killing: {string.Join(", ", _list)}"); 1567 | } 1568 | return _list; 1569 | } 1570 | catch (Exception ex) 1571 | { 1572 | HandleEx(ex); 1573 | return null; 1574 | } 1575 | } 1576 | 1577 | 1578 | private void FindRuntime() 1579 | { 1580 | try 1581 | { 1582 | using (RegistryKey registryManage = Registry.CurrentUser.OpenSubKey("SOFTWARE\\ManniX\\BSManager", true)) 1583 | { 1584 | ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT * FROM Win32_Service"); 1585 | var collection = searcher.Get().Cast() 1586 | .Where(mbo => (string)mbo.GetPropertyValue("Name") == "PiServiceLauncher") 1587 | .Select(mbo => (string)mbo.GetPropertyValue("PathName")); 1588 | 1589 | if (collection.Any()) 1590 | { 1591 | RuntimePath = Path.GetDirectoryName(collection.First()); 1592 | 1593 | LogLine($"[CONFIG] Found Runtime={RuntimePath}"); 1594 | registryManage.SetValue("ManageRuntimePath", RuntimePath); 1595 | 1596 | } 1597 | else 1598 | { 1599 | BalloonMsg($"[CONFIG] Pimax Runtime not found"); 1600 | LogLine($"[CONFIG] Pimax Runtime not found"); 1601 | } 1602 | } 1603 | } 1604 | catch (Exception ex) 1605 | { 1606 | HandleEx(ex); 1607 | } 1608 | 1609 | } 1610 | 1611 | private void toolStripDebugLog_Click(object sender, EventArgs e) 1612 | { 1613 | 1614 | using (RegistryKey registryDebug = Registry.CurrentUser.OpenSubKey("SOFTWARE\\ManniX\\BSManager", true)) 1615 | { 1616 | if (!toolStripDebugLog.Checked) 1617 | { 1618 | registryDebug.SetValue("DebugLog", "1"); 1619 | toolStripDebugLog.Checked = true; 1620 | } 1621 | else 1622 | { 1623 | registryDebug.DeleteValue("DebugLog", false); 1624 | toolStripDebugLog.Checked = false; 1625 | } 1626 | 1627 | } 1628 | } 1629 | 1630 | private void disableProgressToastToolStripMenuItem_Click(object sender, EventArgs e) 1631 | { 1632 | try 1633 | { 1634 | using (RegistryKey registryManage = Registry.CurrentUser.OpenSubKey("SOFTWARE\\ManniX\\BSManager", true)) 1635 | { 1636 | if (!disableProgressToastToolStripMenuItem.Checked) 1637 | { 1638 | SetProgressToast = false; 1639 | ShowProgressToast = false; 1640 | registryManage.DeleteValue("ShowProgressToast", false); 1641 | disableProgressToastToolStripMenuItem.Checked = true; 1642 | } 1643 | else 1644 | { 1645 | SetProgressToast = true; 1646 | ShowProgressToast = true; 1647 | registryManage.SetValue("ShowProgressToast", "1"); 1648 | disableProgressToastToolStripMenuItem.Checked = false; 1649 | } 1650 | } 1651 | 1652 | } 1653 | catch (Exception ex) 1654 | { 1655 | HandleEx(ex); 1656 | } 1657 | } 1658 | 1659 | private void RuntimeToolStripMenuItem_Click(object sender, EventArgs e) 1660 | { 1661 | try 1662 | { 1663 | using (RegistryKey registryManage = Registry.CurrentUser.OpenSubKey("SOFTWARE\\ManniX\\BSManager", true)) 1664 | { 1665 | if (!RuntimeToolStripMenuItem.Checked) 1666 | { 1667 | ManageRuntime = true; 1668 | registryManage.SetValue("ManageRuntime", "1"); 1669 | RuntimeToolStripMenuItem.Checked = true; 1670 | } 1671 | else 1672 | { 1673 | ManageRuntime = false; 1674 | registryManage.DeleteValue("ManageRuntime", false); 1675 | RuntimeToolStripMenuItem.Checked = false; 1676 | } 1677 | } 1678 | 1679 | } 1680 | catch (Exception ex) 1681 | { 1682 | HandleEx(ex); 1683 | } 1684 | 1685 | } 1686 | public new void Dispose() 1687 | { 1688 | ProcessLHtimer.Enabled = false; 1689 | ProcessLHtimer.Stop(); 1690 | removeWatcher.Stop(); 1691 | insertWatcher.Stop(); 1692 | watcher.Stop(); 1693 | Dispose(true); 1694 | } 1695 | 1696 | 1697 | } 1698 | public class ProcessError : Exception 1699 | { 1700 | public ProcessError() 1701 | { 1702 | } 1703 | 1704 | public ProcessError(string message) : base(message) 1705 | { 1706 | } 1707 | 1708 | public ProcessError(string message, Exception innerException) : base(message, innerException) 1709 | { 1710 | } 1711 | 1712 | protected ProcessError(SerializationInfo info, StreamingContext context) : base(info, context) 1713 | { 1714 | } 1715 | 1716 | ProcessError(int severity, string message) : base(message) 1717 | { 1718 | } 1719 | } 1720 | 1721 | } -------------------------------------------------------------------------------- /BSManagerMain.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | text/microsoft-resx 50 | 51 | 52 | 2.0 53 | 54 | 55 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 56 | 57 | 58 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 59 | 60 | 61 | 17, 17 62 | 63 | 64 | 130, 17 65 | 66 | -------------------------------------------------------------------------------- /BSManagerRes.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace BSManager { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class BSManagerRes { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal BSManagerRes() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("BSManager.BSManagerRes", typeof(BSManagerRes).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). 65 | /// 66 | internal static System.Drawing.Icon bsmanager_off { 67 | get { 68 | object obj = ResourceManager.GetObject("bsmanager_off", resourceCulture); 69 | return ((System.Drawing.Icon)(obj)); 70 | } 71 | } 72 | 73 | /// 74 | /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). 75 | /// 76 | internal static System.Drawing.Icon bsmanager_on { 77 | get { 78 | object obj = ResourceManager.GetObject("bsmanager_on", resourceCulture); 79 | return ((System.Drawing.Icon)(obj)); 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /BSManagerRes.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 122 | bsmanager.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 123 | 124 | 125 | Resources\bsmanager_on.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 126 | 127 | -------------------------------------------------------------------------------- /GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage 2 | // attributes that are applied to this project. 3 | // Project-level suppressions either have no target or are given 4 | // a specific target and scoped to a namespace, type, member, etc. 5 | 6 | using System.Diagnostics.CodeAnalysis; 7 | 8 | [assembly: SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "", Scope = "member", Target = "~M:BSManager.BSManagerMain.Form1_Load(System.Object,System.EventArgs)")] 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 ManniX 2 | 3 | Copyright (c) 2018 Shell TechWorks 4 | 5 | MIT License 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | 26 | -------------------------------------------------------------------------------- /LightHouse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Configuration; 3 | 4 | namespace BSManager 5 | { 6 | internal class Lighthouse 7 | { 8 | public string Name { get; private set; } 9 | public ulong Address { get; private set; } 10 | public bool ProcessDone { get; set; } 11 | public bool PoweredOn { get; set; } 12 | public bool V2PoweredOn { get; set; } 13 | public int LastPowerState { get; set; } 14 | public int ErrorTotal { get; set; } 15 | public int OpsTotal { get; set; } 16 | public string ErrorStrings { get; set; } 17 | 18 | private LastCmd _lastCmd; 19 | 20 | private DateTime _lastCmdStamp; 21 | 22 | public BSManufacturer Manufacturer; 23 | 24 | public DateTime LastCmdStamp 25 | { 26 | get 27 | { 28 | return _lastCmdStamp; 29 | } 30 | } 31 | 32 | public LastCmd LastCmd 33 | { 34 | get { 35 | return _lastCmd; 36 | } 37 | set { 38 | _lastCmd = value; 39 | _lastCmdStamp = DateTime.Now; 40 | } 41 | } 42 | 43 | public Action Action { get; set; } 44 | public bool V2 { get; set; } 45 | 46 | private int _errCnt; 47 | public string HowManyErrors 48 | { 49 | get 50 | { 51 | return _errCnt.ToString(); 52 | } 53 | } 54 | 55 | public bool TooManyErrors 56 | { 57 | get { 58 | ErrorTotal++; 59 | if (_errCnt > 5) 60 | { 61 | _errCnt = 0; 62 | return true; 63 | } 64 | else 65 | { 66 | _errCnt++; 67 | return false; 68 | } 69 | } 70 | set { 71 | _errCnt = 0; 72 | ErrorStrings = ""; 73 | } 74 | } 75 | public Lighthouse(string name, ulong address) 76 | { 77 | Name = name; 78 | Address = address; 79 | PoweredOn = false; 80 | V2PoweredOn = false; 81 | LastCmd = LastCmd.NONE; 82 | Action = Action.NONE; 83 | Manufacturer = BSManufacturer.HTC; 84 | _errCnt = 0; 85 | LastPowerState = 0; 86 | ProcessDone = false; 87 | ErrorStrings = ""; 88 | ErrorTotal = 0; 89 | OpsTotal = 0; 90 | } 91 | 92 | public override bool Equals(object obj) 93 | { 94 | return obj is Lighthouse lighthouse && 95 | Name == lighthouse.Name && 96 | Address == lighthouse.Address; 97 | } 98 | 99 | public override int GetHashCode() 100 | { 101 | return HashCode.Combine(Name, Address); 102 | } 103 | 104 | } 105 | 106 | public enum LastCmd 107 | { 108 | NONE, 109 | ERROR, 110 | WAKEUP, 111 | SLEEP 112 | } 113 | public enum BSManufacturer 114 | { 115 | HTC, 116 | VIVE 117 | } 118 | public enum Action 119 | { 120 | NONE, 121 | WAKEUP, 122 | SLEEP 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using System.Windows.Forms; 7 | 8 | namespace BSManager 9 | { 10 | 11 | static class Program 12 | { 13 | /// 14 | /// The main entry point for the application. 15 | /// 16 | static Mutex mutex = new Mutex(true, "{67489549-940B-48FF-9B6E-70D31B4C6E71}"); 17 | [STAThread] 18 | static void Main() 19 | { 20 | if (mutex.WaitOne(TimeSpan.Zero, true)) 21 | { 22 | Application.SetHighDpiMode(HighDpiMode.SystemAware); 23 | Application.EnableVisualStyles(); 24 | Application.SetCompatibleTextRenderingDefault(false); 25 | Application.Run(new Form1()); 26 | 27 | mutex.ReleaseMutex(); 28 | } else { 29 | Application.Exit(); 30 | } 31 | } 32 | 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Properties/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace BSManager.Properties { 12 | 13 | 14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.10.0.0")] 16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { 17 | 18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); 19 | 20 | public static Settings Default { 21 | get { 22 | return defaultInstance; 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Properties/Settings.settings: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BSManager 2 | 3 | 4 | BSManager is a small portable utility to switch automatically on and off the Vive Base Stations with the VR Headset. 5 | 6 | It does support all Pimax models and the Vive Pro headsets. 7 | 8 | Base Stations v1 and v2 are both supported. 9 | 10 | 11 | ## **USE AT YOUR OWN RISK** 12 | 13 | 14 | ## Installation 15 | 16 | It's a portable application; the only software pre-requisite is the Desktop Runtime for .NET Core 3.1 (https://versionsof.net/core/3.1/3.1.19/) but it should be self-contained. 17 | 18 | Move it into a permanent directory, you can create a shortcut to launch it or use the drop-down menu option to create it on the desktop. 19 | 20 | If you wish in the drop-down menu you can also select "Run at Startup" and it will be run at every current user logon. 21 | 22 | To control the Base Stations you need a BLE adapter. The HeadSet Bluetooth adapter, if any, can't be used. 23 | 24 | In theory there's no need to pair the Base Stations in Windows but if they are not seen try it (their name starts with "HTC BS " or "LHB-"). 25 | 26 | If you get many errors or can't see the Base Stations they are too far from to the USB dongle/antenna, shorten the distance. 27 | 28 | 29 | ## Usage 30 | 31 | There's no main window, only the drop-down menu accessible clicking with right mouse button on the system tray icon. 32 | 33 | 34 | ![Tray Menu Picture](https://github.com/mann1x/BSManager/raw/master/BSManager/BSManager_tray.png) 35 | 36 | 37 | You can check the BS discovered, the HMD status, configure the startup, the Steam VR DB and enable logging (it will create a BSManager.log file). 38 | 39 | At startup if the HMD is found active the software will send the base stations a wake-up command or a sleep command if the HMD is off (this only for v1). 40 | 41 | Base stations are not discovered, the App will listen to BLE Advertisement messages. 42 | 43 | The 2nd number is "Discovered:" after the / is the count of Base Stations found in the SteamVR database. 44 | 45 | The icon on the left of the Base Station will change upon a correct wake-up or sleep command is received. After the name the pending action, if any, is displayed. 46 | 47 | Only for the BS v2 if the last issued command is Sleep and the Base Stations are still On after 5 minutes the command will be re-sent. 48 | 49 | Only for the BS v1 the Stations will be briefly powered on, about 30 seconds, to set the Sleep mode (they don't report the power state). 50 | 51 | BSManager can automatically start & kill Pimax Runtime & close selected SteamVR components (which reduces risk of SteamVR crashes). 52 | 53 | Manage Runtime is an option that can be enabled in the System Tray Icon drop-down menu; it can be enabled if the Pimax Runtime is not in the default directory running only once BSManager with Admin privileges. 54 | 55 | With Manage Runtime enabled Pitool is automatically open and closed. The process will start about 15 seconds after the HMD has changed state to allow reboots (HMD reboot, Pimax service restart or manual on/off) without disruptions. 56 | 57 | There's also support for customizable lists of processes to close when the Headset is powered off. There's a graceful and an immediate killing list. It's better if possible to use the graceful list. Unfortunately this will not work for processes that doesn't directly close like Pitool which are asking for user input to quit the application. 58 | 59 | The default list for processes to be killed gracefully is: "vrmonitor", "vrdashboard", "ReviveOverlay", "vrmonitor" (twice in the list to repeat attempts to close). To customize the graceful list use a "BSManager.grace.txt" file in the same directory of BSManager.exe, one process per line (will replace the default). 60 | 61 | The immediate killing list (SIGTERM) is empty by default and can be customized using a "BSManager.kill.txt" file in the same directory of BSManager.exe, one process per line 62 | 63 | **Please use the Issues tab on GitHub if you find issues or have a request** 64 | 65 | 66 | ## Credits 67 | 68 | - Thanks to: 69 | - The excellent BLEConsole from SeNSSoFT [https://github.com/sensboston/BLEConsole] 70 | - LightHouseController from Alex Flynn [https://bitbucket.org/Flynny75/lighthousecontroller/src/master/] 71 | - SparkerInVR's great support in testing [https://www.twitch.tv/sparkerinvr] 72 | 73 | 74 | ## Compilation 75 | 76 | You can compile with Visual Studio 2019 and .NET Core 3.1. 77 | 78 | 79 | ## Changelog: 80 | 81 | - v2.4.1 82 | - Fix: Bug in Run at Startup (watch out the AutoUpdater is impacted as well, you may need to update manually!) 83 | - v2.4.0 84 | - New: Toast for BS commands progress (so you know if it's actually doing something), can be disabled from Help and Info drop-down menu 85 | - New: Notifications and BLE errors improvements 86 | - New: Run at Startup executable path will be replaced with the current if different from registry (avoid startup of an old version) 87 | - New: Moved from .NET 5 to .NET Core 3.1 (less memory requirements, more stable development environment); .NET install now self-contained (bigger file size) 88 | - New: Improved routine to kill processes 89 | - Fix: Support for HTC manufactured Base Stations v2 90 | - Fix: Added support for Pimax LightHouses DB 91 | - Fix: Bug in Run at Startup 92 | - v2.3.0 93 | - New: Option to automatically start & kill Pimax Runtime & close SteamVR components (reduces risk of SteamVR crashes) 94 | - New: Manage Runtime is an option that can be enabled in the System Tray Icon drop-down menu 95 | - New: Manage Runtime can be enabled if the Pimax Runtime is not in the default directory running only once BSManager with Admin privileges 96 | - New: Delayed Base Stations control; rebooting HMD or PiService will not trigger a Base Stations power off/on cycle 97 | - New: Customizable list for processes to be killed gracefully, default: "vrmonitor", "vrdashboard", "ReviveOverlay", "vrmonitor" (twice in the list to repeat) 98 | - New: Graceful list can be customized using a "BSManager.grace.txt" file in the same directory of BSManager.exe, one process per line (will replace the default) 99 | - New: Immediate killing list (SIGTERM) can be customized using a "BSManager.kill.txt" file in the same directory of BSManager.exe, one process per line 100 | - Fix: Improved log files readability 101 | - v2.2.1 102 | - New: Unified BLE workflow for BS v1 and v2, no functional changes 103 | - v2.1.0 104 | - New: Background thread that will start and stop the BLE Advertisement watcher on demand 105 | - New: Reduced CPU usage from 0.01-0.02% to almost 0% in idle 106 | - New: Optimized memory usage, should be stable at about 65MB (Garbage collector every 10 minutes) 107 | - v2.0.0 108 | - New: Support for Base Stations v2 109 | - New: Removed discovery to use BLE Advertisement messages instead 110 | - New: Icon to display BS status, action pending 111 | - New: Switch to enable/disable Debug Log 112 | - Fix: Many fixes and code improvements 113 | - v1.2.5 114 | - Fix: Tentative fix for BS v2, wrong characteristic 115 | - Fix: Improved reliability of SteamVR DB parsing 116 | - Fix: Improved BLE Discovery 117 | - v1.2.4 118 | - Fix: Bug in BS v2 discovery 119 | - v1.2.3 120 | - New: Implemented AutoUpdater 121 | - v1.2.1 122 | - New: Added blind experimental Base Stations v2 support 123 | - v1.2.0 124 | - New: Added mutex for only one instance active 125 | - Fix: Fixed critical bug, mistake on BT devicelist trim 126 | - New: Cycling through command retries and looping the whole command sequence 127 | - New: Added error and exception management 128 | - New: Exceptions are now saved in a log file 129 | - New: Implemented tooltip balloons to display error messages 130 | - New: Tray icon will display HeadSet status via a green dot, means active 131 | - Fix: Fixed BT commands loop with close (open should disconnected previously connected device but seems not doing it) 132 | - Fix: Other small fixes 133 | - v1.0.1 134 | - New: Introduced a wait of 2.5 seconds between BLE discovery cycles to wait for BLE adapter to be available at system startup 135 | - v1.0.0 136 | - Initial release 137 | -------------------------------------------------------------------------------- /Resources/bsmanager_on.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mann1x/BSManager/b5a7bdbc44c8cca84d997a20c392483751b9f666/Resources/bsmanager_on.ico -------------------------------------------------------------------------------- /Resources/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mann1x/BSManager/b5a7bdbc44c8cca84d997a20c392483751b9f666/Resources/error.png -------------------------------------------------------------------------------- /Resources/warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mann1x/BSManager/b5a7bdbc44c8cca84d997a20c392483751b9f666/Resources/warning.png -------------------------------------------------------------------------------- /bsmanager.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mann1x/BSManager/b5a7bdbc44c8cca84d997a20c392483751b9f666/bsmanager.ico -------------------------------------------------------------------------------- /bsmanager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mann1x/BSManager/b5a7bdbc44c8cca84d997a20c392483751b9f666/bsmanager.png --------------------------------------------------------------------------------