-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathWatchPatPacket.cs
More file actions
239 lines (206 loc) · 7.96 KB
/
WatchPatPacket.cs
File metadata and controls
239 lines (206 loc) · 7.96 KB
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
50
51
52
53
54
55
56
57
58
59
60
61
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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
using System;
using System.Collections.Generic;
using System.Linq;
namespace WatchPatBLE;
/// <summary>
/// WatchPAT binary packet structure - matches Android DeviceCommands.java
/// Based on reverse-engineered protocol from ITAMAR Medical device
/// </summary>
public class WatchPatPacket
{
// Magic number for packet header
private const ushort MAGIC_NUMBER = 0xBBBB;
// Command IDs (from DeviceCommands.java)
public enum CommandId : ushort
{
Ack = 0x0000, // 0 decimal - ACK packet
StartSession = 0x0100, // 256 decimal - SessionStartCommandPacket
StopAcquisition = 0x0700, // 1792
StartAcquisition = 0x0600, // 1536
ResetDevice = 0x0B00, // 2816
GetParametersFile = 0x0D00, // 3328
SetParametersFile = 0x0C00, // 3072
SendStoredData = 0x1000, // 4096
BitRequest = 0x1200, // 4608
TechnicalStatusRequest = 0x1500, // 5376 - GET STATUS!
GetEEPROM = 0x1D00, // 7424
SetEEPROM = 0x1F00, // 7936
SetLEDs = 0x2300, // 8960
SetDeviceSerial = 0x2400, // 9216
StartFingerDetection = 0x2500, // 9472
ClearData = 0x2700, // 9984
IsDevicePaired = 0x2A00, // 10752
FWUpgradeRequest = 0x3000, // 12288
ResetReason = 0x3900, // 14592
GetLogFile = 0x4400, // 17408
LogFileResponse = 0x4500, // 17664
SetNightsCounter = 0x4600 // 17920
}
/// <summary>
/// 24-byte packet header structure
/// </summary>
public class Header
{
public ushort Magic { get; set; } = MAGIC_NUMBER;
public ushort CommandId { get; set; }
public long Timestamp { get; set; }
public int TransactionId { get; set; }
public ushort Length { get; set; }
public ushort Flags { get; set; }
public ushort Zero { get; set; } = 0;
public ushort Crc { get; set; }
/// <summary>
/// Convert header to byte array (24 bytes)
/// </summary>
public byte[] ToBytes()
{
var buffer = new List<byte>();
// Java ByteBuffer writes big-endian, but pre-reverses values
// C# BitConverter writes little-endian natively
// So: DON'T reverse values that Java already reversed!
buffer.AddRange(BitConverter.GetBytes(ReverseBytes(Magic))); // Needs reverse (not pre-reversed in Java)
buffer.AddRange(BitConverter.GetBytes(ReverseBytes(CommandId))); // Needs reverse (not pre-reversed in Java)
buffer.AddRange(BitConverter.GetBytes(ReverseBytes(Timestamp))); // Needs reverse (IS pre-reversed in Java)
buffer.AddRange(BitConverter.GetBytes(TransactionId)); // NO reverse (IS pre-reversed in Java)
buffer.AddRange(BitConverter.GetBytes(Length)); // NO reverse (IS pre-reversed in Java)
buffer.AddRange(BitConverter.GetBytes(Flags)); // NO reverse (not pre-reversed in Java)
buffer.AddRange(BitConverter.GetBytes(Zero)); // NO reverse (always 0)
buffer.AddRange(BitConverter.GetBytes(Crc)); // NO reverse (IS pre-reversed in Java)
return buffer.ToArray();
}
}
private Header _header;
private byte[] _payload;
private byte[] _builtPacket;
private static int _nextTransactionId = 1;
public WatchPatPacket(CommandId commandId, byte[] payload = null, ushort flags = 0, long timestamp = 0, int? transactionId = null)
{
_header = new Header
{
CommandId = (ushort)commandId,
Timestamp = timestamp,
TransactionId = transactionId ?? GetNextTransactionId(), // Use provided ID or generate new one
Length = (ushort)(24 + (payload?.Length ?? 0)),
Flags = flags
};
_payload = payload ?? Array.Empty<byte>();
}
/// <summary>
/// Generate next transaction ID
/// </summary>
private static int GetNextTransactionId()
{
return _nextTransactionId++;
}
/// <summary>
/// Build complete packet with CRC (cached to avoid recalculation)
/// </summary>
public byte[] Build()
{
// Return cached packet if already built
if (_builtPacket != null)
{
return _builtPacket;
}
// Combine header and payload
var headerBytes = _header.ToBytes();
var fullPacket = new byte[headerBytes.Length + _payload.Length];
Array.Copy(headerBytes, 0, fullPacket, 0, headerBytes.Length);
Array.Copy(_payload, 0, fullPacket, headerBytes.Length, _payload.Length);
// Calculate CRC (excluding the CRC field itself, but we set it to 0 first)
_header.Crc = CalculateCrc16(fullPacket);
// Rebuild with correct CRC
headerBytes = _header.ToBytes();
Array.Copy(headerBytes, 0, fullPacket, 0, headerBytes.Length);
// Cache the result
_builtPacket = fullPacket;
return fullPacket;
}
/// <summary>
/// Split packet into 20-byte chunks for BLE transmission
/// From DeviceCommands.java line 82-96
/// </summary>
public List<byte[]> SplitIntoChunks()
{
var fullPacket = Build();
var chunks = new List<byte[]>();
int offset = 0;
while (offset < fullPacket.Length)
{
int chunkSize = Math.Min(20, fullPacket.Length - offset);
byte[] chunk = new byte[chunkSize];
Array.Copy(fullPacket, offset, chunk, 0, chunkSize);
chunks.Add(chunk);
offset += chunkSize;
}
return chunks;
}
/// <summary>
/// Calculate CRC-16 checksum
/// From DeviceCommands.java line 407-431
/// </summary>
public static ushort CalculateCrc16(byte[] data)
{
ushort crc = 0xFFFF; // Start with -1 (all bits set)
for (int i = 0; i < data.Length; i++)
{
byte currentByte = data[i];
// Process each bit
for (ushort mask = 0x80; mask > 0; mask >>= 1)
{
bool xorFlag = (crc & 0x8000) != 0; // Check MSB
if ((currentByte & mask) != 0)
{
xorFlag = !xorFlag;
}
crc <<= 1; // Shift left
if (xorFlag)
{
crc ^= 0x1021; // Polynomial for CRC-16-CCITT
}
}
crc &= 0xFFFF; // Keep it 16-bit
}
return crc;
}
/// <summary>
/// Reverse bytes for endianness conversion (little-endian)
/// </summary>
private static ushort ReverseBytes(ushort value)
{
return (ushort)((value >> 8) | (value << 8));
}
private static int ReverseBytes(int value)
{
return (int)((value >> 24) & 0xFF) |
(int)((value >> 8) & 0xFF00) |
(int)((value << 8) & 0xFF0000) |
(int)((value << 24) & 0xFF000000);
}
private static long ReverseBytes(long value)
{
return ((value >> 56) & 0xFF) |
((value >> 40) & 0xFF00) |
((value >> 24) & 0xFF0000) |
((value >> 8) & 0xFF000000) |
((value << 8) & 0xFF00000000L) |
((value << 24) & 0xFF0000000000L) |
((value << 40) & 0xFF000000000000L) |
((value << 56) & unchecked((long)0xFF00000000000000L));
}
/// <summary>
/// Format packet as hex string for logging
/// </summary>
public string ToHexString()
{
var fullPacket = Build();
return BitConverter.ToString(fullPacket).Replace("-", " ");
}
/// <summary>
/// Get command name for logging
/// </summary>
public string GetCommandName()
{
return ((CommandId)_header.CommandId).ToString();
}
}