├── .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 | 
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
--------------------------------------------------------------------------------