diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiTruSleepParser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiTruSleepParser.java index 1f9f46840..ec24dfa50 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiTruSleepParser.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiTruSleepParser.java @@ -16,10 +16,22 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.devices.huawei; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Arrays; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiFileDownloadManager; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider; +import nodomain.freeyourgadget.gadgetbridge.util.GB; public class HuaweiTruSleepParser { + private static final Logger LOG = LoggerFactory.getLogger(HuaweiTruSleepParser.class); + public static final int TAG_ACC = 1; + public static final int TAG_PPG = 2; public static class TruSleepStatus { public final int startTime; @@ -45,7 +57,307 @@ public class HuaweiTruSleepParser { '}'; } } + public static class TruSleepDataAcc { + public final int startTime; + public final short flags; + /* + Accelerometer (ACC): + Accelerometer data is sampled every 60 seconds, 24h a day and provides activity data on 3 axis. + Each axis activity is represented as byte, where 0x00 is no activity and 0xff is high activity. + The 3rd axis is ignored as it's always high (due to gravity??). + */ + public TruSleepDataAcc(int startTime, short flags) { + this.startTime = startTime; + this.flags = flags; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + TruSleepDataAcc that = (TruSleepDataAcc) o; + if (startTime != that.startTime) + return false; + return flags == that.flags; + } + + @Override + public String toString() { + return "TruSleepDataAcc{" + + "startTime=" + startTime + + ", flags=" + flags + + '}'; + } + } + + /* + Photoplethysmogram (PPG): + Every sample has a millisecond timestamp. + At night data is sampled every second, but at day it's sampled less often (about every 5 minutes). + */ + public static class TruSleepDataPpg { + public final long startTime; + public final short flags; + + TruSleepDataPpg(long startTime, short flags) { + this.startTime = startTime; + this.flags = flags; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + TruSleepDataPpg that = (TruSleepDataPpg) o; + if (startTime != that.startTime) + return false; + return flags == that.flags; + } + + @Override + public String toString() { + return "TruSleepDataPpg{" + + "startTime=" + startTime + + ", flags=" + flags + + '}'; + } + } + public static class TruSleepData { + public ArrayList dataACCs; + public ArrayList dataPPGs; + + public TruSleepData() { + this.dataACCs = new ArrayList<>(); + this.dataPPGs = new ArrayList<>(); + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + TruSleepData that = (TruSleepData) o; + if (!dataACCs.equals(that.dataACCs)) + return false; + return dataPPGs.equals(that.dataPPGs); + } + + @Override + public String toString() { + return "TruSleepData{" + + "dataPPGs=" + dataPPGs.toString() + + ", dataACCs=" + dataACCs.toString() + + '}'; + } + + private static final byte TAG_COMPRESSION_RAW = (byte)0xaa; + private static final byte TAG_COMPRESSION_COMP = (byte)0xbb; + private static final byte TAG_COMPRESSION_RESTART = (byte)0xff; + + public void decodeAcc(byte[] data) throws IllegalArgumentException { + /* + Format: + - Timestamp (4 byte) + - Flags (3 bytes) + */ + ByteBuffer buffer = ByteBuffer.wrap(data); + buffer.order(ByteOrder.LITTLE_ENDIAN); + if (buffer.remaining() < 4) + throw new IllegalArgumentException("Timestamp is missing"); + + int startTime = buffer.getInt(); + if (buffer.remaining() < 2) + throw new IllegalArgumentException("Flags are missing"); + + short flags = buffer.getShort(); + dataACCs.add(new TruSleepDataAcc(startTime, flags)); + } + + public void decodePpg(byte[] data) throws IllegalArgumentException, IllegalStateException { + /* + Format: + - Timestamp (4 byte) + - Number of UINT16 (1 byte) + - Compression tag (1 byte) + - Compressed data + - Compression tag (1 byte) + - Compressed data + */ + ByteBuffer buffer = ByteBuffer.wrap(data); + buffer.order(ByteOrder.LITTLE_ENDIAN); + + if (buffer.remaining() < 4) + throw new IllegalArgumentException("Timestamp is missing"); + + int startTime = buffer.getInt(); + + if (buffer.remaining() < 1) + throw new IllegalArgumentException("Count of short is missing"); + + byte countShort = buffer.get(); + + ArrayList peak = decodePeak(countShort, buffer); + ArrayList amp = decodeAmp(countShort, buffer); + + // Sanity check + if (peak.size() != countShort || amp.size() != countShort) + throw new IllegalStateException("Decoded arrays have different length"); + + for (int i = 0; i < countShort; i++) { + dataPPGs.add(new TruSleepDataPpg((long)startTime * 1000 + peak.get(i) * 10, amp.get(i))); + } + LOG.debug("Buffer remaining {}", buffer.remaining()); + } + + private ArrayList decodePeak(byte countShort, ByteBuffer buffer) throws IllegalArgumentException { + if (countShort == 0) + throw new IllegalArgumentException("Number of short to generate is invalid"); + + if (buffer.remaining() < 1) + throw new IllegalArgumentException("Compression tag is missing"); + + byte tag = buffer.get(); + + ArrayList al = new ArrayList(); + if (tag == TAG_COMPRESSION_RAW) { + if (buffer.remaining() < countShort * 2) + throw new IllegalArgumentException("Not enough elements in buffer"); + + while (countShort > 0) { + al.add(buffer.getShort()); + countShort--; + } + } else if (tag == TAG_COMPRESSION_COMP) { + short working = buffer.getShort(); + al.add(working); + countShort--; + + while (countShort > 0) { + byte c = buffer.get(); + if (c == TAG_COMPRESSION_RESTART) { + if (buffer.remaining() < 2) + throw new IllegalArgumentException("Not enough elements in buffer"); + working = buffer.getShort(); + } else { + working += c; + } + al.add(working); + countShort--; + } + } else { + throw new IllegalArgumentException("Compression " + String.format("%02x", tag) + " is unsupported"); + } + return al; + } + + public ArrayList decodeAmp(byte countShort, ByteBuffer buffer) throws IllegalArgumentException { + if (countShort == 0) + throw new IllegalArgumentException("Number of short to generate is invalid"); + + if (buffer.remaining() < 1) + throw new IllegalArgumentException("Compression tag is missing"); + + byte tag = buffer.get(); + + ArrayList al = new ArrayList(); + if (tag == TAG_COMPRESSION_RAW) { + if (buffer.remaining() < countShort * 2) + throw new IllegalArgumentException("Not enough elements in buffer"); + + while (countShort > 0) { + al.add(buffer.getShort()); + countShort--; + } + } else if (tag == TAG_COMPRESSION_COMP) { + if (buffer.remaining() < 2) + throw new IllegalArgumentException("Offset buffer is missing"); + + short offset = buffer.getShort(); + short working = 0; + while (buffer.remaining() > 0) { + byte c = buffer.get(); + if (c == TAG_COMPRESSION_RESTART) { + if (buffer.remaining() < 2) + throw new IllegalArgumentException("Raw buffer is missing"); + working = (short)(offset + buffer.getShort()); + } else { + working = (short)(offset + c); + } + al.add(working); + } + } else { + throw new IllegalArgumentException("Compression " + String.format("%02x", tag) + " is unsupported"); + } + return al; + } + + public void parsePpgData(byte[] data) { + LOG.debug("Decoding PPG: len= {}, data = {}", data.length, GB.hexdump(data)); + + try { + decodePpg(data); + } catch (Exception e) { + LOG.error("Failed to parse TrueSleep PPG data: {}", e.toString()); + } + } + public void parseAccData(byte[] data) { + LOG.debug("Decoding ACC: len= {}, data = {}", data.length, GB.hexdump(data)); + + try { + decodeAcc(data); + } catch (Exception e) { + LOG.error("Failed to parse TrueSleep ACC data: {}", e.toString()); + } + } + } + + public static TruSleepData parseData(byte[] data) { + /* + Format: + - Zero padded + - ACC data interleaved with PPG data + - PPG data is compressed and has variable length + */ + TruSleepData sleepData = new TruSleepData(); + ByteBuffer buffer = ByteBuffer.wrap(data); + buffer.order(ByteOrder.LITTLE_ENDIAN); + byte c; + while (buffer.remaining() > 0) { + c = buffer.get(); + + // Skip over padding + if (c != TAG_ACC && c != TAG_PPG) + continue; + + // The tag identifies the data packet + if (c == TAG_ACC) { + if (buffer.remaining() < 1) + break; + + // ACC has 1 byte length + byte length = buffer.get(); + if (buffer.remaining() < length) + break; + + byte[] accData = new byte[length]; + buffer.get(accData); + + sleepData.parseAccData(accData); + } else if (c == TAG_PPG) { + if (buffer.remaining() < 2) + break; + + // PPG has 2 byte length + short length = buffer.getShort(); + if (buffer.remaining() < length) + break; + + byte[] ppgData = new byte[length]; + buffer.get(ppgData); + + sleepData.parsePpgData(ppgData); + } + } + + return sleepData; + } public static TruSleepStatus[] parseState(byte[] stateData) { /* Format: @@ -63,6 +375,7 @@ public class HuaweiTruSleepParser { while (stateData.length - buffer.position() >= 0x10) { int startTime = buffer.getInt(); int endTime = buffer.getInt(); + // Throw away for now because we don't know what it means, and we don't think we can implement this soon buffer.get(); buffer.get(); buffer.get(); buffer.get(); buffer.get(); buffer.get(); buffer.get(); buffer.get(); @@ -71,4 +384,91 @@ public class HuaweiTruSleepParser { } return retv; } + + private static void analyzeOneTimeframe(HuaweiSupportProvider provider, TruSleepStatus status, ArrayList acc, ArrayList ppg) { + java.util.Date rangeStartTime = new java.util.Date((long)status.startTime*1000); + java.util.Date rangeEndTime = new java.util.Date((long)status.endTime*1000); + LOG.debug("FIXME: Analyse sleep range {} - {} not implemented", rangeStartTime, rangeEndTime); + //FIXME: Sleep analysis algorithm implemented here + for (HuaweiTruSleepParser.TruSleepDataAcc i : acc) { + java.util.Date startTime = new java.util.Date((long)i.startTime*1000); + LOG.debug("TruSleepDataAcc {} : {}", startTime, String.format("%04x", i.flags)); + } + for (HuaweiTruSleepParser.TruSleepDataPpg i : ppg) { + java.util.Date startTime = new java.util.Date((long) i.startTime); + LOG.debug("TruSleepDataPpg {} : {}", startTime, String.format("%04x", i.flags)); + } + } + + public static void analyze(HuaweiSupportProvider provider, TruSleepStatus[] status, TruSleepData data) { + + // Analyse each time range as specified by TruSleepStatus + for (TruSleepStatus s : status) { + + ArrayList acc = new ArrayList<>(); + ArrayList ppg = new ArrayList<>(); + + // Collect Accelerometer data for current time frame + for (TruSleepDataAcc i : data.dataACCs) { + if (i.startTime >= s.startTime && i.startTime <= s.endTime) + acc.add(i); + } + + // Collect PPG data for current time frame + for (TruSleepDataPpg i : data.dataPPGs) { + if (i.startTime >= s.startTime && i.startTime <= s.endTime) + ppg.add(i); + } + + // Analyse time frame + analyzeOneTimeframe(provider, s, acc, ppg); + } + } + + public static class SleepFileDownloadCallback extends HuaweiFileDownloadManager.FileDownloadCallback { + private byte[] statusData; + private byte[] sleepData; + + private boolean statusSynced; + private boolean dataSynced; + protected HuaweiSupportProvider provider; + public SleepFileDownloadCallback(HuaweiSupportProvider provider) { + this.dataSynced = false; + this.statusSynced = false; + this.provider = provider; + } + + public void syncComplete(byte[] statusData, byte[] sleepData) { } + @Override + public void downloadException(HuaweiFileDownloadManager.HuaweiFileDownloadException e) { + if (e.fileRequest == null) { + LOG.error("Failed to download TruSleep file: {}", e.toString()); + syncComplete(statusData, sleepData); + return; + } + + if (e.fileRequest.getFileType() == HuaweiFileDownloadManager.FileType.SLEEP_STATE) { + statusSynced = true; + } else if (e.fileRequest.getFileType() == HuaweiFileDownloadManager.FileType.SLEEP_DATA) { + dataSynced = true; + } + if (statusSynced && dataSynced) + syncComplete(statusData, sleepData); + } + + @Override + public void downloadComplete(HuaweiFileDownloadManager.FileRequest fileRequest) { + if (fileRequest.getFileType() == HuaweiFileDownloadManager.FileType.SLEEP_STATE) { + statusData = fileRequest.getData(); + statusSynced = true; + } else if (fileRequest.getFileType() == HuaweiFileDownloadManager.FileType.SLEEP_DATA) { + sleepData = fileRequest.getData(); + dataSynced = true; + } + LOG.debug("Downloaded TruSleep file {}", fileRequest.getFileType()); + if (statusSynced && dataSynced) + syncComplete(statusData, sleepData); + } + + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java index 00595a1ca..edbd15935 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java @@ -31,6 +31,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; import java.io.IOException; import java.util.ArrayList; @@ -2379,30 +2381,37 @@ public class HuaweiSupportProvider { public void onTestNewFunction() { // Show to user - gbDevice.setBusyTask("Downloading file..."); + gbDevice.setBusyTask("Downloading files..."); gbDevice.sendDeviceUpdateIntent(getContext()); + HuaweiTruSleepParser.SleepFileDownloadCallback callback = new HuaweiTruSleepParser.SleepFileDownloadCallback(this) { + @Override + public void syncComplete(byte[] statusData, byte[] sleepData) { + LOG.debug("Sync of TruSleep status and data finished"); + + if (statusData == null || statusData.length == 0) + return; + + HuaweiTruSleepParser.TruSleepStatus[] results = HuaweiTruSleepParser.parseState(statusData); + if (results.length == 0) + return; + + HuaweiTruSleepParser.TruSleepData data = HuaweiTruSleepParser.parseData(sleepData); + HuaweiTruSleepParser.analyze(this.provider, results, data); + } + }; + huaweiFileDownloadManager.addToQueue(HuaweiFileDownloadManager.FileRequest.sleepStateFileRequest( getHuaweiCoordinator().getSupportsTruSleepNewSync(), 0, (int) (System.currentTimeMillis() / 1000), - new HuaweiFileDownloadManager.FileDownloadCallback() { - @Override - public void downloadComplete(HuaweiFileDownloadManager.FileRequest fileRequest) { - // Handle file contents - } - } + callback ), true); huaweiFileDownloadManager.addToQueue(HuaweiFileDownloadManager.FileRequest.sleepDataFileRequest( getHuaweiCoordinator().getSupportsTruSleepNewSync(), 0, (int) (System.currentTimeMillis() / 1000), - new HuaweiFileDownloadManager.FileDownloadCallback() { - @Override - public void downloadComplete(HuaweiFileDownloadManager.FileRequest fileRequest) { - // Handle file contents - } - } + callback ), true); } } diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/TestHuaweiTruSleepParser.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/TestHuaweiTruSleepParser.java index 5aafe650b..f72442001 100644 --- a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/TestHuaweiTruSleepParser.java +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/TestHuaweiTruSleepParser.java @@ -19,6 +19,8 @@ package nodomain.freeyourgadget.gadgetbridge.devices.huawei; import org.junit.Assert; import org.junit.Test; +import java.util.ArrayList; + import nodomain.freeyourgadget.gadgetbridge.util.GB; public class TestHuaweiTruSleepParser { @@ -36,4 +38,192 @@ public class TestHuaweiTruSleepParser { Assert.assertArrayEquals(expected, result); } + @Test + public void testParseDataPpg() { + byte[] test1 = GB.hexStringToByteArray("02B000403B0B672AAADA13C109A915E707BE130C06E3112104F80F84182F0888109900390805121004850E4317EC07C911D4037610C701580E2A02310F6B022B100E0448119B075B154F099916390A07191A0E9B00D10E2D02A7105405AAE8FDE8FDBDDA92B767943B71104EE52AB907EE06AC08BD06BE067C07DB06CC05E2079B06C206E906920639063807E0056406D905E2067807870660074D03720A8406AA0624064005CA07DF062D088706D2084B0A00"); + byte[] test2 = GB.hexStringToByteArray("027500F61E0B6724BB0C016968685756585753545050535354565B595B5B5B5F5C5A5E5B61635D5E5B5756585656AADD401138452F7826FC1D61221F27DD2BFC1A7B248B11260A7A221E1C9128FE185025C827D41C1B206223E31B73126210EC22B620D212B232DC0A93115A322E279B116717F30E2E1500"); + byte[] test3 = GB.hexStringToByteArray("025A00B2180B671BBB410174706A726F6D6D747B757B776E6E6D6E6F777D77777670757570AA543D0E52276956406837B63C103EB22C6929C7265326EA2FCC2F743CC9380239673B9739EA2FC92CC526BB2E652D6433DD3506397E2F00"); + + // TODO: Unlike PPGs with 0xBB compression the timestamps are not linear + // The data also looks different, hmmm.... + ArrayList expected = new ArrayList<>(); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789362820L, (short)0xfde8)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789336970L, (short)0xfde8)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789367450L, (short)0xdabd)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789332230L, (short)0xb792)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789362540L, (short)0x9467)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789327480L, (short)0x713b)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789357790L, (short)0x4e10)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789322570L, (short)0x2ae5)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789352880L, (short)0x07b9)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789374760L, (short)0x06ee)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789332950L, (short)0x08ac)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789354320L, (short)0x06bd)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789313530L, (short)0x06be)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789333050L, (short)0x077c)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789358130L, (short)0x06db)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789322400L, (short)0x05cc)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789349170L, (short)0x07e2)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789371550L, (short)0x069b)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789332280L, (short)0x06c2)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789357530L, (short)0x06e9)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789321800L, (short)0x0692)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789354140L, (short)0x0639)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789316550L, (short)0x0738)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789348720L, (short)0x05e0)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789317540L, (short)0x0664)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789350890L, (short)0x05d9)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789318190L, (short)0x06e2)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789353390L, (short)0x0778)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789322380L, (short)0x0687)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789356240L, (short)0x0760)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789331470L, (short)0x034d)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789366670L, (short)0x0a72)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789335830L, (short)0x0684)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789369850L, (short)0x06aa)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789338170L, (short)0x0624)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789376070L, (short)0x0540)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789348100L, (short)0x07ca)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789313550L, (short)0x06df)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789349930L, (short)0x082d)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789317570L, (short)0x0687)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789354630L, (short)0x08d2)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728789325640L, (short)0x0a4b)); + + HuaweiTruSleepParser.TruSleepData result = HuaweiTruSleepParser.parseData(test1); + Assert.assertEquals(expected, result.dataPPGs); + + expected = new ArrayList<>(); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728782072680L, (short)0x40dd)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728782073730L, (short)0x3811)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728782074770L, (short)0x2f45)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728782075810L, (short)0x2678)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728782076680L, (short)0x1dfc)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728782077540L, (short)0x2261)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728782078420L, (short)0x271f)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728782079290L, (short)0x2bdd)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728782080120L, (short)0x1afc)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728782080960L, (short)0x247b)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728782081760L, (short)0x118b)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728782082560L, (short)0x0a26)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728782083390L, (short)0x227a)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728782084220L, (short)0x1c1e)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728782085060L, (short)0x2891)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728782085920L, (short)0x18fe)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728782086830L, (short)0x2550)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728782087720L, (short)0x27c8)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728782088630L, (short)0x1cd4)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728782089540L, (short)0x201b)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728782090450L, (short)0x2362)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728782091400L, (short)0x1be3)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728782092320L, (short)0x1273)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728782093220L, (short)0x1062)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728782094160L, (short)0x22ec)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728782095070L, (short)0x20b6)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728782096040L, (short)0x12d2)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728782097030L, (short)0x32b2)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728782097960L, (short)0x0adc)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728782098900L, (short)0x1193)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728782099810L, (short)0x325a)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728782100680L, (short)0x272e)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728782101540L, (short)0x119b)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728782102420L, (short)0x1767)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728782103280L, (short)0x0ef3)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728782104140L, (short)0x152e)); + + result = HuaweiTruSleepParser.parseData(test2); + Assert.assertEquals(expected, result.dataPPGs); + + expected = new ArrayList<>(); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728780469210L, (short)0x3d54)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728780470370L, (short)0x520e)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728780471490L, (short)0x6927)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728780472550L, (short)0x4056)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728780473690L, (short)0x3768)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728780474800L, (short)0x3cb6)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728780475890L, (short)0x3e10)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728780476980L, (short)0x2cb2)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728780478140L, (short)0x2969)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728780479370L, (short)0x26c7)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728780480540L, (short)0x2653)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728780481770L, (short)0x2fea)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728780482960L, (short)0x2fcc)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728780484060L, (short)0x3c74)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728780485160L, (short)0x38c9)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728780486250L, (short)0x3902)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728780487350L, (short)0x3b67)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728780488460L, (short)0x3997)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728780489650L, (short)0x2fea)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728780490900L, (short)0x2cc9)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728780492090L, (short)0x26c5)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728780493280L, (short)0x2ebb)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728780494460L, (short)0x2d65)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728780495580L, (short)0x3364)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728780496750L, (short)0x35dd)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728780497920L, (short)0x3906)); + expected.add(new HuaweiTruSleepParser.TruSleepDataPpg (1728780499040L, (short)0x2f7e)); + + result = HuaweiTruSleepParser.parseData(test3); + Assert.assertEquals(expected, result.dataPPGs); + } + + @Test + public void testParseDataAcc() { + byte[] test = GB.hexStringToByteArray("0107F02A0B670000B6"); + + ArrayList expected = new ArrayList<>(); + expected.add(new HuaweiTruSleepParser.TruSleepDataAcc (0x670b2af0, (short)0x0000)); + + HuaweiTruSleepParser.TruSleepData result = HuaweiTruSleepParser.parseData(test); + Assert.assertEquals(0, result.dataPPGs.size()); + Assert.assertEquals(expected, result.dataACCs); + } + + @Test + public void testParsePadding() { + byte[] test1 = GB.hexStringToByteArray("000107F02A0B670000B6"); + byte[] test2 = GB.hexStringToByteArray("00000000000107F02A0B670000B6"); + + ArrayList expected = new ArrayList<>(); + expected.add(new HuaweiTruSleepParser.TruSleepDataAcc (0x670b2af0, (short)0x0000)); + + HuaweiTruSleepParser.TruSleepData result = HuaweiTruSleepParser.parseData(test1); + + Assert.assertEquals(0, result.dataPPGs.size()); + Assert.assertEquals(expected, result.dataACCs); + + result = HuaweiTruSleepParser.parseData(test2); + + Assert.assertEquals(0, result.dataPPGs.size()); + Assert.assertEquals(expected, result.dataACCs); + } + + @Test + public void testParseFailure() { + ArrayList tests = new ArrayList(); + // Length shorter than expected + tests.add(GB.hexStringToByteArray("0105F02A0B670000B6")); + // Zero length + tests.add(GB.hexStringToByteArray("0100F02A0B670000B6")); + // Length bigger than input data + tests.add(GB.hexStringToByteArray("0108F02A0B670000B6")); + + // Length bigger than input data + tests.add(GB.hexStringToByteArray("027600F61E0B6724BB0C016968685756585753545050535354565B595B5B5B5F5C5A5E5B61635D5E5B5756585656AADD401138452F7826FC1D61221F27DD2BFC1A7B248B11260A7A221E1C9128FE185025C827D41C1B206223E31B73126210EC22B620D212B232DC0A93115A322E279B116717F30E2E1500")); + // Number of UINT16 less than provided + tests.add(GB.hexStringToByteArray("027500F61E0B6723BB0C016968685756585753545050535354565B595B5B5B5F5C5A5E5B61635D5E5B5756585656AADD401138452F7826FC1D61221F27DD2BFC1A7B248B11260A7A221E1C9128FE185025C827D41C1B206223E31B73126210EC22B620D212B232DC0A93115A322E279B116717F30E2E1500")); + // Wrong compression tag + tests.add(GB.hexStringToByteArray("025A00B2180B671BCC410174706A726F6D6D747B757B776E6E6D6E6F777D77777670757570AA543D0E52276956406837B63C103EB22C6929C7265326EA2FCC2F743CC9380239673B9739EA2FC92CC526BB2E652D6433DD3506397E2F00")); + // Wrong compression tag 2 + tests.add(GB.hexStringToByteArray("025A00B2180B671BBB410174706A726F6D6D747B757B776E6E6D6E6F777D77777670757570DD543D0E52276956406837B63C103EB22C6929C7265326EA2FCC2F743CC9380239673B9739EA2FC92CC526BB2E652D6433DD3506397E2F00")); + // Wrong restart tags in compressed data + tests.add(GB.hexStringToByteArray("025A00B2180B671BBB410FFFFFFA726F6D6D747B757B776E6E6D6E6F777D77777670757570AA543D0E52276956406837B63C103EB22C6929C7265326EA2FCC2F743CC9380239673B9739EA2FC92CC526BB2E652D6433DD3506397E2F00")); + + for (byte[] test: tests) { + HuaweiTruSleepParser.TruSleepData result = HuaweiTruSleepParser.parseData(test); + + Assert.assertEquals("Test input data " + GB.hexdump(test), 0, result.dataPPGs.size()); + Assert.assertEquals("Test input data " + GB.hexdump(test),0, result.dataACCs.size()); + } + } }