Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue70 read matrix #71

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ URL: https://github.com/wadpac/GGIRread/
BugReports: https://github.com/wadpac/GGIRread/issues
License: Apache License (== 2.0)
Suggests: testthat
Imports: matlab, bitops, Rcpp (>= 0.12.10), data.table, readxl, jsonlite
Imports: matlab, bitops, Rcpp (>= 0.12.10), data.table, readxl, jsonlite, digest
Depends: stats, utils, R (>= 3.5.0)
NeedsCompilation: yes
LinkingTo: Rcpp
Expand Down
2 changes: 1 addition & 1 deletion NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export(readGenea, readAxivity, readGENEActiv,
readActiGraphCount, readActiwatchCount,
readActicalCount, readPHBCount,
readFitbit, mergePHBdata,
mergeFitbitData)
mergeFitbitData, readParmayMatrix)
useDynLib(GGIRread, .registration = TRUE)
importFrom(Rcpp, sourceCpp)
importFrom(data.table, fread)
Expand Down
5 changes: 5 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# Changes in version 1.0.3 (release date:X-X-2025)

- Add functions for reading Parmay Tech Matrix sensor (bin files) with accelerometer, gyroscope, temperature, and heart rate #70.


# Changes in version 1.0.2 (release date:25-10-2024)

- Added a `NEWS.md` file to track changes to the package.
Expand Down
4 changes: 4 additions & 0 deletions R/RcppExports.R
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ GENEActivReader <- function(filename, start = 0L, end = 0L, progress_bar = FALSE
.Call(`_GGIRread_GENEActivReader`, filename, start, end, progress_bar)
}

find_matrix_packet_start <- function(data, pattern) {
.Call(`_GGIRread_find_matrix_packet_start`, data, pattern)
}

