From bfabf2283e73a4d025f9afb01ae53c814b4e5cb0 Mon Sep 17 00:00:00 2001 From: Jenkins Date: Tue, 10 Feb 2026 11:48:21 -0600 Subject: [PATCH] Garmin FIT SDK 21.194.0 Change-Id: I307e80071b0f3081e2f33150b742260ec1833520 --- README.md | 586 +++++++++++++------------ package.json | 2 +- src/accumulator.js | 6 +- src/bit-stream.js | 6 +- src/crc-calculator.js | 6 +- src/decoder.js | 6 +- src/encoder.js | 35 +- src/fit.js | 52 ++- src/index.js | 6 +- src/mesg-definition.js | 10 +- src/output-stream.js | 19 +- src/profile.js | 28 +- src/stream.js | 6 +- src/utils-hr-mesg.js | 6 +- src/utils-internal.js | 6 +- src/utils-memo-glob.js | 6 +- src/utils.js | 6 +- test/accumulator.test.js | 2 +- test/bit-stream.test.js | 2 +- test/crc.test.js | 2 +- test/data/test-data-expand-hr-mesgs.js | 2 +- test/data/test-data.js | 2 +- test/decoder.test.js | 2 +- test/encode-activity-recipe.test.js | 2 +- test/encoder.test.js | 271 +++++++++--- test/output-stream.test.js | 29 +- test/stream.test.js | 2 +- test/testUtils.js | 124 ++++++ test/utils-hr-mesg.test.js | 2 +- test/utils-memo-glob.test.js | 2 +- test/utils.test.js | 2 +- 31 files changed, 828 insertions(+), 410 deletions(-) create mode 100644 test/testUtils.js diff --git a/README.md b/README.md index 002323e..8c35c4b 100644 --- a/README.md +++ b/README.md @@ -1,288 +1,298 @@ -# Garmin - FIT JavaScript SDK -## FIT SDK Documentation -The FIT SDK documentation is available at [https://developer.garmin.com/fit](https://developer.garmin.com/fit). -## FIT SDK Developer Forum -Share your knowledge, ask questions, and get the latest FIT SDK news in the [FIT SDK Developer Forum](https://forums.garmin.com/developer/). -## FIT JavaScript SDK Requirements -The FIT JavaScript SDK uses ECMAScript module syntax and requires Node.js v14.0 or higher, or a browser with a compatible JavaScript runtime engine. -## Install -```sh -npm install @garmin/fitsdk -``` -## Decoder -### Usage -````js -import { Decoder, Stream, Profile, Utils } from '@garmin/fitsdk'; - -const bytes = [0x0E, 0x10, 0xD9, 0x07, 0x00, 0x00, 0x00, 0x00, 0x2E, 0x46, 0x49, 0x54, 0x91, 0x33, 0x00, 0x00]; - -const stream = Stream.fromByteArray(bytes); -console.log("isFIT (static method): " + Decoder.isFIT(stream)); - -const decoder = new Decoder(stream); -console.log("isFIT (instance method): " + decoder.isFIT()); -console.log("checkIntegrity: " + decoder.checkIntegrity()); - -const { messages, errors } = decoder.read(); - -console.log(errors); -console.log(messages); -```` -### Constructor - -Decoder objects are created from Streams representing the binary FIT file data to be decoded. See [Creating Streams](#creatingstreams) for more information on constructing Stream objects. - -Once a Decoder object is created it can be used to check that the Stream is a FIT file, that the FIT file is valid, and to read the contents of the FIT file. - -### isFIT Method - -All valid FIT files should include a 12 or 14 byte file header. The 14 byte header is the preferred header size and the most common size used. Bytes 8–11 of the header contain the ASCII values ".FIT". This string can easily be spotted when opening a binary FIT file in a text or hex editor. - -````bash - Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F -00000000: 0E 10 43 08 78 06 09 00 2E 46 49 54 96 85 40 00 ..C.x....FIT..@. -00000010: 00 00 00 07 03 04 8C 04 04 86 07 04 86 01 02 84 ................ -00000020: 02 02 84 05 02 84 00 01 00 00 19 28 7E C5 95 B0 ...........(~E.0 -```` - -The isFIT method reads the file header and returns true if bytes 8–11 are equal to the ACSII values ".FIT". isFIT provides a quick way to check that the file is a FIT file before attempting to decode the file. - -The Decoder class includes a static and instance version of the isFIT method. - -### Check Integrity Method - -The checkIntegrity method performs three checks on a FIT file: - -1. Checks that bytes 8–11 of the header contain the ASCII values ".FIT". -2. Checks that the total file size is equal to Header Size + Data Size + CRC Size. -3. Reads the contents of the file, computes the CRC, and then checks that the computed CRC matches the file CRC. - -A file must pass all three of these tests to be considered a valid FIT file. See the [IsFIT(), CheckIntegrity(), and Read() Methods recipe](/fit/cookbook/isfit-checkintegrity-read/) for use-cases where the checkIntegrity method should be used and cases when it might be better to avoid it. - -### Read Method -The Read method decodes all message from the input stream and returns an object containing an array of errors encountered during the decoding and a dictionary of decoded messages grouped by message type. Any exceptions encountered during decoding will be caught by the Read method and added to the array of errors. - -The Read method accepts an optional options object that can be used to customize how field data is represented in the decoded messages. All options are enabled by default. Disabling options may speed up file decoding. Options may also be enabled or disable based on how the decoded data will be used. - -````js -const { messages, errors } = decoder.read({ - mesgListener: (messageNumber, message) => {}, - mesgDefinitionListener: (mesgDefinition) => {}, - fieldDescriptionListener: (key, developerDataIdMesg, fieldDescriptionMesg) => {}, - applyScaleAndOffset: true, - expandSubFields: true, - expandComponents: true, - convertTypesToStrings: true, - convertDateTimesToDates: true, - includeUnknownData: false, - mergeHeartRates: true - decodeMemoGlobs: false, -}); -```` -#### mesgListener = (messageNumber, message) => {} -Optional callback function that can be used to inspect or manipulate messages after they are fully decoded and all the options have been applied. The message is mutable and we be returned from the Read method in the messages dictionary. - -Example mesgListener callback that tracks the field names across all Record messages. -````js -const recordFields = new Set(); - -const onMesg = (messageNumber, message) => { - if (Profile.types.mesgNum[messageNumber] === "record") { - Object.keys(message).forEach(field => recordFields.add(field)); - } -} - -const { messages, errors } = decoder.read({ - mesgListener = onMesg -}); - -console.log(recordFields); -```` -#### mesgDefinitionListener: (mesgDefinition) => {} -Optional callback function that can be used to inspect message defintions as they are decoded from the file. -#### fieldDescriptionListener: (key, developerDataIdMesg, fieldDescriptionMesg) => {} -Optional callback function that can be used to inspect developer field descriptions as they are decoded from the file. -#### applyScaleAndOffset: true | false -When true the scale and offset values as defined in the FIT Profile are applied to the raw field values. -````js -{ - altitude: 1587 // with a scale of 5 and offset of 500 applied -} -```` -When false the raw field value is used. -````js -{ - altitude: 10435 // raw value store in file -} -```` -#### expandSubFields: true | false -When true subfields are created for fields as defined in the FIT Profile. -````js -{ - event: 'rearGearChange', - data: 16717829, - gearChangeData:16717829 // Sub Field of data when event == 'rearGearChange' -} -```` -When false subfields are omitted. -````js -{ - event: 'rearGearChange', - data: 16717829 -} -```` -#### expandComponents: true | false -When true field components as defined in the FIT Profile are expanded into new fields. expandSubFields must be set to true in order for subfields to be expanded - -````js -{ - event: 'rearGearChange' - data: 16717829, - gearChangeData:16717829, // Sub Field of data when event == 'rearGearChange - frontGear: 2, // Expanded field of gearChangeData, bits 0-7 - frontGearNum: 53, // Expanded field of gearChangeData, bits 8-15 - rearGear: 11, // Expanded field of gearChangeData, bits 16-23 - rearGearNum: 1, // Expanded field of gearChangeData, bits 24-31 -} -```` -When false field components are not expanded. -````js -{ - event: 'rearGearChange', - data: 16717829, - gearChangeData: 16717829 // Sub Field of data when event == 'rearGearChange -} -```` -#### convertTypesToStrings: true | false -When true field values are converted from raw integer values to the corresponding string values as defined in the FIT Profile. -````js -{ type:'activity'} -```` -When false the raw integer value is used. -````js -{ type: 4 } -```` -#### convertDateTimesToDates: true | false -When true FIT Epoch values are converted to JavaScript Date objects. -````js -{ timeCreated: {JavaScript Date object} } -```` -When false the FIT Epoch value is used. -````js -{ timeCreated: 995749880 } -```` -When false the Utils.convertDateTimeToDate method may be used to convert FIT Epoch values to JavaScript Date objects. -#### includeUnknownData: true | false -When true unknown field values are stored in the message using the field id as the key. -````js -{ 249: 1234} // Unknown field with an id of 249 -```` -#### mergeHeartRates: true | false -When true automatically merge heart rate values from HR messages into the Record messages. This option requires the applyScaleAndOffset and expandComponents options to be enabled. This option has no effect on the Record messages when no HR messages are present in the decoded messages. - -#### decodeMemoGlobs: true | false -When true, the decoder will reconstruct strings from memoGlob messages. Each reconstructed string will overwrite the targeted message field. - -## Creating Streams -Stream objects contain the binary FIT data to be decoded. Streams objects can be created from byte-arrays, ArrayBuffers, and Node.js Buffers. Internally the Stream class uses an ArrayBuffer to manage the byte stream. -#### From a Byte Array -````js -const streamFromByteArray = Stream.fromByteArray([0x0E, 0x10, 0xD9, 0x07, 0x00, 0x00, 0x00, 0x00, 0x2E, 0x46, 0x49, 0x54, 0x91, 0x33, 0x00, 0x00]); -console.log("isFIT: " + Decoder.isFIT(streamFromByteArray)); -```` -#### From an ArrayBuffer -````js -const uint8Array = new Uint8Array([0x0E, 0x10, 0xD9, 0x07, 0x00, 0x00, 0x00, 0x00, 0x2E, 0x46, 0x49, 0x54, 0x91, 0x33, 0x00, 0x00]); -const streamFromArrayBuffer = Stream.fromArrayBuffer(uint8Array.buffer); -console.log("isFIT: " + Decoder.isFIT(streamFromArrayBuffer)); -```` -#### From a Node.js Buffer -````js -const buffer = Buffer.from([0x0E, 0x10, 0xD9, 0x07, 0x00, 0x00, 0x00, 0x00, 0x2E, 0x46, 0x49, 0x54, 0x91, 0x33, 0x00, 0x00]); -const streamFromBuffer = Stream.fromBuffer(buffer); -console.log("isFIT: " + Decoder.isFIT(streamFromBuffer)); -```` -#### From a file using Node.js -````js -const buf = fs.readFileSync('activity.fit'); -const streamfromFileSync = Stream.fromBuffer(buf); -console.log("isFIT: " + Decoder.isFIT(streamfromFileSync)); -```` -## Utils -The Utils object contains both constants and methods for working with decoded messages and fields. -### FIT_EPOCH_MS Constant -The FIT_EPOCH_MS constant represents the number of milliseconds between the Unix Epoch and the FIT Epoch. -````js -const FIT_EPOCH_MS = 631065600000; -```` -The FIT_EPOCH_MS value can be used to convert FIT Epoch values to JavaScript Date objects. -````js -const jsDate = new Date(fitDateTime * 1000 + Utils.FIT_EPOCH_MS); -```` -### convertDateTimeToDate Method -A convince method for converting FIT Epoch values to JavaScript Date objects. -````js -const jsDate = Utils.convertDateTimeToDate(fitDateTime); -```` -## Encoder -### Usage -````js -// Import the SDK -import { Encoder, Profile} from "@garmin/fitsdk"; - -// Create an Encoder -const encoder = new Encoder(); - -// -// Write messages to the output-stream -// -// The message data should match the format returned by -// the Decoder. Field names should be camelCase. The fields -// definitions can be found in the Profile. -// - -// Pass the MesgNum and message data as separate parameters to the onMesg() method -encoder.onMesg(Profile.MesgNum.FILE_ID, { - manufacturer: "development", - product: 1, - timeCreated: new Date(), - type: "activity", -}); - -// The writeMesg() method expects the mesgNum to be included in the message data -// Internally, writeMesg() calls onMesg() -encoder.writeMesg({ - mesgNum: Profile.MesgNum.FILE_ID, - manufacturer: "development", - product: 1, - timeCreated: new Date(), - type: "activity", -}); - -// Unknown values in the message will be ignored by the Encoder -encoder.onMesg(Profile.MesgNum.FILE_ID, { - manufacturer: "development", - product: 1, - timeCreated: new Date(), - type: "activity", - customField: 12345, // This value will be ignored by the Encoder -}); - -// Subfield values in the message will be ignored by the Encoder -encoder.onMesg(Profile.MesgNum.FILE_ID, { - manufacturer: "development", - product: 4440, // This is the main product field, which is a uint16 - garminProduct: "edge1050", // This value will be ignored by the Encoder, use the main field value instead - timeCreated: new Date(), - type: "activity", -}); - -// Closing the encoder returns the file as an UInt8 Array -const uint8Array = encoder.close(); - -// Write the file to disk, -import * as fs from "fs"; -fs.writeFileSync("example.fit", uint8Array); - -```` -See the [Encode Activity Recipe](https://github.com/garmin/fit-javascript-sdk/blob/main/test/encode-activity-recipe.test.js) for a complete example of encoding a FIT Activity file usine the FIT JavaScript SDK. +# Garmin - FIT JavaScript SDK +## FIT SDK Documentation +The FIT SDK documentation is available at [https://developer.garmin.com/fit](https://developer.garmin.com/fit). +## FIT SDK Developer Forum +Share your knowledge, ask questions, and get the latest FIT SDK news in the [FIT SDK Developer Forum](https://forums.garmin.com/developer/). +## FIT JavaScript SDK Requirements +The FIT JavaScript SDK uses ECMAScript module syntax and requires Node.js v14.0 or higher, or a browser with a compatible JavaScript runtime engine. +## Install +The FIT JavaScript SDK is published as a NodeJS Pacakge on npm as [@garmin/fitsdk](https://www.npmjs.com/package/@garmin/fitsdk) and can be installed with the npm cli. + +```sh +npm install @garmin/fitsdk +``` +## Decoder +### Usage +````js +import { Decoder, Stream, Profile, Utils } from '@garmin/fitsdk'; + +const bytes = [0x0E, 0x10, 0xD9, 0x07, 0x00, 0x00, 0x00, 0x00, 0x2E, 0x46, 0x49, 0x54, 0x91, 0x33, 0x00, 0x00]; + +const stream = Stream.fromByteArray(bytes); +console.log("isFIT (static method): " + Decoder.isFIT(stream)); + +const decoder = new Decoder(stream); +console.log("isFIT (instance method): " + decoder.isFIT()); +console.log("checkIntegrity: " + decoder.checkIntegrity()); + +const { messages, errors } = decoder.read(); + +console.log(errors); +console.log(messages); +```` +### Constructor + +Decoder objects are created from Streams representing the binary FIT file data to be decoded. See [Creating Streams](#creatingstreams) for more information on constructing Stream objects. + +Once a Decoder object is created it can be used to check that the Stream is a FIT file, that the FIT file is valid, and to read the contents of the FIT file. + +### isFIT Method + +All valid FIT files should include a 12 or 14 byte file header. The 14 byte header is the preferred header size and the most common size used. Bytes 8–11 of the header contain the ASCII values ".FIT". This string can easily be spotted when opening a binary FIT file in a text or hex editor. + +````bash + Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F +00000000: 0E 10 43 08 78 06 09 00 2E 46 49 54 96 85 40 00 ..C.x....FIT..@. +00000010: 00 00 00 07 03 04 8C 04 04 86 07 04 86 01 02 84 ................ +00000020: 02 02 84 05 02 84 00 01 00 00 19 28 7E C5 95 B0 ...........(~E.0 +```` + +The isFIT method reads the file header and returns true if bytes 8–11 are equal to the ACSII values ".FIT". isFIT provides a quick way to check that the file is a FIT file before attempting to decode the file. + +The Decoder class includes a static and instance version of the isFIT method. + +### Check Integrity Method + +The checkIntegrity method performs three checks on a FIT file: + +1. Checks that bytes 8–11 of the header contain the ASCII values ".FIT". +2. Checks that the total file size is equal to Header Size + Data Size + CRC Size. +3. Reads the contents of the file, computes the CRC, and then checks that the computed CRC matches the file CRC. + +A file must pass all three of these tests to be considered a valid FIT file. See the [IsFIT(), CheckIntegrity(), and Read() Methods recipe](/fit/cookbook/isfit-checkintegrity-read/) for use-cases where the checkIntegrity method should be used and cases when it might be better to avoid it. + +### Read Method +The Read method decodes all message from the input stream and returns an object containing an array of errors encountered during the decoding and a dictionary of decoded messages grouped by message type. Any exceptions encountered during decoding will be caught by the Read method and added to the array of errors. + +The Read method accepts an optional options object that can be used to customize how field data is represented in the decoded messages. All options are enabled by default. Disabling options may speed up file decoding. Options may also be enabled or disable based on how the decoded data will be used. + +````js +const { messages, errors } = decoder.read({ + mesgListener: (messageNumber, message) => {}, + mesgDefinitionListener: (mesgDefinition) => {}, + fieldDescriptionListener: (key, developerDataIdMesg, fieldDescriptionMesg) => {}, + applyScaleAndOffset: true, + expandSubFields: true, + expandComponents: true, + convertTypesToStrings: true, + convertDateTimesToDates: true, + includeUnknownData: false, + mergeHeartRates: true + decodeMemoGlobs: false, + skipHeader: false, + dataOnly: false, +}); +```` +#### mesgListener = (messageNumber, message) => {} +Optional callback function that can be used to inspect or manipulate messages after they are fully decoded and all the options have been applied. The message is mutable and we be returned from the Read method in the messages dictionary. + +Example mesgListener callback that tracks the field names across all Record messages. +````js +const recordFields = new Set(); + +const onMesg = (messageNumber, message) => { + if (Profile.types.mesgNum[messageNumber] === "record") { + Object.keys(message).forEach(field => recordFields.add(field)); + } +} + +const { messages, errors } = decoder.read({ + mesgListener = onMesg +}); + +console.log(recordFields); +```` +#### mesgDefinitionListener: (mesgDefinition) => {} +Optional callback function that can be used to inspect message defintions as they are decoded from the file. +#### fieldDescriptionListener: (key, developerDataIdMesg, fieldDescriptionMesg) => {} +Optional callback function that can be used to inspect developer field descriptions as they are decoded from the file. +#### applyScaleAndOffset: true | false +When true the scale and offset values as defined in the FIT Profile are applied to the raw field values. +````js +{ + altitude: 1587 // with a scale of 5 and offset of 500 applied +} +```` +When false the raw field value is used. +````js +{ + altitude: 10435 // raw value store in file +} +```` +#### expandSubFields: true | false +When true subfields are created for fields as defined in the FIT Profile. +````js +{ + event: 'rearGearChange', + data: 16717829, + gearChangeData:16717829 // Sub Field of data when event == 'rearGearChange' +} +```` +When false subfields are omitted. +````js +{ + event: 'rearGearChange', + data: 16717829 +} +```` +#### expandComponents: true | false +When true field components as defined in the FIT Profile are expanded into new fields. expandSubFields must be set to true in order for subfields to be expanded + +````js +{ + event: 'rearGearChange' + data: 16717829, + gearChangeData:16717829, // Sub Field of data when event == 'rearGearChange + frontGear: 2, // Expanded field of gearChangeData, bits 0-7 + frontGearNum: 53, // Expanded field of gearChangeData, bits 8-15 + rearGear: 11, // Expanded field of gearChangeData, bits 16-23 + rearGearNum: 1, // Expanded field of gearChangeData, bits 24-31 +} +```` +When false field components are not expanded. +````js +{ + event: 'rearGearChange', + data: 16717829, + gearChangeData: 16717829 // Sub Field of data when event == 'rearGearChange +} +```` +#### convertTypesToStrings: true | false +When true field values are converted from raw integer values to the corresponding string values as defined in the FIT Profile. +````js +{ type:'activity'} +```` +When false the raw integer value is used. +````js +{ type: 4 } +```` +#### convertDateTimesToDates: true | false +When true FIT Epoch values are converted to JavaScript Date objects. +````js +{ timeCreated: {JavaScript Date object} } +```` +When false the FIT Epoch value is used. +````js +{ timeCreated: 995749880 } +```` +When false the Utils.convertDateTimeToDate method may be used to convert FIT Epoch values to JavaScript Date objects. +#### includeUnknownData: true | false +When true unknown field values are stored in the message using the field id as the key. +````js +{ 249: 1234} // Unknown field with an id of 249 +```` +#### mergeHeartRates: true | false +When true automatically merge heart rate values from HR messages into the Record messages. This option requires the applyScaleAndOffset and expandComponents options to be enabled. This option has no effect on the Record messages when no HR messages are present in the decoded messages. + +#### decodeMemoGlobs: true | false +When true, the decoder will reconstruct strings from memoGlob messages. Each reconstructed string will overwrite the targeted message field. + +#### skipHeader: true | false +When true, the decoder will read past the 14-byte header and ignore its contents. The decoder then assumes file data size from the size of the file's stream. + +#### dataOnly: true | false +When true, the decoder will read the file as if the 14-byte header was not written. The decoder then assumes the file begins with a message definition and assumes file data size from the size of the file's stream. + +## Creating Streams +Stream objects contain the binary FIT data to be decoded. Streams objects can be created from byte-arrays, ArrayBuffers, and Node.js Buffers. Internally the Stream class uses an ArrayBuffer to manage the byte stream. +#### From a Byte Array +````js +const streamFromByteArray = Stream.fromByteArray([0x0E, 0x10, 0xD9, 0x07, 0x00, 0x00, 0x00, 0x00, 0x2E, 0x46, 0x49, 0x54, 0x91, 0x33, 0x00, 0x00]); +console.log("isFIT: " + Decoder.isFIT(streamFromByteArray)); +```` +#### From an ArrayBuffer +````js +const uint8Array = new Uint8Array([0x0E, 0x10, 0xD9, 0x07, 0x00, 0x00, 0x00, 0x00, 0x2E, 0x46, 0x49, 0x54, 0x91, 0x33, 0x00, 0x00]); +const streamFromArrayBuffer = Stream.fromArrayBuffer(uint8Array.buffer); +console.log("isFIT: " + Decoder.isFIT(streamFromArrayBuffer)); +```` +#### From a Node.js Buffer +````js +const buffer = Buffer.from([0x0E, 0x10, 0xD9, 0x07, 0x00, 0x00, 0x00, 0x00, 0x2E, 0x46, 0x49, 0x54, 0x91, 0x33, 0x00, 0x00]); +const streamFromBuffer = Stream.fromBuffer(buffer); +console.log("isFIT: " + Decoder.isFIT(streamFromBuffer)); +```` +#### From a file using Node.js +````js +const buf = fs.readFileSync('activity.fit'); +const streamfromFileSync = Stream.fromBuffer(buf); +console.log("isFIT: " + Decoder.isFIT(streamfromFileSync)); +```` +## Utils +The Utils object contains both constants and methods for working with decoded messages and fields. +### FIT_EPOCH_MS Constant +The FIT_EPOCH_MS constant represents the number of milliseconds between the Unix Epoch and the FIT Epoch. +````js +const FIT_EPOCH_MS = 631065600000; +```` +The FIT_EPOCH_MS value can be used to convert FIT Epoch values to JavaScript Date objects. +````js +const jsDate = new Date(fitDateTime * 1000 + Utils.FIT_EPOCH_MS); +```` +### convertDateTimeToDate Method +A convince method for converting FIT Epoch values to JavaScript Date objects. +````js +const jsDate = Utils.convertDateTimeToDate(fitDateTime); +```` +## Encoder +### Usage +````js +// Import the SDK +import { Encoder, Profile} from "@garmin/fitsdk"; + +// Create an Encoder +const encoder = new Encoder(); + +// +// Write messages to the output-stream +// +// The message data should match the format returned by +// the Decoder. Field names should be camelCase. The fields +// definitions can be found in the Profile. +// + +// Pass the MesgNum and message data as separate parameters to the onMesg() method +encoder.onMesg(Profile.MesgNum.FILE_ID, { + manufacturer: "development", + product: 1, + timeCreated: new Date(), + type: "activity", +}); + +// The writeMesg() method expects the mesgNum to be included in the message data +// Internally, writeMesg() calls onMesg() +encoder.writeMesg({ + mesgNum: Profile.MesgNum.FILE_ID, + manufacturer: "development", + product: 1, + timeCreated: new Date(), + type: "activity", +}); + +// Unknown values in the message will be ignored by the Encoder +encoder.onMesg(Profile.MesgNum.FILE_ID, { + manufacturer: "development", + product: 1, + timeCreated: new Date(), + type: "activity", + customField: 12345, // This value will be ignored by the Encoder +}); + +// Subfield values in the message will be ignored by the Encoder +encoder.onMesg(Profile.MesgNum.FILE_ID, { + manufacturer: "development", + product: 4440, // This is the main product field, which is a uint16 + garminProduct: "edge1050", // This value will be ignored by the Encoder, use the main field value instead + timeCreated: new Date(), + type: "activity", +}); + +// Closing the encoder returns the file as an UInt8 Array +const uint8Array = encoder.close(); + +// Write the file to disk, +import * as fs from "fs"; +fs.writeFileSync("example.fit", uint8Array); + +```` +See the [Encode Activity Recipe](https://github.com/garmin/fit-javascript-sdk/blob/main/test/encode-activity-recipe.test.js) for a complete example of encoding a FIT Activity file usine the FIT JavaScript SDK. diff --git a/package.json b/package.json index fd3a704..0386707 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@garmin/fitsdk", - "version": "21.188.0", + "version": "21.194.0", "description": "FIT JavaScript SDK", "main": "src/index.js", "type": "module", diff --git a/src/accumulator.js b/src/accumulator.js index 8dbe3e1..6f3bd7c 100644 --- a/src/accumulator.js +++ b/src/accumulator.js @@ -1,12 +1,12 @@ ///////////////////////////////////////////////////////////////////////////////////////////// -// Copyright 2025 Garmin International, Inc. +// Copyright 2026 Garmin International, Inc. // Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you // may not use this file except in compliance with the Flexible and Interoperable Data // Transfer (FIT) Protocol License. ///////////////////////////////////////////////////////////////////////////////////////////// // ****WARNING**** This file is auto-generated! Do NOT edit this file. -// Profile Version = 21.188.0Release -// Tag = production/release/21.188.0-0-g55050f8 +// Profile Version = 21.194.0Release +// Tag = production/release/21.194.0-0-g65135fc ///////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/bit-stream.js b/src/bit-stream.js index b95c670..3fde10d 100644 --- a/src/bit-stream.js +++ b/src/bit-stream.js @@ -1,12 +1,12 @@ ///////////////////////////////////////////////////////////////////////////////////////////// -// Copyright 2025 Garmin International, Inc. +// Copyright 2026 Garmin International, Inc. // Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you // may not use this file except in compliance with the Flexible and Interoperable Data // Transfer (FIT) Protocol License. ///////////////////////////////////////////////////////////////////////////////////////////// // ****WARNING**** This file is auto-generated! Do NOT edit this file. -// Profile Version = 21.188.0Release -// Tag = production/release/21.188.0-0-g55050f8 +// Profile Version = 21.194.0Release +// Tag = production/release/21.194.0-0-g65135fc ///////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/crc-calculator.js b/src/crc-calculator.js index 810561d..ed604fd 100644 --- a/src/crc-calculator.js +++ b/src/crc-calculator.js @@ -1,12 +1,12 @@ ///////////////////////////////////////////////////////////////////////////////////////////// -// Copyright 2025 Garmin International, Inc. +// Copyright 2026 Garmin International, Inc. // Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you // may not use this file except in compliance with the Flexible and Interoperable Data // Transfer (FIT) Protocol License. ///////////////////////////////////////////////////////////////////////////////////////////// // ****WARNING**** This file is auto-generated! Do NOT edit this file. -// Profile Version = 21.188.0Release -// Tag = production/release/21.188.0-0-g55050f8 +// Profile Version = 21.194.0Release +// Tag = production/release/21.194.0-0-g65135fc ///////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/decoder.js b/src/decoder.js index 088f8a0..5ee0c7c 100644 --- a/src/decoder.js +++ b/src/decoder.js @@ -1,12 +1,12 @@ ///////////////////////////////////////////////////////////////////////////////////////////// -// Copyright 2025 Garmin International, Inc. +// Copyright 2026 Garmin International, Inc. // Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you // may not use this file except in compliance with the Flexible and Interoperable Data // Transfer (FIT) Protocol License. ///////////////////////////////////////////////////////////////////////////////////////////// // ****WARNING**** This file is auto-generated! Do NOT edit this file. -// Profile Version = 21.188.0Release -// Tag = production/release/21.188.0-0-g55050f8 +// Profile Version = 21.194.0Release +// Tag = production/release/21.194.0-0-g65135fc ///////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/encoder.js b/src/encoder.js index 732e291..8dcba8b 100644 --- a/src/encoder.js +++ b/src/encoder.js @@ -1,12 +1,12 @@ ///////////////////////////////////////////////////////////////////////////////////////////// -// Copyright 2025 Garmin International, Inc. +// Copyright 2026 Garmin International, Inc. // Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you // may not use this file except in compliance with the Flexible and Interoperable Data // Transfer (FIT) Protocol License. ///////////////////////////////////////////////////////////////////////////////////////////// // ****WARNING**** This file is auto-generated! Do NOT edit this file. -// Profile Version = 21.188.0Release -// Tag = production/release/21.188.0-0-g55050f8 +// Profile Version = 21.194.0Release +// Tag = production/release/21.194.0-0-g65135fc ///////////////////////////////////////////////////////////////////////////////////////////// @@ -212,12 +212,25 @@ class Encoder { throw new Error(); } + // Convert valid numeric strings to the correct type + if (FIT.isString(value)) { + value = FIT.BigIntFieldTypes.includes(fieldDefinition.type) + ? BigInt(value) + : Number(value); + } + const hasScaleOrOffset = (fieldDefinition.scale != FIT.FIELD_DEFAULT_SCALE || fieldDefinition.offset != FIT.FIELD_DEFAULT_OFFSET); + if (!hasScaleOrOffset && !this.#isValidType(value, fieldDefinition.type)) { + throw new Error(); + } + if (hasScaleOrOffset) { - const scaledValue = (value + fieldDefinition.offset) * fieldDefinition.scale; + const scaledValue = this.#unapplyScaleAndOffset(value, fieldDefinition.scale, fieldDefinition.offset); + + const roundedValue = FIT.FloatingPointFieldTypes.includes(fieldDefinition.type) || FIT.isBigInt(scaledValue) ? scaledValue : Math.round(scaledValue); - return FIT.FloatingPointFieldTypes.includes(fieldDefinition.type) ? scaledValue : Math.round(scaledValue); + return FIT.BigIntFieldTypes.includes(fieldDefinition.type) ? BigInt(roundedValue) : Number(roundedValue); } return value; @@ -265,6 +278,18 @@ class Encoder { } } + #isValidType = (value, type) => { + const jsType = FIT.FieldTypeToJsType[type]; + + return typeof value === jsType + } + + #unapplyScaleAndOffset(value, scale, offset) { + return FIT.isBigInt(value) + ? (value + BigInt(offset)) * BigInt(scale) + : (value + offset) * scale; + } + /** * Creates a MesgDefinition from the mesgNum and mesg. * @param {Number} mesgNum - The mesg number for this message diff --git a/src/fit.js b/src/fit.js index fa63ba9..288746a 100644 --- a/src/fit.js +++ b/src/fit.js @@ -1,12 +1,12 @@ ///////////////////////////////////////////////////////////////////////////////////////////// -// Copyright 2025 Garmin International, Inc. +// Copyright 2026 Garmin International, Inc. // Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you // may not use this file except in compliance with the Flexible and Interoperable Data // Transfer (FIT) Protocol License. ///////////////////////////////////////////////////////////////////////////////////////////// // ****WARNING**** This file is auto-generated! Do NOT edit this file. -// Profile Version = 21.188.0Release -// Tag = production/release/21.188.0-0-g55050f8 +// Profile Version = 21.194.0Release +// Tag = production/release/21.194.0-0-g65135fc ///////////////////////////////////////////////////////////////////////////////////////////// @@ -73,6 +73,32 @@ const NumericFieldTypes = [ "uint64z" ]; +const FieldTypeToJsType = { + "enum": "number", + "sint8": "number", + "uint8": "number", + "sint16": "number", + "uint16": "number", + "sint32": "number", + "uint32": "number", + "string": "string", + "float32": "number", + "float64": "number", + "uint8z": "number", + "uint16z": "number", + "uint32z": "number", + "byte": "number", + "sint64": "bigint", + "uint64": "bigint", + "uint64z": "bigint" +} + +const BigIntFieldTypes = [ + "sint64", + "uint64", + "uint64z" +]; + const FloatingPointFieldTypes = [ "float32", "float64", @@ -138,8 +164,21 @@ const isString = (obj) => { return typeof obj === "string"; }; +const isBigInt = (obj) => { + return typeof obj === "bigint"; +}; + const isNumeric = (obj) => { - return !isNaN(parseFloat(obj)) && isFinite(obj); + if (typeof obj === "number") { + return !isNaN(obj) && isFinite(obj); + } + + if (typeof obj === "bigint") { + return true; + } + + const num = parseFloat(obj); + return !isNaN(num) && isFinite(num); }; const isNotNumberStringDateOrBoolean = (obj) => { @@ -151,7 +190,7 @@ const isNumberStringDateOrBoolean = (obj) => { return false; } - if (!isDate(obj) && !isString(obj) && !isNumeric(obj) && !isBoolean(obj)) { + if (!isNumeric(obj) && !isDate(obj) && !isString(obj) && !isBoolean(obj)) { return false; } @@ -163,14 +202,17 @@ export default { BaseTypeMask, BaseTypeDefinitions, NumericFieldTypes, + BigIntFieldTypes, FloatingPointFieldTypes, FieldTypeToBaseType, BaseTypeToFieldType, + FieldTypeToJsType, isNullOrUndefined, isObject, isBoolean, isDate, isString, + isBigInt, isNumeric, isNumberStringDateOrBoolean, isNotNumberStringDateOrBoolean, diff --git a/src/index.js b/src/index.js index c9d4c85..4c395b7 100644 --- a/src/index.js +++ b/src/index.js @@ -1,12 +1,12 @@ ///////////////////////////////////////////////////////////////////////////////////////////// -// Copyright 2025 Garmin International, Inc. +// Copyright 2026 Garmin International, Inc. // Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you // may not use this file except in compliance with the Flexible and Interoperable Data // Transfer (FIT) Protocol License. ///////////////////////////////////////////////////////////////////////////////////////////// // ****WARNING**** This file is auto-generated! Do NOT edit this file. -// Profile Version = 21.188.0Release -// Tag = production/release/21.188.0-0-g55050f8 +// Profile Version = 21.194.0Release +// Tag = production/release/21.194.0-0-g65135fc ///////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/mesg-definition.js b/src/mesg-definition.js index ce751da..ff8b390 100644 --- a/src/mesg-definition.js +++ b/src/mesg-definition.js @@ -1,12 +1,12 @@ ///////////////////////////////////////////////////////////////////////////////////////////// -// Copyright 2025 Garmin International, Inc. +// Copyright 2026 Garmin International, Inc. // Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you // may not use this file except in compliance with the Flexible and Interoperable Data // Transfer (FIT) Protocol License. ///////////////////////////////////////////////////////////////////////////////////////////// // ****WARNING**** This file is auto-generated! Do NOT edit this file. -// Profile Version = 21.188.0Release -// Tag = production/release/21.188.0-0-g55050f8 +// Profile Version = 21.194.0Release +// Tag = production/release/21.194.0-0-g65135fc ///////////////////////////////////////////////////////////////////////////////////////////// @@ -86,6 +86,10 @@ class MesgDefinition { fieldDefinitionNumber: fieldDescriptionMesg.fieldDefinitionNumber, size: this.#fieldSize(mesg.developerFields[key], baseTypeDef), developerDataIndex: developerDataIdMesg.developerDataIndex, + type: FIT.BaseTypeToFieldType[baseTypeDef.type], + scale: 1, + offset: 0, + components: [], }); }); diff --git a/src/output-stream.js b/src/output-stream.js index cb6a934..bf288c7 100644 --- a/src/output-stream.js +++ b/src/output-stream.js @@ -1,20 +1,21 @@ ///////////////////////////////////////////////////////////////////////////////////////////// -// Copyright 2025 Garmin International, Inc. +// Copyright 2026 Garmin International, Inc. // Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you // may not use this file except in compliance with the Flexible and Interoperable Data // Transfer (FIT) Protocol License. ///////////////////////////////////////////////////////////////////////////////////////////// // ****WARNING**** This file is auto-generated! Do NOT edit this file. -// Profile Version = 21.188.0Release -// Tag = production/release/21.188.0-0-g55050f8 +// Profile Version = 21.194.0Release +// Tag = production/release/21.194.0-0-g65135fc ///////////////////////////////////////////////////////////////////////////////////////////// import FIT from "./fit.js"; const ONE_MEGABYTE = 1048576; -const TEN_MEGABYTES = ONE_MEGABYTE * 10; +const FIVE_HUNDRED_MEGABYTES = ONE_MEGABYTE * 500; const HALF_MEGABYTE = ONE_MEGABYTE / 2; +const MAX_BYTE_LENGTH = FIVE_HUNDRED_MEGABYTES; class OutputStream { #arrayBuffer = null; @@ -29,12 +30,12 @@ class OutputStream { * @constructor * @param {Object=} [options] - Read options (optional) * @param {Number} [options.initialByteLength=0.5MB] - (optional, default 0.5 MB) - * @param {Number} [options.maxByteLength=2MB] - (optional, default 2 MB) + * @param {Number} [options.maxByteLength=500MB] - (optional, default 500 MB) * @param {Number} [options.resizeByBytes=0.5MB] - (optional, default 0.5 MB) */ constructor({ initialByteLength = HALF_MEGABYTE, - maxByteLength = TEN_MEGABYTES, + maxByteLength = MAX_BYTE_LENGTH, resizeByBytes = HALF_MEGABYTE, } = {}) { this.#arrayBuffer = new ArrayBuffer(initialByteLength, { maxByteLength, }); @@ -209,11 +210,13 @@ class OutputStream { return; } - if (!this.#arrayBuffer.resizable) { + const newByteLength = this.#arrayBuffer.byteLength + Math.max(this.#resizeByBytes, byteCount); + + if (newByteLength > this.#arrayBuffer.maxByteLength) { throw new Error("Can not resize OutputStream. Set a larger initial size."); } - this.#arrayBuffer.resize(this.#arrayBuffer.byteLength + Math.max(this.#resizeByBytes, byteCount)); + this.#arrayBuffer.resize(newByteLength); } } diff --git a/src/profile.js b/src/profile.js index 038ade5..4fc4d6f 100644 --- a/src/profile.js +++ b/src/profile.js @@ -1,19 +1,19 @@ ///////////////////////////////////////////////////////////////////////////////////////////// -// Copyright 2025 Garmin International, Inc. +// Copyright 2026 Garmin International, Inc. // Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you // may not use this file except in compliance with the Flexible and Interoperable Data // Transfer (FIT) Protocol License. ///////////////////////////////////////////////////////////////////////////////////////////// // ****WARNING**** This file is auto-generated! Do NOT edit this file. -// Profile Version = 21.188.0Release -// Tag = production/release/21.188.0-0-g55050f8 +// Profile Version = 21.194.0Release +// Tag = production/release/21.194.0-0-g65135fc ///////////////////////////////////////////////////////////////////////////////////////////// const Profile = { version: { major: 21, - minor: 188, + minor: 194, patch: 0, type: "Release" }, @@ -7042,6 +7042,21 @@ const Profile = { hasComponents: false, subFields: [] }, + 196: { + num: 196, + name: "metabolicCalories", + type: "uint16", + baseType: "uint16", + array: false, + scale: 1, + offset: 0, + units: "kcal", + bits: [], + components: [], + isAccumulated: false, + hasComponents: false, + subFields: [] + }, 197: { num: 197, // Standard deviation of R-R interval (SDRR) - Heart rate variability measure most useful for wellness users. name: "sdrrHrv", @@ -24363,6 +24378,10 @@ types: { 334: "daradInnovationCorporation", 335: "cycloptim", 337: "runna", + 339: "zepp", + 340: "peloton", + 341: "carv", + 342: "tissot", 5759: "actigraphcorp", }, garminProduct: { @@ -24825,6 +24844,7 @@ types: { 4759: "instinct3Solar50mm", 4775: "tactix8Amoled", 4776: "tactix8Solar", + 4825: "approachJ1", 4879: "d2Mach2", 4678: "instinctCrossoverAmoled", 4944: "d2AirX15", diff --git a/src/stream.js b/src/stream.js index 5f7ba6f..f591c7b 100644 --- a/src/stream.js +++ b/src/stream.js @@ -1,12 +1,12 @@ ///////////////////////////////////////////////////////////////////////////////////////////// -// Copyright 2025 Garmin International, Inc. +// Copyright 2026 Garmin International, Inc. // Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you // may not use this file except in compliance with the Flexible and Interoperable Data // Transfer (FIT) Protocol License. ///////////////////////////////////////////////////////////////////////////////////////////// // ****WARNING**** This file is auto-generated! Do NOT edit this file. -// Profile Version = 21.188.0Release -// Tag = production/release/21.188.0-0-g55050f8 +// Profile Version = 21.194.0Release +// Tag = production/release/21.194.0-0-g65135fc ///////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/utils-hr-mesg.js b/src/utils-hr-mesg.js index d5effbb..cab0b5d 100644 --- a/src/utils-hr-mesg.js +++ b/src/utils-hr-mesg.js @@ -1,12 +1,12 @@ ///////////////////////////////////////////////////////////////////////////////////////////// -// Copyright 2025 Garmin International, Inc. +// Copyright 2026 Garmin International, Inc. // Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you // may not use this file except in compliance with the Flexible and Interoperable Data // Transfer (FIT) Protocol License. ///////////////////////////////////////////////////////////////////////////////////////////// // ****WARNING**** This file is auto-generated! Do NOT edit this file. -// Profile Version = 21.188.0Release -// Tag = production/release/21.188.0-0-g55050f8 +// Profile Version = 21.194.0Release +// Tag = production/release/21.194.0-0-g65135fc ///////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/utils-internal.js b/src/utils-internal.js index 81ce703..97407fd 100644 --- a/src/utils-internal.js +++ b/src/utils-internal.js @@ -1,12 +1,12 @@ ///////////////////////////////////////////////////////////////////////////////////////////// -// Copyright 2025 Garmin International, Inc. +// Copyright 2026 Garmin International, Inc. // Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you // may not use this file except in compliance with the Flexible and Interoperable Data // Transfer (FIT) Protocol License. ///////////////////////////////////////////////////////////////////////////////////////////// // ****WARNING**** This file is auto-generated! Do NOT edit this file. -// Profile Version = 21.188.0Release -// Tag = production/release/21.188.0-0-g55050f8 +// Profile Version = 21.194.0Release +// Tag = production/release/21.194.0-0-g65135fc ///////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/utils-memo-glob.js b/src/utils-memo-glob.js index 27516e7..5ba3b35 100644 --- a/src/utils-memo-glob.js +++ b/src/utils-memo-glob.js @@ -1,12 +1,12 @@ ///////////////////////////////////////////////////////////////////////////////////////////// -// Copyright 2025 Garmin International, Inc. +// Copyright 2026 Garmin International, Inc. // Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you // may not use this file except in compliance with the Flexible and Interoperable Data // Transfer (FIT) Protocol License. ///////////////////////////////////////////////////////////////////////////////////////////// // ****WARNING**** This file is auto-generated! Do NOT edit this file. -// Profile Version = 21.188.0Release -// Tag = production/release/21.188.0-0-g55050f8 +// Profile Version = 21.194.0Release +// Tag = production/release/21.194.0-0-g65135fc ///////////////////////////////////////////////////////////////////////////////////////////// import Profile from "./profile.js"; diff --git a/src/utils.js b/src/utils.js index 27df85e..e52fa10 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,12 +1,12 @@ ///////////////////////////////////////////////////////////////////////////////////////////// -// Copyright 2025 Garmin International, Inc. +// Copyright 2026 Garmin International, Inc. // Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you // may not use this file except in compliance with the Flexible and Interoperable Data // Transfer (FIT) Protocol License. ///////////////////////////////////////////////////////////////////////////////////////////// // ****WARNING**** This file is auto-generated! Do NOT edit this file. -// Profile Version = 21.188.0Release -// Tag = production/release/21.188.0-0-g55050f8 +// Profile Version = 21.194.0Release +// Tag = production/release/21.194.0-0-g65135fc ///////////////////////////////////////////////////////////////////////////////////////////// diff --git a/test/accumulator.test.js b/test/accumulator.test.js index bf47636..a76c15f 100644 --- a/test/accumulator.test.js +++ b/test/accumulator.test.js @@ -1,5 +1,5 @@ ///////////////////////////////////////////////////////////////////////////////////////////// -// Copyright 2025 Garmin International, Inc. +// Copyright 2026 Garmin International, Inc. // Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you // may not use this file except in compliance with the Flexible and Interoperable Data // Transfer (FIT) Protocol License. diff --git a/test/bit-stream.test.js b/test/bit-stream.test.js index bd53260..8f62873 100644 --- a/test/bit-stream.test.js +++ b/test/bit-stream.test.js @@ -1,5 +1,5 @@ ///////////////////////////////////////////////////////////////////////////////////////////// -// Copyright 2025 Garmin International, Inc. +// Copyright 2026 Garmin International, Inc. // Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you // may not use this file except in compliance with the Flexible and Interoperable Data // Transfer (FIT) Protocol License. diff --git a/test/crc.test.js b/test/crc.test.js index 437f618..4e445d5 100644 --- a/test/crc.test.js +++ b/test/crc.test.js @@ -1,5 +1,5 @@ ///////////////////////////////////////////////////////////////////////////////////////////// -// Copyright 2025 Garmin International, Inc. +// Copyright 2026 Garmin International, Inc. // Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you // may not use this file except in compliance with the Flexible and Interoperable Data // Transfer (FIT) Protocol License. diff --git a/test/data/test-data-expand-hr-mesgs.js b/test/data/test-data-expand-hr-mesgs.js index ca2f27f..44092ca 100644 --- a/test/data/test-data-expand-hr-mesgs.js +++ b/test/data/test-data-expand-hr-mesgs.js @@ -1,5 +1,5 @@ ///////////////////////////////////////////////////////////////////////////////////////////// -// Copyright 2025 Garmin International, Inc. +// Copyright 2026 Garmin International, Inc. // Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you // may not use this file except in compliance with the Flexible and Interoperable Data // Transfer (FIT) Protocol License. diff --git a/test/data/test-data.js b/test/data/test-data.js index 6076925..1d4f13c 100644 --- a/test/data/test-data.js +++ b/test/data/test-data.js @@ -1,5 +1,5 @@ ///////////////////////////////////////////////////////////////////////////////////////////// -// Copyright 2025 Garmin International, Inc. +// Copyright 2026 Garmin International, Inc. // Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you // may not use this file except in compliance with the Flexible and Interoperable Data // Transfer (FIT) Protocol License. diff --git a/test/decoder.test.js b/test/decoder.test.js index 2fdddea..c7e297d 100644 --- a/test/decoder.test.js +++ b/test/decoder.test.js @@ -1,5 +1,5 @@ ///////////////////////////////////////////////////////////////////////////////////////////// -// Copyright 2025 Garmin International, Inc. +// Copyright 2026 Garmin International, Inc. // Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you // may not use this file except in compliance with the Flexible and Interoperable Data // Transfer (FIT) Protocol License. diff --git a/test/encode-activity-recipe.test.js b/test/encode-activity-recipe.test.js index 6929386..f4fbdf9 100644 --- a/test/encode-activity-recipe.test.js +++ b/test/encode-activity-recipe.test.js @@ -1,5 +1,5 @@ ///////////////////////////////////////////////////////////////////////////////////////////// -// Copyright 2025 Garmin International, Inc. +// Copyright 2026 Garmin International, Inc. // Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you // may not use this file except in compliance with the Flexible and Interoperable Data // Transfer (FIT) Protocol License. diff --git a/test/encoder.test.js b/test/encoder.test.js index 7789696..ba0600c 100644 --- a/test/encoder.test.js +++ b/test/encoder.test.js @@ -1,13 +1,15 @@ ///////////////////////////////////////////////////////////////////////////////////////////// -// Copyright 2025 Garmin International, Inc. +// Copyright 2026 Garmin International, Inc. // Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you // may not use this file except in compliance with the Flexible and Interoperable Data // Transfer (FIT) Protocol License. ///////////////////////////////////////////////////////////////////////////////////////////// + import { describe, expect, test } from "vitest"; -import { Profile, Encoder, Stream, Decoder } from "../src/index.js"; +import { Profile, Encoder, Utils } from "../src/index.js"; +import { DEFAULT_CUSTOM_MESG_NUM, addCustomMesgToFitProfile, encodeThenDecodeMesgs, encodeMesgs, } from "./testUtils.js"; describe("Encoder Tests", () => { test("A file encoded with no messages should be 16 bytes long.", () => { @@ -46,9 +48,7 @@ describe("Encoder Tests", () => { }; try { - const encoder = new Encoder(); - encoder.onMesg(Profile.MesgNum.FILE_ID, fileIdMesg); - const uint8Array = encoder.close(); + const uint8Array = encodeMesgs([{ mesgNum: Profile.MesgNum.FILE_ID, mesg: fileIdMesg }]); expect(uint8Array.length).toBe(65); } @@ -88,9 +88,7 @@ describe("Encoder Tests", () => { }; try { - const encoder = new Encoder(); - encoder.onMesg(Profile.MesgNum.FILE_ID, fileIdMesg); - const uint8Array = encoder.close(); + const uint8Array = encodeMesgs([{ mesgNum: Profile.MesgNum.FILE_ID, mesg: fileIdMesg }]); expect(uint8Array.length).toBe(48); } @@ -177,9 +175,7 @@ describe("Encoder Tests", () => { product: 0x1234, } - const encoder = new Encoder(); - encoder.onMesg(Profile.MesgNum.FILE_ID, fileIdMesg); - const uint8Array = encoder.close(); + const uint8Array = encodeMesgs([{ mesgNum: Profile.MesgNum.FILE_ID, mesg: fileIdMesg }]); // Base type UINT16 with endianness is 0x84 expect(uint8Array[22]).toBe(0x84); @@ -210,16 +206,38 @@ describe("Encoder Tests", () => { }, }; - const encoder = new Encoder({ fieldDescriptions }); + const mesgs = [ + { mesgNum: Profile.MesgNum.DEVELOPER_DATA_ID, mesg: developerDataIdMesg }, + { mesgNum: Profile.MesgNum.FIELD_DESCRIPTION, mesg: fieldDescriptionMesg }, + { mesgNum: Profile.MesgNum.SESSION, mesg: sessionMesg }, + ]; - encoder.onMesg(Profile.MesgNum.DEVELOPER_DATA_ID, developerDataIdMesg); - encoder.onMesg(Profile.MesgNum.FIELD_DESCRIPTION, fieldDescriptionMesg); - encoder.onMesg(Profile.MesgNum.SESSION, sessionMesg); - const uint8Array = encoder.close(); + const uint8Array = encodeMesgs(mesgs, { fieldDescriptions }); // Dev data FIT Base type UINT16 with endianness is 0x84 expect(uint8Array[43]).toBe(0x84); }); + + test.for([ + ["uint8", 123n], + ["uint8", "hello"], + ["uint64", 123], + ["uint64", "123n"], + ["uint64", "hello"], + ["uint64", 12.34], + ])("Encoder throws when encoding an unexpected JavaScript type", ([fitBaseType, value]) => { + addCustomMesgToFitProfile(DEFAULT_CUSTOM_MESG_NUM, "customMesg", { + 0: { name: "customField", type: fitBaseType, baseType: fitBaseType, }, + }) + + const mesg = { + customField: value, + } + + expect(() => { + encodeMesgs([{ mesgNum: DEFAULT_CUSTOM_MESG_NUM, mesg }]); + }).toThrowError(); + }); }); describe("Encoder-Decoder Integration Tests", () => { @@ -237,13 +255,7 @@ describe("Encoder-Decoder Integration Tests", () => { }; try { - const encoder = new Encoder(); - encoder.onMesg(Profile.MesgNum.FILE_ID, fileIdMesg); - const stream = Stream.fromByteArray(encoder.close()); - - const decoder = new Decoder(stream); - - const { messages, errors, } = decoder.read(DECODER_OPTIONS); + const { messages, errors, } = encodeThenDecodeMesgs([{ mesgNum: Profile.MesgNum.FILE_ID, mesg: fileIdMesg }], { decoderOptions: DECODER_OPTIONS, }); expect(errors.length).toBe(0); @@ -275,13 +287,7 @@ describe("Encoder-Decoder Integration Tests", () => { ]; try { - const encoder = new Encoder(); - encoder.onMesg(Profile.MesgNum.HR, hrMesg); - const stream = Stream.fromByteArray(encoder.close()); - - const decoder = new Decoder(stream); - - const { messages, errors, } = decoder.read(DECODER_OPTIONS); + const { messages, errors, } = encodeThenDecodeMesgs([{ mesgNum: Profile.MesgNum.HR, mesg: hrMesg }], { decoderOptions: DECODER_OPTIONS, }); expect(errors.length).toBe(0); @@ -306,14 +312,7 @@ describe("Encoder-Decoder Integration Tests", () => { distance: 10.789, } - const encoder = new Encoder(); - encoder.onMesg(Profile.MesgNum.RECORD, recordMesg); - - const stream = Stream.fromByteArray(encoder.close()); - - const decoder = new Decoder(stream); - - const { messages, errors, } = decoder.read(DECODER_OPTIONS); + const { messages, errors, } = encodeThenDecodeMesgs([{ mesgNum: Profile.MesgNum.RECORD, mesg: recordMesg }]); expect(errors.length).toBe(0); expect(messages.recordMesgs.length).toBe(1); @@ -338,17 +337,16 @@ describe("Encoder-Decoder Integration Tests", () => { } try { - const encoder = new Encoder(); - encoder.onMesg(Profile.MesgNum.RECORD, recordMesg); - const uint8Array = encoder.close(); - - const decoder = new Decoder(Stream.fromByteArray(uint8Array)); - const { messages, errors, } = decoder.read(); + const { messages, errors, } = encodeThenDecodeMesgs([{ mesgNum: Profile.MesgNum.RECORD, mesg: recordMesg }]); expect(errors.length).toBe(0); expect(messages.recordMesgs.length).toBe(1); - expect(messages.recordMesgs[0]).toMatchObject(recordMesg); + expect(messages.recordMesgs[0]).toMatchObject({ + heartRate: recordMesg.heartRate, + altitude: recordMesg.altitude, + speed: recordMesg.speed, + }); } catch (error) { console.error(`${error.name}: ${error.message} \n${JSON.stringify(error.cause, null, 3)}`); @@ -381,16 +379,13 @@ describe("Encoder-Decoder Integration Tests", () => { }, }; - const encoder = new Encoder({ fieldDescriptions }); - - encoder.onMesg(Profile.MesgNum.DEVELOPER_DATA_ID, developerDataIdMesg); - encoder.onMesg(Profile.MesgNum.FIELD_DESCRIPTION, fieldDescriptionMesg); - encoder.onMesg(Profile.MesgNum.FILE_ID, fileIdMesg); - - const stream = Stream.fromByteArray(encoder.close()); - const decoder = new Decoder(stream); + const mesgs = [ + { mesgNum: Profile.MesgNum.DEVELOPER_DATA_ID, mesg: developerDataIdMesg, }, + { mesgNum: Profile.MesgNum.FIELD_DESCRIPTION, mesg: fieldDescriptionMesg, }, + { mesgNum: Profile.MesgNum.FILE_ID, mesg: fileIdMesg, }, + ]; - const { messages, errors, } = decoder.read(DECODER_OPTIONS); + const { messages, errors, } = encodeThenDecodeMesgs(mesgs, { fieldDescriptions, decoderOptions: DECODER_OPTIONS, }); expect(errors.length).toBe(0); expect(messages.fileIdMesgs.length).toBe(1); @@ -399,5 +394,173 @@ describe("Encoder-Decoder Integration Tests", () => { expect(decodedFileIdMesg.product).toBe(fileIdMesg.product); expect(decodedFileIdMesg.developerFields[0]).toBe(fileIdMesg.developerFields[0]); }); + + describe("Base Type Encode-Decode Tests", () => { + test.for([ + ["uint8", 123, 123], + ["uint16", 12345, 12345], + ["uint32", 1234567890, 1234567890], + ["sint8", -123, -123], + ["sint16", -12345, -12345], + ["sint32", -123456789, -123456789], + ["string", "Test String", "Test String"], + ["float32", 123.4, 123.4], + ["float64", 123456.789012, 123456.789012], + ["uint8z", 200, 200], + ["uint16z", 60000, 60000], + ["uint32z", 4000000000, 4000000000], + ["byte", 0xDE, 0xDE], + ["sint64", -12345678901234n, -12345678901234n], + ["uint64", 12345678901234n, 12345678901234n], + ["uint64z", 12345678901234n, 12345678901234n], + // Offset Tests + ["uint8", 123, 123, { offset: 2 }], + ["uint16", 12345, 12345, { offset: 2 }], + ["uint32", 1234567890, 1234567890, { offset: 2 }], + ["sint8", -123, -123, { offset: 2 }], + ["sint16", -12345, -12345, { offset: 2 }], + ["sint32", -123456789, -123456789, { offset: 2 }], + ["string", "Test String", "Test String", { offset: 2 }], + ["float32", 123.4, 123.4, { offset: 2 }], + ["float64", 123456.789012, 123456.789012, { offset: 2 }], + ["uint8z", 200, 200, { offset: 2 }], + ["uint16z", 60000, 60000, { offset: 2 }], + ["uint32z", 4000000000, 4000000000, { offset: 2 }], + ["byte", 0xDE, 0xDE, { offset: 2 }], + // Scale Tests + ["uint8", 123, 123, { scale: 2 }], + ["uint16", 12345, 12345, { scale: 2 }], + ["uint32", 1234567890, 1234567890, { scale: 2 }], + ["sint8", -12, -12, { scale: 2 }], + ["sint16", -1234, -1234, { scale: 2 }], + ["sint32", -12345, -12345, { scale: 2 }], + ["string", "Test String", "Test String", { scale: 2 }], + ["float32", 123.4, 123.4, { scale: 2 }], + ["float64", 123456.789012, 123456.789012, { scale: 2 }], + ["uint8z", 123, 123, { scale: 2 }], + ["uint16z", 1234, 1234, { scale: 2 }], + ["uint32z", 12345, 12345, { scale: 2 }], + ["byte", 0x01, 0x01, { scale: 2 }], + // 64 bit Scale/Offset Tests (Decoder Scale/Offset not applied) + ["sint64", -100n, -98n, { offset: 2 }], + ["uint64", 100n, 102n, { offset: 2 }], + ["uint64z", 100n, 102n, { offset: 2 }], + ["sint64", -500n, -1000n, { scale: 2 }], + ["uint64", 100n, 200n, { scale: 2 }], + ["uint64z", 100n, 200n, { scale: 2 }], + ["uint64", 123.45, 12345n, { scale: 100 }], + // Integer Fields Scale Rounding Tests + ["uint8", 12.21, 12.2, { scale: 10 }], + ["uint8", 12.77, 12.8, { scale: 10 }], + ["uint8", 12.5, 12.5, { scale: 10 }], + // String Numeric Value Tests + ["uint8", "123", 123], + ["float32", "123.456", 123.456], + ["float64", "123456.789012", 123456.789012], + ["uint64", "12345678901234", 12345678901234n], + ["sint64", "-12345678901234", -12345678901234n], + ["uint64", "1234567890123456789012345678901234567890", 12446928571455179474n], + // Overflow Mask Tests + ["uint8", 0x1234, 0x34], + ["sint8", 0x12FF, -1], + ["uint8z", 0x1234, 0x34], + ["uint16", 0x123456, 0x3456], + ["sint16", 0x12FFFF, -1], + ["uint16z", 0x123456, 0x3456], + ["uint32", 0x1234567899, 0x34567899], + ["sint32", 0x12FFFFFFFF, -1], + ["uint32z", 0x1234567899, 0x34567899], + ["byte", 0x1234, 0x34], + ["uint64", 0x12FFFFFFFFFFFFFFFFFFFFFFFFFn, 0xFFFFFFFFFFFFFFFFn], + ["uint64z", 0x12FFFFFFFFFFFFFFFFFFFFFFFFFFn, 0xFFFFFFFFFFFFFFFFn], + ["sint64", 0x12FFFFFFFFFFFFFFFFFFFFFFFFFFFFn, -1n], + ])("Encoding field of base type: %s %#", ([fitBaseType, value, expectedValue, { scale = 1, offset = 0 } = {}]) => { + + addCustomMesgToFitProfile(DEFAULT_CUSTOM_MESG_NUM, "testMesg", { + 0: { name: "testField", type: fitBaseType, baseType: fitBaseType, scale, offset, }, + }) + + const testMesg = { + testField: value, + } + + const { messages, errors, } = encodeThenDecodeMesgs([{ mesgNum: DEFAULT_CUSTOM_MESG_NUM, mesg: testMesg }]); + + expect(errors.length).toBe(0); + const mesg = messages.testMesgMesgs[0]; + + (fitBaseType === "string" || typeof mesg.testField === "bigint") + ? expect(mesg.testField).toBe(expectedValue) + : expect(mesg.testField).toBeCloseTo(expectedValue, 2); + }); + + test.for([ + ["uint8", 123], + ["uint16", 12345], + ["uint32", 1234567890], + ["sint8", -123], + ["sint16", -12345], + ["sint32", -123456789], + ["string", "Test String"], + ["float32", 123.456], + ["float64", 123456.789012], + ["uint8z", 200], + ["uint16z", 60000], + ["uint32z", 4000000000], + ["byte", 0xDE], + ["sint64", -12345678901234n], + ["uint64", 12345678901234n], + ["uint64z", 12345678901234n], + ])("Encoding developer field of base type: %s", ([fitBaseType, expectedValue]) => { + const DEV_FIELD_KEY = 0; + + const developerDataIdMesg = { + applicationId: Array(16).fill(0), + applicationVersion: 1, + developerDataIndex: 0, + }; + + const fieldDescriptionMesg = { + developerDataIndex: 0, + fieldDefinitionNumber: 0, + fitBaseTypeId: Utils.FieldTypeToBaseType[fitBaseType], + fieldName: "Test Field", + units: "units", + nativeMesgNum: Profile.MesgNum.SESSION, + }; + + const fieldDescriptions = { + [DEV_FIELD_KEY]: { + developerDataIdMesg, + fieldDescriptionMesg, + }, + }; + + const sessionMesg = { + messageIndex: 0, + sport: "running", + developerFields: { + [DEV_FIELD_KEY]: expectedValue, + }, + } + + const mesgs = [ + { mesgNum: Profile.MesgNum.DEVELOPER_DATA_ID, mesg: developerDataIdMesg, }, + { mesgNum: Profile.MesgNum.FIELD_DESCRIPTION, mesg: fieldDescriptionMesg, }, + { mesgNum: Profile.MesgNum.SESSION, mesg: sessionMesg, }, + ]; + + const { messages, errors, } = encodeThenDecodeMesgs(mesgs, { fieldDescriptions, }); + + expect(errors.length).toBe(0); + expect(messages.sessionMesgs.length).toBe(1); + + const actualValue = messages.sessionMesgs[0].developerFields[DEV_FIELD_KEY]; + + (fitBaseType === "string" || typeof actualValue === "bigint") + ? expect(actualValue).toBe(expectedValue) + : expect(actualValue).toBeCloseTo(expectedValue, 2); + }); + }); }); diff --git a/test/output-stream.test.js b/test/output-stream.test.js index 942c20e..bdabd43 100644 --- a/test/output-stream.test.js +++ b/test/output-stream.test.js @@ -1,5 +1,5 @@ ///////////////////////////////////////////////////////////////////////////////////////////// -// Copyright 2025 Garmin International, Inc. +// Copyright 2026 Garmin International, Inc. // Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you // may not use this file except in compliance with the Flexible and Interoperable Data // Transfer (FIT) Protocol License. @@ -316,3 +316,30 @@ describe("Can write strings to an OutputStream", () => { expect(outputStream.length).toBe(10); }); }); + +describe("OutputStream handles array resizing", () => { + test("OutputStream throws when exceeding max byte length", () => { + const outputStream = new OutputStream({ + initialByteLength: 4, + maxByteLength: 4, + resizeByBytes: 1, + }); + + expect(() => { + outputStream.writeUInt8([0, 1, 2, 3, 4,]); + }).toThrow(/Can not resize/); + }); + + test("OutputStream does not throw when meeting max byte length", () => { + const outputStream = new OutputStream({ + initialByteLength: 4, + maxByteLength: 4, + resizeByBytes: 1, + }); + + outputStream.writeUInt8([0, 1, 2, 3]); + + expect(outputStream.length).toBe(4); + expect(Array.from(outputStream.uint8Array)).toEqual([0, 1, 2, 3,]); + }); +}); diff --git a/test/stream.test.js b/test/stream.test.js index d48b646..2660ab2 100644 --- a/test/stream.test.js +++ b/test/stream.test.js @@ -1,5 +1,5 @@ ///////////////////////////////////////////////////////////////////////////////////////////// -// Copyright 2025 Garmin International, Inc. +// Copyright 2026 Garmin International, Inc. // Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you // may not use this file except in compliance with the Flexible and Interoperable Data // Transfer (FIT) Protocol License. diff --git a/test/testUtils.js b/test/testUtils.js new file mode 100644 index 0000000..6096662 --- /dev/null +++ b/test/testUtils.js @@ -0,0 +1,124 @@ +///////////////////////////////////////////////////////////////////////////////////////////// +// Copyright 2026 Garmin International, Inc. +// Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you +// may not use this file except in compliance with the Flexible and Interoperable Data +// Transfer (FIT) Protocol License. +///////////////////////////////////////////////////////////////////////////////////////////// + + +import { Encoder, Stream, Decoder, Profile } from "../src/index.js"; + +export const DEFAULT_CUSTOM_MESG_NUM = 0xFF00; + +/** + * Encodes an array of FIT messages into a byte array. + * @param {Array} mesgs - Array of message objects containing a mesg and its mesgNum. + * @param {Object} options - Encoder options. + * @param {Object} [options.fieldDescriptions] - Field descriptions for encoding developer fields. + * @returns {Uint8Array} The encoded FIT file as a byte array. + */ +export const encodeMesgs = (mesgs = [], { fieldDescriptions } = {}) => { + const encoder = new Encoder({ fieldDescriptions }); + + mesgs.forEach(({ mesgNum, mesg }) => { + encoder.onMesg(mesgNum, mesg); + }); + + return encoder.close(); +} + +/** + * Decodes a byte array into FIT messages. + * @param {Uint8Array} byteArray - The byte array to decode. + * @param {Object} decoderOptions - Decoder configuration options. + * @returns {Object} The decoded FIT data. + */ +export const decodeByteArray = (byteArray, decoderOptions) => { + const stream = Stream.fromByteArray(byteArray); + const decoder = new Decoder(stream); + + return decoder.read(decoderOptions); +} + +/** + * Encodes an array of FIT messages and then decodes them back. + * @param {Array} mesgs - Array of message objects containing a mesg and its mesgNum. + * @param {Object} options - Encoding and decoding options. + * @param {Object} [options.fieldDescriptions] - fieldDescriptions for encoding developer fields. + * @param {Object} [options.decoderOptions] - Decoder configuration options. + * @returns {Object} The decoded FIT data. + */ +export const encodeThenDecodeMesgs = (mesgs = [], { fieldDescriptions, decoderOptions } = {}) => { + const byteArray = encodeMesgs(mesgs, { fieldDescriptions }); + + return decodeByteArray(byteArray, decoderOptions); +} + +/** + * Adds a custom field to a message profile for testing custom or unknown fields. + * @param {number} mesgNum - The mesgNum of the message to add the field to. + * @param {number} fieldNum - The field number for the new field. + * @param {Object} options - Field configuration options. + * @param {string} [options.name="testField"] - The name of the field. + * @param {string} [options.type="uint8"] - The field type. + * @param {string} [options.baseType="uint8"] - The base type of the field. + * @param {boolean} [options.array=false] - Whether the field is an array. + * @param {number} [options.scale=1] - The scale factor for the field value. + * @param {number} [options.offset=0] - The offset for the field value. + * @param {string} [options.units=""] - The units for the field. + * @param {number} [options.bits=0] - The number of bits used by the field. + * @param {Array} [options.components=[]] - Array of field components. + * @param {boolean} [options.hasComponents=false] - Whether the field has components. + * @param {boolean} [options.isAccumulated=false] - Whether the field is accumulated. + * @param {Array} [options.subFields=[]] - Array of subfields. + */ +export const addCustomFieldToFitMesgProfile = (mesgNum, fieldNum, { + name = `testField${fieldNum}`, + type = "uint8", + baseType = "uint8", + array = false, + scale = 1, + offset = 0, + units = "", + bits = 0, + components = [], + hasComponents = false, + isAccumulated = false, + subFields = [], +} = {}) => { + Profile.messages[mesgNum].fields[fieldNum] = { + num: fieldNum, + name, + type, + baseType, + array, + scale, + offset, + units, + bits, + components, + hasComponents, + isAccumulated, + subFields, + } +} + +/** + * Adds a custom message to the FIT Profile for testing custom or unknown messages. + * @param {number} mesgNum - The mesgNum for the message. + * @param {string} - The name of the message. + * @param {Object} [fields={}] - Object mapping field numbers to field definitions. Each field definition follows + * the structure of field definitions in the FIT Profile. + */ +export const addCustomMesgToFitProfile = (mesgNum, name = `testMesg${mesgNum}`, fieldDefs = {}) => { + Profile.messages[mesgNum] = { + num: mesgNum, + name, + messagesKey: `${name}Mesgs`, + fields: {}, + } + + Object.entries(fieldDefs).forEach(([fieldNum, fieldDef]) => { + addCustomFieldToFitMesgProfile(mesgNum, Number(fieldNum), fieldDef); + }); +} \ No newline at end of file diff --git a/test/utils-hr-mesg.test.js b/test/utils-hr-mesg.test.js index d071f93..dec7446 100644 --- a/test/utils-hr-mesg.test.js +++ b/test/utils-hr-mesg.test.js @@ -1,5 +1,5 @@ ///////////////////////////////////////////////////////////////////////////////////////////// -// Copyright 2025 Garmin International, Inc. +// Copyright 2026 Garmin International, Inc. // Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you // may not use this file except in compliance with the Flexible and Interoperable Data // Transfer (FIT) Protocol License. diff --git a/test/utils-memo-glob.test.js b/test/utils-memo-glob.test.js index 5017d5b..75fae77 100644 --- a/test/utils-memo-glob.test.js +++ b/test/utils-memo-glob.test.js @@ -1,5 +1,5 @@ ///////////////////////////////////////////////////////////////////////////////////////////// -// Copyright 2025 Garmin International, Inc. +// Copyright 2026 Garmin International, Inc. // Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you // may not use this file except in compliance with the Flexible and Interoperable Data // Transfer (FIT) Protocol License. diff --git a/test/utils.test.js b/test/utils.test.js index 74b1f4d..006656d 100644 --- a/test/utils.test.js +++ b/test/utils.test.js @@ -1,5 +1,5 @@ ///////////////////////////////////////////////////////////////////////////////////////////// -// Copyright 2025 Garmin International, Inc. +// Copyright 2026 Garmin International, Inc. // Licensed under the Flexible and Interoperable Data Transfer (FIT) Protocol License; you // may not use this file except in compliance with the Flexible and Interoperable Data // Transfer (FIT) Protocol License.