diff --git a/Form1.Designer.cs b/Form1.Designer.cs index fff0f90..f080e93 100644 --- a/Form1.Designer.cs +++ b/Form1.Designer.cs @@ -28,6 +28,7 @@ protected override void Dispose(bool disposing) /// private void InitializeComponent() { + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Form1)); listBoxDevices = new ListBox(); lblStatus = new Label(); btnScan = new Button(); @@ -39,20 +40,20 @@ private void InitializeComponent() listBoxDevices.ItemHeight = 15; listBoxDevices.Location = new Point(12, 12); listBoxDevices.Name = "listBoxDevices"; - listBoxDevices.Size = new Size(300, 109); + listBoxDevices.Size = new Size(311, 109); listBoxDevices.TabIndex = 0; // // lblStatus // lblStatus.AutoSize = true; - lblStatus.Location = new Point(93, 131); + lblStatus.Location = new Point(99, 135); lblStatus.Name = "lblStatus"; lblStatus.Size = new Size(0, 15); lblStatus.TabIndex = 1; // // btnScan // - btnScan.Location = new Point(12, 127); + btnScan.Location = new Point(18, 131); btnScan.Name = "btnScan"; btnScan.Size = new Size(75, 23); btnScan.TabIndex = 2; @@ -64,10 +65,11 @@ private void InitializeComponent() // AutoScaleDimensions = new SizeF(7F, 15F); AutoScaleMode = AutoScaleMode.Font; - ClientSize = new Size(321, 163); + ClientSize = new Size(335, 168); Controls.Add(btnScan); Controls.Add(lblStatus); Controls.Add(listBoxDevices); + Icon = (Icon)resources.GetObject("$this.Icon"); Name = "Form1"; Text = "Polar H10 to LSL"; ResumeLayout(false); diff --git a/Form1.cs b/Form1.cs index 3b5f45a..2920a4c 100644 --- a/Form1.cs +++ b/Form1.cs @@ -1,7 +1,12 @@ -using Windows.Devices.Bluetooth.Advertisement; +using Windows.Devices.Bluetooth.Advertisement; using Windows.Devices.Bluetooth; using Windows.Devices.Bluetooth.GenericAttributeProfile; + using LSL; +using System.Reflection.PortableExecutable; +using Windows.Storage.Streams; +using System.Windows.Forms; + namespace PolarBLE { @@ -18,17 +23,19 @@ public partial class Form1 : Form /// /// Dictionary storing detected BLE devices, indexed by their Bluetooth address. /// - Dictionary devices = []; + Dictionary devices = new Dictionary(); /// - /// LSL outlet used to stream ECG data. + /// LSL outlet used to stream ECG and Acc data. /// - StreamOutlet outlet; + StreamOutlet ecgOutlet; + StreamOutlet accOutlet; /// /// Reference to the GATT characteristic used for receiving ECG data from the Polar H10. /// GattCharacteristic ecgChar; + GattCharacteristic accChar; /// /// UUID for the Polar Measurement Data (PMD) control characteristic. @@ -43,14 +50,42 @@ public partial class Form1 : Form /// /// Byte array command used to initialize ECG streaming on the Polar H10 sensor. /// - private static readonly byte[] ECG_WRITE = new byte[] + private static readonly byte[] ECG_WRITE = { - 0x02, 0x00, 0x00, 0x01, 0x82, 0x00, 0x01, 0x01, 0x0E, 0x00 + 0x02, // [0] Start measurement command + 0x00, 0x00, // [1-2] Reserved or unused (typically 0x0000) + 0x01, // [3] Measurement type: PMD (Physical Measurement Data) + 0x82, 0x00, // [4-5] Feature type: ECG (0x0082 in little-endian) + 0x01, // [6] Resolution index: 0x01 → 16-bit resolution + 0x01, // [7] Sample rate: 0x01 → 130 Hz + 0x0E, 0x00 // [8-9] Range index or frame type (0x000E is a bit mysterious; varies by firmware) }; + + /// + /// Byte array command used to initialize ACC streaming on the Polar H10 sensor. + /// Range index 0x02 = ±2g + /// Range index 0x04 = ±4g + /// Range index 0x08 = ±8g + /// + private static readonly byte[] ACC_WRITE = + { + 0x02, // Start measurement + 0x02, 0x00, // Reserved + 0x01, // Measurement type: PMD (0x01) + 0xC8, 0x00, // Feature type: ACC (0x0083 little-endian) + 0x01, // Resolution index (0x01 = 16-bit) + 0x01, // Sample rate (0x01 = 200Hz) + 0x10, 0x00, // Range index (optional; this sets ±4g in some docs) + 0x02, 0x01, 0x08, 0x00 // Additional configuration bytes + }; + + private const string BatteryLevelCharacteristicUuid = "00002A19-0000-1000-8000-00805F9B34FB"; + /// /// Internal counter for cycling the "Streaming" status animation dots. /// private int dotState = 0; + private TextProgressBar? Battery = null; /// /// Initializes a new instance of the class and sets up event handlers. @@ -58,9 +93,40 @@ public partial class Form1 : Form public Form1() { InitializeComponent(); + Battery = new TextProgressBar() + { + Size = new Size(100, 23), + Location = new Point(223, 131), + Visible = false, + Value = 0, + }; + this.Controls.Add(Battery); listBoxDevices.SelectedIndexChanged += ListBoxDevices_SelectedIndexChanged; } + private void CustomDrawProgressBar(PaintEventArgs e) + { + int progress = 60; // Your progress value + int max = 100; + + float percent = (float)progress / max; + int width = (int)(this.Width * percent); + + // Draw background + e.Graphics.FillRectangle(Brushes.Gray, 0, 0, this.Width, this.Height); + // Draw progress + e.Graphics.FillRectangle(Brushes.Green, 0, 0, width, this.Height); + // Draw text + string text = $"{progress}%"; + var size = e.Graphics.MeasureString(text, this.Font); + var textPos = new PointF( + (this.Width - size.Width) / 2, + (this.Height - size.Height) / 2 + ); + e.Graphics.DrawString(text, this.Font, Brushes.White, textPos); + } + + /// /// Event handler for the Scan button click event. Starts scanning for nearby Polar H10 BLE devices. /// @@ -102,6 +168,8 @@ private async void ListBoxDevices_SelectedIndexChanged(object? sender, EventArgs lblStatus.Text = "Connecting..."; var selected = listBoxDevices.SelectedItem?.ToString(); + if (selected == null) return; + var address = ulong.Parse(selected.Split('[')[1].TrimEnd(']')); var device = await BluetoothLEDevice.FromBluetoothAddressAsync(address); @@ -111,21 +179,46 @@ private async void ListBoxDevices_SelectedIndexChanged(object? sender, EventArgs var chars = await service.GetCharacteristicsAsync(); foreach (var c in chars.Characteristics) { - if (c.Uuid.ToString().ToUpper() == PMD_CONTROL) + if (c == null) continue; + if (c?.Uuid.ToString().ToUpper() == PMD_CONTROL) { - await c.ReadValueAsync(); await c.WriteValueAsync(Windows.Security.Cryptography.CryptographicBuffer.CreateFromByteArray(ECG_WRITE)); + await c.WriteValueAsync(Windows.Security.Cryptography.CryptographicBuffer.CreateFromByteArray(ACC_WRITE)); } - if (c.Uuid.ToString().ToUpper() == PMD_DATA) + + if (c?.Uuid.ToString().ToUpper() == PMD_DATA) { ecgChar = c; ecgChar.ValueChanged += EcgChar_ValueChanged; await ecgChar.WriteClientCharacteristicConfigurationDescriptorAsync( GattClientCharacteristicConfigurationDescriptorValue.Notify); + + accChar = c; + accChar.ValueChanged += AccChar_ValueChanged; + await accChar.WriteClientCharacteristicConfigurationDescriptorAsync( + GattClientCharacteristicConfigurationDescriptorValue.Notify); + } + if (c?.Uuid == new Guid(BatteryLevelCharacteristicUuid)) + { + // Read the value + var readResult = await c?.ReadValueAsync(BluetoothCacheMode.Uncached); + if (readResult.Status == GattCommunicationStatus.Success) + { + var reader = DataReader.FromBuffer(readResult.Value); + byte[] data = new byte[readResult.Value.Length]; + reader.ReadBytes(data); + + // Use the input byte array + if (Battery != null) + { + Battery.Value = data[0]; + } + } } } } + if (Battery != null) Battery.Visible = true; StartLSL(device.Name, address.ToString()); lblStatus.Text = "Wait for Streaming..."; } @@ -141,7 +234,7 @@ private unsafe void EcgChar_ValueChanged(GattCharacteristic sender, GattValueCha reader.ByteOrder = Windows.Storage.Streams.ByteOrder.LittleEndian; byte[] data = new byte[args.CharacteristicValue.Length]; reader.ReadBytes(data); - + if (data[0] != 0x00) return; int step = 3; @@ -154,7 +247,7 @@ private unsafe void EcgChar_ValueChanged(GattCharacteristic sender, GattValueCha if ((raw & 0x800000) != 0) // if sign bit (bit 23) is set raw |= unchecked((int)0xFF000000); // sign-extend to 32 bits - outlet.push_sample(new float[] { raw }); + ecgOutlet?.push_sample(new float[] { raw }); offset += step; } // Animate "Streaming" label with dots to show activity @@ -163,6 +256,38 @@ private unsafe void EcgChar_ValueChanged(GattCharacteristic sender, GattValueCha BeginInvoke(() => lblStatus.Text = $"Streaming{dots}"); } + /// + /// Callback invoked when ACC characteristic receives new data. Parses ACC samples and pushes them to the LSL stream. + /// + /// The GATT characteristic that triggered the event. + /// Event arguments containing the characteristic value data. + private void AccChar_ValueChanged(GattCharacteristic sender, GattValueChangedEventArgs args) + { + var reader = Windows.Storage.Streams.DataReader.FromBuffer(args.CharacteristicValue); + reader.ByteOrder = Windows.Storage.Streams.ByteOrder.LittleEndian; + byte[] data = new byte[args.CharacteristicValue.Length]; + reader.ReadBytes(data); + + if (data.Length < 10 || data[0] != 0x02) return; + + byte frame_type = data[9]; + int resolution = (frame_type + 1) *8; + int bytesPerAxis = resolution / 8; + int bytesPerSample = 3 * bytesPerAxis; + + data = data.Skip(10).ToArray(); // skip header + + for (int i = 0; i + bytesPerSample <= data.Length; i += bytesPerSample) + { + short x = BitConverter.ToInt16(data, i); + short y = BitConverter.ToInt16(data, i + 2); + short z = BitConverter.ToInt16(data, i + 4); + + accOutlet?.push_sample(new float[] { x, y, z }); + } + BeginInvoke(() => lblStatus.ForeColor = Color.Green); + } + /// /// Initializes and starts the LSL (LabStreamingLayer) outlet stream for ECG data. /// @@ -170,14 +295,21 @@ private unsafe void EcgChar_ValueChanged(GattCharacteristic sender, GattValueCha /// A unique identifier for the stream based on the Bluetooth address. private void StartLSL(string name, string id) { - var info = new StreamInfo(name, "ECG", 1, 130, channel_format_t.cf_float32, id); + var info = new StreamInfo(name + "_ecg", "ECG", 1, 130, channel_format_t.cf_float32, id = name + "_ecg"); var channels = info.desc().append_child("channels"); channels.append_child("channel") .append_child_value("name", "ECG") .append_child_value("unit", "microvolts") .append_child_value("type", "ECG"); - outlet = new StreamOutlet(info, 74, 360); + ecgOutlet = new StreamOutlet(info, 74, 360); + + var accInfo = new StreamInfo(name + "_acc", "Accelerometer", 3, 200, channel_format_t.cf_float32, id = name + "_acc"); + var accChannels = accInfo.desc().append_child("channels"); + accChannels.append_child("channel").append_child_value("name", "X"); + accChannels.append_child("channel").append_child_value("name", "Y"); + accChannels.append_child("channel").append_child_value("name", "Z"); + accOutlet = new StreamOutlet(accInfo, 25, 360); } } } diff --git a/Form1.resx b/Form1.resx index 8b2ff64..fcc2443 100644 --- a/Form1.resx +++ b/Form1.resx @@ -117,4 +117,81 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + AAABAAEAICAAAAEAIACoEAAAFgAAACgAAAAgAAAAQAAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASQAAAEkAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFYAAAD7AAAA/AAA + AFwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABWAAAA+wAA + AP8AAAD/AAAA/AAAAFwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVgAA + APsAAAD/AAAA/wAAAP8AAAD/AAAA/AAAAFwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AFYAAAD7AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/AAAAFwAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAABVAAAA+wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/AAAAFwAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAWQAAAPwAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAADxAAAAywAAAP8AAAD/AAAA/AAA + AFgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAFkAAAD8AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAGcAAAAJAAAA8QAA + AP8AAAD/AAAA/AAAAFgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAABZAAAA/AAAAP8AAAD/AAAA/wAAAHoAAABSAAAA/wAAAP8AAADbAAAABQAA + AAAAAAChAAAA/wAAAP8AAAD/AAAA+wAAAFgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAVQAAAPsAAAD/AAAA/wAAAP8AAADsAAAACwAAAAAAAAC9AAAA/wAA + AFoAAAAkAAAARQAAAEkAAAD/AAAA/wAAAP8AAAD/AAAA/AAAAFsAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAARAAAAKoAAACqAAAAqgAAAFsAAABSAAAAXAAAAOoAAAD/AAAA/wAAAH4AAAASAAAAOgAA + ADoAAADRAAAAAgAAAKUAAACiAAAABAAAAOoAAAD/AAAA/wAAANEAAADMAAAAyAAAAD8AAAAzAAAAMwAA + ADMAAAAUAAAAAAAAAAAAAABEAAAAqgAAAKoAAACoAAAAWgAAAFUAAAAQAAAAfAAAAP8AAADzAAAAEgAA + AHwAAADFAAAAAAAAACIAAAAuAAAA/QAAAPIAAAAJAAAAlgAAAP8AAAC5AAAAAAAAAAAAAAAAAAAACAAA + ANEAAAD/AAAA/wAAAGYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMEAAAD/AAAA/wAAAHkAAAAVAAAA9wAA + AI8AAAAJAAAA6QAAAP8AAABLAAAAAAAAALIAAAD/AAAA/wAAAFUAAAA+AAAA/wAAAHAAAAAnAAAA3QAA + AN0AAADdAAAAsQAAACIAAAAiAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAfAAAA/wAAAP8AAAD/AAAA4wAA + AAUAAACbAAAAHwAAAGsAAAD/AAAA/wAAANgAAAB/AAAA/wAAAP8AAAD/AAAArQAAAAEAAADgAAAAKwAA + AG0AAAD/AAAA/wAAAP8AAAD+AAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE8AAAD/AAAA/wAA + AP8AAAD/AAAAVwAAAAoAAAAEAAAA3QAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD3AAAADwAA + AHIAAAABAAAAsgAAAP8AAAD/AAAA/wAAAP8AAABMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVwAA + AP8AAAD/AAAA/wAAAP8AAADFAAAAAAAAAFkAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAA + AP8AAABfAAAABAAAAAUAAADyAAAA/wAAAP8AAAD/AAAA/wAAAFUAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAA3AAAA/wAAAP8AAAD/AAAA/wAAAP8AAACEAAAA3AAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAA + AP8AAAD/AAAA/wAAALgAAAAAAAAAPQAAAP8AAAD/AAAA/wAAAP8AAAD/AAAANAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAMAAADnAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAA + AP8AAAD/AAAA/wAAAP8AAAD/AAAA+wAAABkAAACGAAAA/wAAAP8AAAD/AAAA/wAAAOUAAAADAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGoAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAA + AP8AAAD/AAAArgAAAK4AAAD/AAAA/wAAAP8AAAD/AAAA6QAAAPoAAAD/AAAA/wAAAP8AAAD/AAAAaQAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAKQAAAD/AAAA/wAAAP8AAAD/AAAA/wAA + AP8AAAD/AAAA/wAAAKsAAAAFAAAABQAAAKwAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAA + AKUAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAH0AAAD0AAAA/wAA + AP8AAAD/AAAA/wAAAPQAAAB8AAAAAgAAAAAAAAAAAAAAAgAAAH0AAAD0AAAA/wAAAP8AAAD/AAAA/wAA + APQAAAB7AAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + ABIAAABdAAAAggAAAIIAAABcAAAAEgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABMAAABdAAAAggAA + AIIAAABcAAAAEgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAA/////////////////////////////n////w////4H///8A///+AH///A + A///gAH//wAA//4AIH/8BAA/gAAAAYACAcHwAQAB4AAAB+AAAAfgQAAH4AAEB+AAAAfwAAAP8AAAD/gB + gB/+B+B///////////////////////////8= + + \ No newline at end of file diff --git a/PolarBLE.csproj b/PolarBLE.csproj index 8b3eea9..1f65fa7 100644 --- a/PolarBLE.csproj +++ b/PolarBLE.csproj @@ -7,6 +7,17 @@ true enable true + icon.ico + + + + + + + Always + + + \ No newline at end of file diff --git a/README.md b/README.md index 3fbe6a0..8575e18 100644 --- a/README.md +++ b/README.md @@ -3,17 +3,16 @@ **PolarBLE** is a C# .NET 8 WinForms application that connects to a **Polar H10 ECG sensor** over **Bluetooth Low Energy (BLE)** and streams raw ECG data in real-time to **LabStreamingLayer (LSL)**. This project is a port of my previous [PolarBand2lsl](https://github.com/markspan/PolarBand2lsl) Python/Kivy implementation. +I took info from the project [Dont-hold-your-breath](https://github.com/kieranabrennan/dont-hold-your-breath) by Kieran Brennan. Look it up! --- ## 🧠 Features - Scan for nearby **Polar H10** devices via BLE -- Connect and subscribe to the ECG and ACC measurement service -- Decode and stream **130 Hz** ECG data to **LSL** -- Decode and stream **200 Hz** ACC data to **LSL** -- Simple UI for quick interaction - +- Connect and subscribe to the ECG measurement service +- Decode and stream **130 Hz** ECG data to **LSL** +- Decode and stream **200Hz** ACC data to **LSL** --- ## 💻 Requirements @@ -27,9 +26,7 @@ This project is a port of my previous [PolarBand2lsl](https://github.com/markspa ## 🚀 Quickstart -### Clone & Restore - -```bash -git clone https://github.com/YOUR_USERNAME/PolarBLE.git - - +Download the last Release from the GitHub repository, unzip and run. +The Program will start looking for nearby polar bands and display their ID. +When you click on the ID of the band you want to stream, it will start streaming. +This can take some time (up to a minute), the programm will inform you when streaming is in progress. diff --git a/TextProgressBar.cs b/TextProgressBar.cs new file mode 100644 index 0000000..dccba48 --- /dev/null +++ b/TextProgressBar.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.Eventing.Reader; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace PolarBLE +{ + public class TextProgressBar : Panel + { + private int _value = 0; + private int _maximum = 100; + + public int Value + { + get => _value; + set + { + _value = Math.Min(_maximum, Math.Max(0, value)); + this.Invalidate(); // Redraw + } + } + + public int Maximum + { + get => _maximum; + set + { + _maximum = Math.Max(1, value); + this.Invalidate(); + } + } + + public TextProgressBar() + { + this.DoubleBuffered = true; + this.ResizeRedraw = true; + } + + protected override void OnPaint(PaintEventArgs e) + { + base.OnPaint(e); + + float percent = (float)_value / _maximum; + int fillWidth = (int)(this.Width * percent); + + // Draw progress bar + if (percent < .10) + { + using (Brush progressBrush = new SolidBrush(Color.Red)) + { + e.Graphics.FillRectangle(progressBrush, 0, 0, fillWidth, this.Height); + } + } + else + { + using (Brush progressBrush = new SolidBrush(Color.Green)) + { + e.Graphics.FillRectangle(progressBrush, 0, 0, fillWidth, this.Height); + } + } + + + // Draw text + string text = $"{_value}%"; + SizeF textSize = e.Graphics.MeasureString(text, this.Font); + PointF textPos = new PointF( + (this.Width - textSize.Width) / 2, + (this.Height - textSize.Height) / 2 + ); + + e.Graphics.DrawString(text, this.Font, Brushes.White, textPos); + } + } +} diff --git a/icon.ico b/icon.ico new file mode 100644 index 0000000..49095f5 Binary files /dev/null and b/icon.ico differ diff --git a/lsl.dll b/lsl.dll new file mode 100644 index 0000000..cf5af42 Binary files /dev/null and b/lsl.dll differ