Home Reference Source

src/binary.js

import { FIT } from './fit';
import { getFitMessage, getFitMessageBaseType } from './messages';

export function addEndian(littleEndian, bytes) {
    let result = 0;
    if (!littleEndian) bytes.reverse();
    for (let i = 0; i < bytes.length; i++) {
        result += (bytes[i] << (i << 3)) >>> 0;
    }

    return result;
}

function readData(blob, fDef, startIndex) {
    if (fDef.endianAbility === true) {
        const temp = [];
        for (let i = 0; i < fDef.size; i++) {
            temp.push(blob[startIndex + i]);
        }
        const uint32Rep = addEndian(fDef.littleEndian, temp);

        if (fDef.dataType === 'sint32') {
            return (uint32Rep >> 0);
        }

        return uint32Rep;
    }
    return blob[startIndex];
}

function formatByType(data, type, scale, offset) {
    switch (type) {
        case 'date_time': return new Date((data * 1000) + 631065600000);
        case 'sint32':
        case 'sint16':
            return data * FIT.scConst;
        case 'uint32':
        case 'uint16':
            return scale ? data / scale + offset : data;
        default:
            if (FIT.types[type]) {
                return FIT.types[type][data];
            }
            return data;
    }
}

function isInvalidValue(data, type) {
    switch (type) {
      case 'enum': return data === 0xFF;
      case 'sint8': return data === 0x7F;
      case 'uint8': return data === 0xFF;
      case 'sint16': return data === 0x7FFF;
      case 'unit16': return data === 0xFFFF;
      case 'sint32': return data === 0x7FFFFFFF;
      case 'uint32': return data === 0xFFFFFFFF;
      case 'string': return data === 0x00;
      case 'float32': return data === 0xFFFFFFFF;
      case 'float64': return data === 0xFFFFFFFFFFFFFFFF;
      case 'uint8z': return data === 0x00;
      case 'uint16z': return data === 0x0000;
      case 'uint32z': return data === 0x000000;
      case 'byte': return data === 0xFF;
      case 'sint64': return data === 0x7FFFFFFFFFFFFFFF;
      case 'uint64': return data === 0xFFFFFFFFFFFFFFFF;
      case 'uint64z': return data === 0x0000000000000000;
      default: return false;
    }
  }

function convertTo(data, unitsList, speedUnit) {
    const unitObj = FIT.options[unitsList][speedUnit];
    return unitObj ? data * unitObj.multiplier + unitObj.offset : data;
}

function applyOptions(data, field, options) {
    switch (field) {
        case 'speed':
        case 'enhanced_speed':
        case 'vertical_speed':
        case 'avg_speed':
        case 'max_speed':
        case 'speed_1s':
        case 'ball_speed':
        case 'enhanced_avg_speed':
        case 'enhanced_max_speed':
        case 'avg_pos_vertical_speed':
        case 'max_pos_vertical_speed':
        case 'avg_neg_vertical_speed':
        case 'max_neg_vertical_speed':
            return convertTo(data, 'speedUnits', options.speedUnit);
        case 'distance':
        case 'total_distance':
        case 'enhanced_avg_altitude':
        case 'enhanced_min_altitude':
        case 'enhanced_max_altitude':
        case 'enhanced_altitude':
        case 'height':
        case 'odometer':
        case 'avg_stroke_distance':
        case 'min_altitude':
        case 'avg_altitude':
        case 'max_altitude':
        case 'total_ascent':
        case 'total_descent':
        case 'altitude':
        case 'cycle_length':
        case 'auto_wheelsize':
        case 'custom_wheelsize':
        case 'gps_accuracy':
            return convertTo(data, 'lengthUnits', options.lengthUnit);
        case 'temperature':
        case 'avg_temperature':
        case 'max_temperature':
            return convertTo(data, 'temperatureUnits', options.temperatureUnit);
        default: return data;
    }
}