resample <- function(raw, rawTime, time, stop, type = 1L) {
.Call(`_GGIRread_resample`, raw, rawTime, time, stop, type)
}
Expand Down
363 changes: 363 additions & 0 deletions R/readParmayMatrix.R

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Brand | Device name | File extension | Data type | GGIRread function
Axivity Ltd https://axivity.com/ | AX3 and AX6 | .cwa | raw gravitational units |readAxivity
ActivInsights Ltd https://activinsights.com/ | GENEActiv Original and Sleep | .bin | raw gravitational units | readGENEActiv
Unilever Discover Ltd | Genea (no longer manufactured) | .bin | raw gravitational units | readGenea
Parmay Tech https://www.parmaytech.com/ | Matrix | .bin | raw gravitational units | readParmayMatrix
ActiGraph | ??? | .csv | count data | readActigraphCount
Actiwatch | ??? | .csv and .awd | count data | readActiwatchCount
Actical | ??? | .csv | count data | readActicalCount
Expand Down
Binary file added inst/testfiles/mtx_100Hz_acc_HR_temp.BIN
Binary file not shown.
Binary file added inst/testfiles/mtx_12.5Hz_acc.BIN
Binary file not shown.
Binary file added inst/testfiles/mtx_25Hz_acc_HR.BIN
Binary file not shown.
111 changes: 111 additions & 0 deletions man/readParmayMatrix.Rd
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
\name{readParmayMatrix}
\alias{readParmayMatrix}
\title{Read and Process Binary Data from Matrix Devices}
\description{
Reads a binary file generated by Parmay Tech Matrix devices, processes its header and packet
data, validates data integrity using CRC32 checksums, and outputs structured
sensor data and quality check information.
}
\usage{
readParmayMatrix(bin_file, return = c("all", "sf", "dynrange")[1], start = 1, end = NULL,
desiredtz = "", configtz = NULL, interpolationType = 1)
}
\arguments{
\item{bin_file}{Character. Path to the binary file to be read.}
\item{return}{Character. Specifies the type of output. Options include:
\describe{
\item{"all"}{Returns the full processed data.}
\item{"sf"}{Returns the sampling frequency of the accelerometer data.}
\item{"dynrange"}{Returns the dynamic range of the accelerometer.}
}
}
\item{start}{Integer. The index of the starting packet to process. Default is 1.}
\item{end}{Integer. The index of the ending packet to process. Default is \code{NULL}, which means all packets are processed.}
\item{desiredtz}{Character. Time zone for the returned timestamps. Default is an empty string, which uses the system's default time zone.}
\item{configtz}{Character. Time zone specified in the file's configuration. Default is \code{NULL}, which means that it uses \code{desiredtz}.}
\item{interpolationType}{Integer. Specifies the type of interpolation
(see \code{\link{resample}}) to use when resampling data: 1 for Linear interpolation,
2 for Nearest-neighbor interpolation.}
}
\details{
Matrix devices store binary data in packets, with varying lengths depending on
the number of sensor recordings in each packet. The function processes the file's
header to extract metadata such as the total number of packets and sensor ranges,
validates data integrity using CRC32 checksums, and interpolates data to a
consistent sampling frequency.

\strong{Header Information:}
\itemize{
\item Remarks: Bytes 1-512.
\item Total packets: Bytes 513-516.
\item Header string: Bytes 517-520. If not "MDTC", the file is considered corrupt.
\item Accelerometer dynamic range: Bytes 521-522.
\item Gyroscope range: Bytes 523-524.
}

\strong{Packet Structure:}

Each packet contains accelerometer, gyroscope, temperature, and heart rate data.

\itemize{
\item 8-byte package header.
\item 4-byte CRC32 indicator.
\item 4-byte start timestamp.
\item 4-byte end timestamp.
\item 4-byte accelerometer recording count.
\item 4-byte gyroscope recording count.
\item 4-byte temperature recording count.
\item 4-byte heart rate recording count.
\item Sensor data for accelerometer, gyroscope, temperature, and heart rate.
}
}
\value{
A list containing the following elements (when \code{return = "all"}):
\itemize{
\item \code{QClog}: A data frame with quality control information, including
checksum validation and data gaps.
\item \code{output}: A data frame with resampled sensor data, including:
\describe{
\item{\code{time}}{Timestamps of the recordings.}
\item{\code{acc_x}, \code{acc_y}, \code{acc_z}}{Resampled accelerometer data.}
\item{\code{gyro_x}, \code{gyro_y}, \code{gyro_z}}{Resampled gyroscope data.}
\item{\code{bodySurface_temp}, \code{ambient_temp}}{Resampled temperature data.}
\item{\code{hr_raw}, \code{hr}}{Resampled heart rate data.}
\item{\code{remarks}}{Remarks extracted from the file header.}
}
\item \code{sf}: Sampling frequency of the accelerometer data.
\item \code{acc_dynrange}: Dynamic range of the accelerometer.
\item \code{starttime}: Start time of the first packet in POSIXct format.
\item \code{lastchunk}: Logical, indicating if the processed data includes the last packet in the file.
}

If \code{return = "sf"}, the function returns only the sampling frequency.

If \code{return = "dynrange"}, it returns the dynamic range of the accelerometer.
}
\examples{
\dontrun{
# Example usage:
# Read full data and process all packets
result <- readParmayMatrix("example_file.bin")

# Get sampling frequency only
sf <- readParmayMatrix("example_file.bin", return = "sf")

# Get accelerometer dynamic range
dynrange <- readParmayMatrix("example_file.bin", return = "dynrange")

# Process a subset of packets
result_subset <- readParmayMatrix("example_file.bin", start = 10, end = 20)
}
}
\references{
For more details on Matrix devices', see:
\url{https://www.parmaytech.com/devices/en-matrix}
}
\seealso{
\code{\link{resample}} for resampling sensor data.
}
\author{
Jairo H Migueles <[email protected]>
}
12 changes: 12 additions & 0 deletions src/RcppExports.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@ BEGIN_RCPP
return rcpp_result_gen;
END_RCPP
}
// find_matrix_packet_start
IntegerVector find_matrix_packet_start(RawVector data, RawVector pattern);
RcppExport SEXP _GGIRread_find_matrix_packet_start(SEXP dataSEXP, SEXP patternSEXP) {
BEGIN_RCPP
Rcpp::RObject rcpp_result_gen;
Rcpp::RNGScope rcpp_rngScope_gen;
Rcpp::traits::input_parameter< RawVector >::type data(dataSEXP);
Rcpp::traits::input_parameter< RawVector >::type pattern(patternSEXP);
rcpp_result_gen = Rcpp::wrap(find_matrix_packet_start(data, pattern));
return rcpp_result_gen;
END_RCPP
}
// resample
NumericMatrix resample(NumericMatrix raw, NumericVector rawTime, NumericVector time, int stop, int type);
RcppExport SEXP _GGIRread_resample(SEXP rawSEXP, SEXP rawTimeSEXP, SEXP timeSEXP, SEXP stopSEXP, SEXP typeSEXP) {
Expand Down
23 changes: 23 additions & 0 deletions src/find_matrix_packet_start.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#include <Rcpp.h>
using namespace Rcpp;

// [[Rcpp::export]]
IntegerVector find_matrix_packet_start(RawVector data, RawVector pattern) {
int data_len = data.size();
int pattern_len = pattern.size();
IntegerVector indices;

for (int i = 0; i <= data_len - pattern_len; i++) {
bool match = true;
for (int j = 0; j < pattern_len; j++) {
if (data[i + j] != pattern[j]) {
match = false;
break;
}
}
if (match) {
indices.push_back(i + 1); // Use 1-based index for R
}
}
return indices;
}
2 changes: 2 additions & 0 deletions src/init.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
extern SEXP _GGIRread_AxivityNumUnpack(SEXP);
extern SEXP _GGIRread_resample(SEXP, SEXP, SEXP, SEXP, SEXP);
extern SEXP _GGIRread_GENEActivReader(SEXP, SEXP, SEXP, SEXP);
extern SEXP _GGIRread_find_matrix_packet_start(SEXP, SEXP);

static const R_CallMethodDef CallEntries[] = {
{"_GGIRread_AxivityNumUnpack", (DL_FUNC) &_GGIRread_AxivityNumUnpack, 1},
{"_GGIRread_GENEActivReader", (DL_FUNC) &_GGIRread_GENEActivReader, 4},
{"_GGIRread_resample", (DL_FUNC) &_GGIRread_resample, 5},
{"_GGIRread_find_matrix_packet_start", (DL_FUNC) &_GGIRread_find_matrix_packet_start, 2},
{NULL, NULL, 0}
};

Expand Down
37 changes: 37 additions & 0 deletions tests/testthat/test_readParmayMatrix.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
library(GGIRread)
context("reading Parmay Matrix .bin data")
test_that("File including only accelerometer data at 12.5Hz", {
binfile = system.file("testfiles/mtx_12.5Hz_acc.BIN", package = "GGIRread")[1]
BIN = readParmayMatrix(bin_file = binfile, desiredtz = "Europe/Berlin", start = 1, end = NULL)
expect_equal(BIN$sf, 12.5)
expect_equal(BIN$acc_dynrange, 8)
expect_equal(as.numeric(BIN$starttime), BIN$data$time[1])
expect_equal(nrow(BIN$data), 12587)
expect_equal(ncol(BIN$data), 5) # time, x, y, z, remarks
expect_equal(nrow(BIN$QClog), 2) # 2 packets in file
expect_true(all(BIN$QClog$checksum_pass))
})

test_that("File including accelerometer and heart rate data at 25Hz", {
binfile = system.file("testfiles/mtx_25Hz_acc_HR.BIN", package = "GGIRread")[1]
BIN = readParmayMatrix(bin_file = binfile, desiredtz = "Europe/Berlin", start = 1, end = NULL)
expect_equal(BIN$sf, 25)
expect_equal(BIN$acc_dynrange, 8)
expect_equal(as.numeric(BIN$starttime), BIN$data$time[1])
expect_equal(nrow(BIN$data), 24525)
expect_equal(ncol(BIN$data), 7) # time, x, y, z, hr_raw, hr, remarks
expect_equal(nrow(BIN$QClog), 4) # 4 packets in file
expect_true(all(BIN$QClog$checksum_pass))
})

test_that("File including accelerometer, heart rate, and temperature data at 100Hz", {
binfile = system.file("testfiles/mtx_100Hz_acc_HR_temp.BIN", package = "GGIRread")[1]
BIN = readParmayMatrix(bin_file = binfile, desiredtz = "Europe/Berlin", start = 1, end = NULL)
expect_equal(BIN$sf, 100)
expect_equal(BIN$acc_dynrange, 8)
expect_equal(as.numeric(BIN$starttime), BIN$data$time[1])
expect_equal(nrow(BIN$data), 39400)
expect_equal(ncol(BIN$data), 9) # time, x, y, z, boyd temp, ambient temp, hr_raw, hr, remarks
expect_equal(nrow(BIN$QClog), 4) # 4 packets in file
expect_true(all(BIN$QClog$checksum_pass))
})
Loading