export function readRecord(blob, messageTypes, startIndex, options, startDate) {
    const recordHeader = blob[startIndex];
    const localMessageType = (recordHeader & 15);

    if ((recordHeader & 64) === 64) {
        // is definition message
        // startIndex + 1 is reserved

        const lEnd = blob[startIndex + 2] === 0;
        const mTypeDef = {
            littleEndian: lEnd,
            globalMessageNumber: addEndian(lEnd, [blob[startIndex + 3], blob[startIndex + 4]]),
            numberOfFields: blob[startIndex + 5],
            fieldDefs: [],
        };

        const message = getFitMessage(mTypeDef.globalMessageNumber);

        for (let i = 0; i < mTypeDef.numberOfFields; i++) {
            const fDefIndex = startIndex + 6 + (i * 3);
            const baseType = blob[fDefIndex + 2];
            const { field, type } = message.getAttributes(blob[fDefIndex]);
            const fDef = {
                type,
                fDefNo: blob[fDefIndex],
                size: blob[fDefIndex + 1],
                endianAbility: (baseType & 128) === 128,
                littleEndian: lEnd,
                baseTypeNo: (baseType & 15),
                name: field,
                dataType: getFitMessageBaseType(baseType & 15),
            };

            mTypeDef.fieldDefs.push(fDef);
        }
        messageTypes[localMessageType] = mTypeDef;

        return {
            messageType: 'fieldDescription',
            nextIndex: startIndex + 6 + (mTypeDef.numberOfFields * 3)
        };
    }

    let messageType;

    if (messageTypes[localMessageType]) {
        messageType = messageTypes[localMessageType];
    } else {
        messageType = messageTypes[0];
    }

    // TODO: handle compressed header ((recordHeader & 128) == 128)

    // uncompressed header
    let messageSize = 0;
    let readDataFromIndex = startIndex + 1;
    const fields = {};
    const message = getFitMessage(messageType.globalMessageNumber);

    for (let i = 0; i < messageType.fieldDefs.length; i++) {
        const fDef = messageType.fieldDefs[i];
        const data = readData(blob, fDef, readDataFromIndex);
        
        if (!isInvalidValue(data, fDef.type)) {
            const { field, type, scale, offset } = message.getAttributes(fDef.fDefNo);
            if (field !== 'unknown' && field !== '' && field !== undefined) {
                fields[field] = applyOptions(formatByType(data, type, scale, offset), field, options);
            }

            if (message.name === 'record' && options.elapsedRecordField) {
                fields.elapsed_time = (fields.timestamp - startDate) / 1000;
            }
        }

        readDataFromIndex += fDef.size;
        messageSize += fDef.size;
    }

    const result = {
        messageType: message.name,
        nextIndex: startIndex + messageSize + 1,
        message: fields,
    };

    return result;

}

export function getArrayBuffer(buffer) {
    if(buffer instanceof ArrayBuffer) {
        return buffer;
    }
    const ab = new ArrayBuffer(buffer.length);
    const view = new Uint8Array(ab);
    for (let i = 0; i < buffer.length; ++i) {
        view[i] = buffer[i];
    }
    return ab;
}

export function calculateCRC(blob, start, end) {
    const crcTable = [
        0x0000, 0xCC01, 0xD801, 0x1400, 0xF001, 0x3C00, 0x2800, 0xE401,
        0xA001, 0x6C00, 0x7800, 0xB401, 0x5000, 0x9C01, 0x8801, 0x4400,
    ];

    let crc = 0;
    for (let i = start; i < end; i++) {
        const byte = blob[i];
        let tmp = crcTable[crc & 0xF];
        crc = (crc >> 4) & 0x0FFF;
        crc = crc ^ tmp ^ crcTable[byte & 0xF];
        tmp = crcTable[crc & 0xF];
        crc = (crc >> 4) & 0x0FFF;
        crc = crc ^ tmp ^ crcTable[(byte >> 4) & 0xF];
    }

    return crc;
}