Initial release
This commit is contained in:
276
.gitignore
vendored
Normal file
276
.gitignore
vendored
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
# Created by https://www.toptal.com/developers/gitignore/api/node,react,sass,macos,linux,windows,visualstudiocode,vim
|
||||||
|
# Edit at https://www.toptal.com/developers/gitignore?templates=node,react,sass,macos,linux,windows,visualstudiocode,vim
|
||||||
|
|
||||||
|
### Linux ###
|
||||||
|
*~
|
||||||
|
|
||||||
|
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||||
|
.fuse_hidden*
|
||||||
|
|
||||||
|
# KDE directory preferences
|
||||||
|
.directory
|
||||||
|
|
||||||
|
# Linux trash folder which might appear on any partition or disk
|
||||||
|
.Trash-*
|
||||||
|
|
||||||
|
# .nfs files are created when an open file is removed but is still being accessed
|
||||||
|
.nfs*
|
||||||
|
|
||||||
|
### macOS ###
|
||||||
|
# General
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
|
||||||
|
# Icon must end with two \r
|
||||||
|
Icon
|
||||||
|
|
||||||
|
|
||||||
|
# Thumbnails
|
||||||
|
._*
|
||||||
|
|
||||||
|
# Files that might appear in the root of a volume
|
||||||
|
.DocumentRevisions-V100
|
||||||
|
.fseventsd
|
||||||
|
.Spotlight-V100
|
||||||
|
.TemporaryItems
|
||||||
|
.Trashes
|
||||||
|
.VolumeIcon.icns
|
||||||
|
.com.apple.timemachine.donotpresent
|
||||||
|
|
||||||
|
# Directories potentially created on remote AFP share
|
||||||
|
.AppleDB
|
||||||
|
.AppleDesktop
|
||||||
|
Network Trash Folder
|
||||||
|
Temporary Items
|
||||||
|
.apdisk
|
||||||
|
|
||||||
|
### macOS Patch ###
|
||||||
|
# iCloud generated files
|
||||||
|
*.icloud
|
||||||
|
|
||||||
|
### Node ###
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
.temp
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
### Node Patch ###
|
||||||
|
# Serverless Webpack directories
|
||||||
|
.webpack/
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
|
||||||
|
# SvelteKit build / generate output
|
||||||
|
.svelte-kit
|
||||||
|
|
||||||
|
### react ###
|
||||||
|
.DS_*
|
||||||
|
**/*.backup.*
|
||||||
|
**/*.back.*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
*.sublime*
|
||||||
|
|
||||||
|
psd
|
||||||
|
thumb
|
||||||
|
sketch
|
||||||
|
|
||||||
|
### Sass ###
|
||||||
|
.sass-cache/
|
||||||
|
*.css.map
|
||||||
|
*.sass.map
|
||||||
|
*.scss.map
|
||||||
|
|
||||||
|
### Vim ###
|
||||||
|
# Swap
|
||||||
|
[._]*.s[a-v][a-z]
|
||||||
|
!*.svg # comment out if you don't need vector files
|
||||||
|
[._]*.sw[a-p]
|
||||||
|
[._]s[a-rt-v][a-z]
|
||||||
|
[._]ss[a-gi-z]
|
||||||
|
[._]sw[a-p]
|
||||||
|
|
||||||
|
# Session
|
||||||
|
Session.vim
|
||||||
|
Sessionx.vim
|
||||||
|
|
||||||
|
# Temporary
|
||||||
|
.netrwhist
|
||||||
|
# Auto-generated tag files
|
||||||
|
tags
|
||||||
|
# Persistent undo
|
||||||
|
[._]*.un~
|
||||||
|
|
||||||
|
### VisualStudioCode ###
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
!.vscode/*.code-snippets
|
||||||
|
|
||||||
|
# Local History for Visual Studio Code
|
||||||
|
.history/
|
||||||
|
|
||||||
|
# Built Visual Studio Code Extensions
|
||||||
|
*.vsix
|
||||||
|
|
||||||
|
### VisualStudioCode Patch ###
|
||||||
|
# Ignore all local history of files
|
||||||
|
.history
|
||||||
|
.ionide
|
||||||
|
|
||||||
|
### Windows ###
|
||||||
|
# Windows thumbnail cache files
|
||||||
|
Thumbs.db
|
||||||
|
Thumbs.db:encryptable
|
||||||
|
ehthumbs.db
|
||||||
|
ehthumbs_vista.db
|
||||||
|
|
||||||
|
# Dump file
|
||||||
|
*.stackdump
|
||||||
|
|
||||||
|
# Folder config file
|
||||||
|
[Dd]esktop.ini
|
||||||
|
|
||||||
|
# Recycle Bin used on file shares
|
||||||
|
$RECYCLE.BIN/
|
||||||
|
|
||||||
|
# Windows Installer files
|
||||||
|
*.cab
|
||||||
|
*.msi
|
||||||
|
*.msix
|
||||||
|
*.msm
|
||||||
|
*.msp
|
||||||
|
|
||||||
|
# Windows shortcuts
|
||||||
|
*.lnk
|
||||||
|
|
||||||
|
# End of https://www.toptal.com/developers/gitignore/api/node,react,sass,macos,linux,windows,visualstudiocode,vim
|
||||||
30
.pre-commit-config.yaml
Normal file
30
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v6.0.0
|
||||||
|
hooks:
|
||||||
|
- id: check-yaml
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- id: trailing-whitespace
|
||||||
|
|
||||||
|
- repo: https://github.com/shellcheck-py/shellcheck-py
|
||||||
|
rev: v0.11.0.1
|
||||||
|
hooks:
|
||||||
|
- id: shellcheck
|
||||||
|
|
||||||
|
- repo: https://github.com/pre-commit/mirrors-eslint
|
||||||
|
rev: v10.0.3
|
||||||
|
hooks:
|
||||||
|
- id: eslint
|
||||||
|
files: "\\.(js|jsx|ts|tsx)$"
|
||||||
|
exclude: node_modules/
|
||||||
|
|
||||||
|
# Use stylelint (local) instead of the deprecated scss-lint Ruby gem which
|
||||||
|
# cannot parse modern Sass `@use` and module syntax. This invokes the
|
||||||
|
# project's installed `stylelint` via `npx` so the devDependency is used.
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: stylelint
|
||||||
|
name: stylelint
|
||||||
|
entry: npx stylelint --fix
|
||||||
|
language: system
|
||||||
|
files: "\\.(scss|sass|css)$"
|
||||||
51
.vscode/settings.json
vendored
Normal file
51
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"gopls": {
|
||||||
|
"formatting.local": "git.maze.io",
|
||||||
|
"ui.semanticTokens": true
|
||||||
|
},
|
||||||
|
|
||||||
|
// Global defaults for all other languages (4 spaces)
|
||||||
|
"editor.tabSize": 4,
|
||||||
|
"editor.insertSpaces": true,
|
||||||
|
"editor.detectIndentation": false,
|
||||||
|
|
||||||
|
// Go: Use tabs, with a tab size of 4
|
||||||
|
"[go]": {
|
||||||
|
"editor.insertSpaces": false,
|
||||||
|
"editor.tabSize": 4,
|
||||||
|
"editor.detectIndentation": false
|
||||||
|
},
|
||||||
|
|
||||||
|
// CSS, JavaScript, TypeScript, JSON: Use 2 spaces
|
||||||
|
"[css]": {
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
},
|
||||||
|
"[sass]": {
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
},
|
||||||
|
"[scss]": {
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
},
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
},
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
},
|
||||||
|
"[typescriptreact]": {
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
},
|
||||||
|
"[json]": {
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
},
|
||||||
|
"[yaml]": {
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
},
|
||||||
|
|
||||||
|
// For JSON with comments, often used in VSCode config files
|
||||||
|
"[jsonc]": {
|
||||||
|
"editor.insertSpaces": true,
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"editor.detectIndentation": false
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
docs/APRS12c.pdf
Normal file
BIN
docs/APRS12c.pdf
Normal file
Binary file not shown.
19
eslint.config.js
Normal file
19
eslint.config.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
3283
package-lock.json
generated
Normal file
3283
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
52
package.json
Normal file
52
package.json
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"name": "@hamradio/aprs",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "APRS (Automatic Packet Reporting System) protocol support for Typescript",
|
||||||
|
"keywords": [
|
||||||
|
"APRS",
|
||||||
|
"radio",
|
||||||
|
"amateur radio",
|
||||||
|
"HAM radio",
|
||||||
|
"rf"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.maze.io/ham/aprs.js"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"author": "Wijnand Modderman-Lenstra",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"module": "dist/index.mjs",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.mjs",
|
||||||
|
"require": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsup src/index.ts --format cjs,esm --dts --out-dir dist",
|
||||||
|
"dev": "tsup src/index.ts --format cjs,esm --dts --watch --out-dir dist",
|
||||||
|
"clean": "rm -rf dist",
|
||||||
|
"test": "vitest",
|
||||||
|
"test:watch": "vitest --watch",
|
||||||
|
"test:ci": "vitest --run",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"prepare": "npm run build"
|
||||||
|
},
|
||||||
|
"dependencies": {},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^10.0.1",
|
||||||
|
"@vitest/coverage-v8": "^4.0.18",
|
||||||
|
"eslint": "^10.0.3",
|
||||||
|
"globals": "^17.4.0",
|
||||||
|
"tsup": "^8.5.1",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"typescript-eslint": "^8.57.0",
|
||||||
|
"vitest": "^4.0.18"
|
||||||
|
}
|
||||||
|
}
|
||||||
1109
src/frame.ts
Normal file
1109
src/frame.ts
Normal file
File diff suppressed because it is too large
Load Diff
325
src/frame.types.ts
Normal file
325
src/frame.types.ts
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
import { PacketSegment, PacketStructure } from "./parser.types";
|
||||||
|
|
||||||
|
export interface IAddress {
|
||||||
|
call: string;
|
||||||
|
ssid: string;
|
||||||
|
isRepeated: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFrame {
|
||||||
|
source: IAddress;
|
||||||
|
destination: IAddress;
|
||||||
|
path: IAddress[];
|
||||||
|
payload: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// APRS Data Type Identifiers (first character of payload)
|
||||||
|
export const DataTypeIdentifier = {
|
||||||
|
// Position Reports
|
||||||
|
PositionNoTimestampNoMessaging: '!',
|
||||||
|
PositionNoTimestampWithMessaging: '=',
|
||||||
|
PositionWithTimestampNoMessaging: '/',
|
||||||
|
PositionWithTimestampWithMessaging: '@',
|
||||||
|
|
||||||
|
// Mic-E
|
||||||
|
MicECurrent: '`',
|
||||||
|
MicEOld: "'",
|
||||||
|
|
||||||
|
// Messages and Bulletins
|
||||||
|
Message: ':',
|
||||||
|
|
||||||
|
// Objects and Items
|
||||||
|
Object: ';',
|
||||||
|
Item: ')',
|
||||||
|
|
||||||
|
// Status
|
||||||
|
Status: '>',
|
||||||
|
|
||||||
|
// Query
|
||||||
|
Query: '?',
|
||||||
|
|
||||||
|
// Telemetry
|
||||||
|
TelemetryData: 'T',
|
||||||
|
|
||||||
|
// Weather
|
||||||
|
WeatherReportNoPosition: '_',
|
||||||
|
|
||||||
|
// Raw GPS Data
|
||||||
|
RawGPS: '$',
|
||||||
|
|
||||||
|
// Station Capabilities
|
||||||
|
StationCapabilities: '<',
|
||||||
|
|
||||||
|
// User-Defined
|
||||||
|
UserDefined: '{',
|
||||||
|
|
||||||
|
// Third-Party Traffic
|
||||||
|
ThirdParty: '}',
|
||||||
|
|
||||||
|
// Invalid/Test Data
|
||||||
|
InvalidOrTest: ',',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type DataTypeIdentifier = typeof DataTypeIdentifier[keyof typeof DataTypeIdentifier];
|
||||||
|
|
||||||
|
export interface ISymbol {
|
||||||
|
table: string; // Symbol table identifier
|
||||||
|
code: string; // Symbol code
|
||||||
|
|
||||||
|
toString(): string; // Return combined symbol representation (e.g., "tablecode")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position data common to multiple formats
|
||||||
|
export interface IPosition {
|
||||||
|
latitude: number; // Decimal degrees
|
||||||
|
longitude: number; // Decimal degrees
|
||||||
|
ambiguity?: number; // Position ambiguity (0-4)
|
||||||
|
altitude?: number; // Meters
|
||||||
|
speed?: number; // Speed in knots/kmh depending on source
|
||||||
|
course?: number; // Course in degrees
|
||||||
|
symbol?: ISymbol;
|
||||||
|
comment?: string;
|
||||||
|
|
||||||
|
toString(): string; // Return combined position representation (e.g., "lat,lon,alt")
|
||||||
|
toCompressed?(): CompressedPosition; // Optional method to convert to compressed format
|
||||||
|
distanceTo?(other: IPosition): number; // Optional method to calculate distance to another position
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITimestamp {
|
||||||
|
day?: number; // Day of month (DHM format)
|
||||||
|
month?: number; // Month (MDHM format)
|
||||||
|
hours: number;
|
||||||
|
minutes: number;
|
||||||
|
seconds?: number;
|
||||||
|
format: 'DHM' | 'HMS' | 'MDHM'; // Day-Hour-Minute, Hour-Minute-Second, Month-Day-Hour-Minute
|
||||||
|
zulu?: boolean; // Is UTC/Zulu time
|
||||||
|
toDate(): Date; // Convert to Date object respecting timezone
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position Report Payload
|
||||||
|
export interface PositionPayload {
|
||||||
|
type: 'position';
|
||||||
|
timestamp?: ITimestamp;
|
||||||
|
position: IPosition;
|
||||||
|
messaging: boolean; // Whether APRS messaging is enabled
|
||||||
|
micE?: {
|
||||||
|
messageType?: string;
|
||||||
|
isStandard?: boolean;
|
||||||
|
};
|
||||||
|
sections?: PacketSegment[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compressed Position Format
|
||||||
|
export interface CompressedPosition {
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
symbol: {
|
||||||
|
table: string;
|
||||||
|
code: string;
|
||||||
|
};
|
||||||
|
course?: number; // Degrees
|
||||||
|
speed?: number; // Knots
|
||||||
|
range?: number; // Miles
|
||||||
|
altitude?: number; // Feet
|
||||||
|
radioRange?: number; // Miles
|
||||||
|
compression: 'old' | 'current';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mic-E Payload (compressed in destination address)
|
||||||
|
export interface MicEPayload {
|
||||||
|
type: 'mic-e';
|
||||||
|
position: IPosition;
|
||||||
|
course?: number;
|
||||||
|
speed?: number;
|
||||||
|
altitude?: number;
|
||||||
|
messageType?: string; // Standard Mic-E message
|
||||||
|
telemetry?: number[]; // Optional telemetry channels
|
||||||
|
status?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message Payload
|
||||||
|
export interface MessagePayload {
|
||||||
|
type: 'message';
|
||||||
|
addressee: string; // 9 character padded callsign
|
||||||
|
text: string; // Message text
|
||||||
|
messageNumber?: string; // Message ID for acknowledgment
|
||||||
|
ack?: string; // Acknowledgment of message ID
|
||||||
|
reject?: string; // Rejection of message ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bulletin/Announcement (variant of message)
|
||||||
|
export interface BulletinPayload {
|
||||||
|
type: 'bulletin';
|
||||||
|
bulletinId: string; // Bulletin identifier (BLN#)
|
||||||
|
text: string;
|
||||||
|
group?: string; // Optional group bulletin
|
||||||
|
}
|
||||||
|
|
||||||
|
// Object Payload
|
||||||
|
export interface ObjectPayload {
|
||||||
|
type: 'object';
|
||||||
|
name: string; // 9 character object name
|
||||||
|
timestamp: ITimestamp;
|
||||||
|
alive: boolean; // True if object is active, false if killed
|
||||||
|
position: IPosition;
|
||||||
|
course?: number;
|
||||||
|
speed?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item Payload
|
||||||
|
export interface ItemPayload {
|
||||||
|
type: 'item';
|
||||||
|
name: string; // 3-9 character item name
|
||||||
|
alive: boolean; // True if item is active, false if killed
|
||||||
|
position: IPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status Payload
|
||||||
|
export interface StatusPayload {
|
||||||
|
type: 'status';
|
||||||
|
timestamp?: ITimestamp;
|
||||||
|
text: string;
|
||||||
|
maidenhead?: string; // Optional Maidenhead grid locator
|
||||||
|
symbol?: {
|
||||||
|
table: string;
|
||||||
|
code: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query Payload
|
||||||
|
export interface QueryPayload {
|
||||||
|
type: 'query';
|
||||||
|
queryType: string; // e.g., 'APRSD', 'APRST', 'PING'
|
||||||
|
target?: string; // Target callsign or area
|
||||||
|
}
|
||||||
|
|
||||||
|
// Telemetry Data Payload
|
||||||
|
export interface TelemetryDataPayload {
|
||||||
|
type: 'telemetry-data';
|
||||||
|
sequence: number;
|
||||||
|
analog: number[]; // Up to 5 analog channels
|
||||||
|
digital: number; // 8-bit digital value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Telemetry Parameter Names
|
||||||
|
export interface TelemetryParameterPayload {
|
||||||
|
type: 'telemetry-parameters';
|
||||||
|
names: string[]; // Parameter names
|
||||||
|
}
|
||||||
|
|
||||||
|
// Telemetry Unit/Label
|
||||||
|
export interface TelemetryUnitPayload {
|
||||||
|
type: 'telemetry-units';
|
||||||
|
units: string[]; // Units for each parameter
|
||||||
|
}
|
||||||
|
|
||||||
|
// Telemetry Coefficients
|
||||||
|
export interface TelemetryCoefficientsPayload {
|
||||||
|
type: 'telemetry-coefficients';
|
||||||
|
coefficients: {
|
||||||
|
a: number[]; // a coefficients
|
||||||
|
b: number[]; // b coefficients
|
||||||
|
c: number[]; // c coefficients
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Telemetry Bit Sense/Project Name
|
||||||
|
export interface TelemetryBitSensePayload {
|
||||||
|
type: 'telemetry-bitsense';
|
||||||
|
sense: number; // 8-bit sense value
|
||||||
|
projectName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weather Report Payload
|
||||||
|
export interface WeatherPayload {
|
||||||
|
type: 'weather';
|
||||||
|
timestamp?: ITimestamp;
|
||||||
|
position?: IPosition;
|
||||||
|
windDirection?: number; // Degrees
|
||||||
|
windSpeed?: number; // MPH
|
||||||
|
windGust?: number; // MPH
|
||||||
|
temperature?: number; // Fahrenheit
|
||||||
|
rainLastHour?: number; // Hundredths of inch
|
||||||
|
rainLast24Hours?: number; // Hundredths of inch
|
||||||
|
rainSinceMidnight?: number; // Hundredths of inch
|
||||||
|
humidity?: number; // Percent
|
||||||
|
pressure?: number; // Tenths of millibar
|
||||||
|
luminosity?: number; // Watts per square meter
|
||||||
|
snowfall?: number; // Inches
|
||||||
|
rawRain?: number; // Raw rain counter
|
||||||
|
software?: string; // Weather software type
|
||||||
|
weatherUnit?: string; // Weather station type
|
||||||
|
}
|
||||||
|
|
||||||
|
// Raw GPS Payload (NMEA sentences)
|
||||||
|
export interface RawGPSPayload {
|
||||||
|
type: 'raw-gps';
|
||||||
|
sentence: string; // Raw NMEA sentence
|
||||||
|
}
|
||||||
|
|
||||||
|
// Station Capabilities Payload
|
||||||
|
export interface StationCapabilitiesPayload {
|
||||||
|
type: 'capabilities';
|
||||||
|
capabilities: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// User-Defined Payload
|
||||||
|
export interface UserDefinedPayload {
|
||||||
|
type: 'user-defined';
|
||||||
|
userPacketType: string;
|
||||||
|
data: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Third-Party Traffic Payload
|
||||||
|
export interface ThirdPartyPayload {
|
||||||
|
type: 'third-party';
|
||||||
|
header: string; // Source path of third-party packet
|
||||||
|
payload: string; // Nested APRS packet
|
||||||
|
}
|
||||||
|
|
||||||
|
// DF Report Payload
|
||||||
|
export interface DFReportPayload {
|
||||||
|
type: 'df-report';
|
||||||
|
timestamp?: ITimestamp;
|
||||||
|
position: IPosition;
|
||||||
|
course?: number;
|
||||||
|
bearing?: number; // Direction finding bearing
|
||||||
|
quality?: number; // Signal quality
|
||||||
|
strength?: number; // Signal strength
|
||||||
|
height?: number; // Antenna height
|
||||||
|
gain?: number; // Antenna gain
|
||||||
|
directivity?: string; // Antenna directivity pattern
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BasePayload {
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Union type for all decoded payload types
|
||||||
|
export type Payload = BasePayload & (
|
||||||
|
| PositionPayload
|
||||||
|
| MicEPayload
|
||||||
|
| MessagePayload
|
||||||
|
| BulletinPayload
|
||||||
|
| ObjectPayload
|
||||||
|
| ItemPayload
|
||||||
|
| StatusPayload
|
||||||
|
| QueryPayload
|
||||||
|
| TelemetryDataPayload
|
||||||
|
| TelemetryParameterPayload
|
||||||
|
| TelemetryUnitPayload
|
||||||
|
| TelemetryCoefficientsPayload
|
||||||
|
| TelemetryBitSensePayload
|
||||||
|
| WeatherPayload
|
||||||
|
| RawGPSPayload
|
||||||
|
| StationCapabilitiesPayload
|
||||||
|
| UserDefinedPayload
|
||||||
|
| ThirdPartyPayload
|
||||||
|
| DFReportPayload
|
||||||
|
);
|
||||||
|
|
||||||
|
// Extended Frame with decoded payload
|
||||||
|
export interface DecodedFrame extends IFrame {
|
||||||
|
decoded?: Payload;
|
||||||
|
structure?: PacketStructure; // Routing and other frame-level sections
|
||||||
|
}
|
||||||
57
src/index.ts
Normal file
57
src/index.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
export {
|
||||||
|
Frame,
|
||||||
|
Address,
|
||||||
|
Timestamp,
|
||||||
|
} from "./frame";
|
||||||
|
|
||||||
|
export {
|
||||||
|
type IAddress,
|
||||||
|
type IFrame,
|
||||||
|
DataTypeIdentifier,
|
||||||
|
} from "./frame.types";
|
||||||
|
|
||||||
|
export {
|
||||||
|
type ISymbol,
|
||||||
|
type IPosition,
|
||||||
|
type ITimestamp,
|
||||||
|
type PositionPayload,
|
||||||
|
type CompressedPosition,
|
||||||
|
type MicEPayload,
|
||||||
|
type MessagePayload,
|
||||||
|
type BulletinPayload,
|
||||||
|
type ObjectPayload,
|
||||||
|
type ItemPayload,
|
||||||
|
type StatusPayload,
|
||||||
|
type QueryPayload,
|
||||||
|
type TelemetryDataPayload,
|
||||||
|
type TelemetryParameterPayload,
|
||||||
|
type TelemetryUnitPayload,
|
||||||
|
type TelemetryCoefficientsPayload,
|
||||||
|
type TelemetryBitSensePayload,
|
||||||
|
type WeatherPayload,
|
||||||
|
type RawGPSPayload,
|
||||||
|
type StationCapabilitiesPayload,
|
||||||
|
type UserDefinedPayload,
|
||||||
|
type ThirdPartyPayload,
|
||||||
|
type DFReportPayload,
|
||||||
|
type BasePayload,
|
||||||
|
type Payload,
|
||||||
|
type DecodedFrame,
|
||||||
|
} from "./frame.types";
|
||||||
|
|
||||||
|
export {
|
||||||
|
base91ToNumber,
|
||||||
|
knotsToKmh,
|
||||||
|
kmhToKnots,
|
||||||
|
feetToMeters,
|
||||||
|
metersToFeet,
|
||||||
|
celsiusToFahrenheit,
|
||||||
|
fahrenheitToCelsius,
|
||||||
|
} from "./parser";
|
||||||
|
export {
|
||||||
|
type PacketStructure,
|
||||||
|
type PacketSegment,
|
||||||
|
type PacketField,
|
||||||
|
type PacketFieldBit,
|
||||||
|
FieldType,
|
||||||
|
} from "./parser.types";
|
||||||
91
src/parser.ts
Normal file
91
src/parser.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* Decode a Base91 encoded string to a number.
|
||||||
|
* Base91 uses ASCII characters 33-123 (! to {) for encoding.
|
||||||
|
* Each character represents a digit in base 91.
|
||||||
|
*
|
||||||
|
* @param str Base91 encoded string (typically 4 characters for 32-bit values in APRS)
|
||||||
|
* @returns Decoded number value
|
||||||
|
*/
|
||||||
|
export const base91ToNumber = (str: string): number => {
|
||||||
|
let value = 0;
|
||||||
|
const base = 91;
|
||||||
|
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
const charCode = str.charCodeAt(i);
|
||||||
|
const digit = charCode - 33; // Base91 uses chars 33-123 (! to {)
|
||||||
|
|
||||||
|
if (digit < 0 || digit >= base) {
|
||||||
|
throw new Error(`Invalid Base91 character: '${str[i]}' (code ${charCode})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
value = value * base + digit;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Conversions from Freedom Units to whatever the rest of the world uses and understands. */
|
||||||
|
|
||||||
|
const KNOTS_TO_KMH = 1.852;
|
||||||
|
const FEET_TO_METERS = 0.3048;
|
||||||
|
const FAHRENHEIT_TO_CELSIUS_OFFSET = 32;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert speed from knots to kilometers per hour.
|
||||||
|
*
|
||||||
|
* @param knots number of knots
|
||||||
|
* @returns equivalent speed in kilometers per hour
|
||||||
|
*/
|
||||||
|
export const knotsToKmh = (knots: number): number => {
|
||||||
|
return knots * KNOTS_TO_KMH;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert speed from kilometers per hour to knots.
|
||||||
|
*
|
||||||
|
* @param kmh speed in kilometers per hour
|
||||||
|
* @returns equivalent speed in knots
|
||||||
|
*/
|
||||||
|
export const kmhToKnots = (kmh: number): number => {
|
||||||
|
return kmh / KNOTS_TO_KMH;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert altitude from feet to meters.
|
||||||
|
*
|
||||||
|
* @param feet altitude in feet
|
||||||
|
* @returns equivalent altitude in meters
|
||||||
|
*/
|
||||||
|
export const feetToMeters = (feet: number): number => {
|
||||||
|
return feet * FEET_TO_METERS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert altitude from meters to feet.
|
||||||
|
*
|
||||||
|
* @param meters altitude in meters
|
||||||
|
* @returns equivalent altitude in feet
|
||||||
|
*/
|
||||||
|
export const metersToFeet = (meters: number): number => {
|
||||||
|
return meters / FEET_TO_METERS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert temperature from Celsius to Fahrenheit.
|
||||||
|
*
|
||||||
|
* @param celsius temperature in Celsius
|
||||||
|
* @returns equivalent temperature in Fahrenheit
|
||||||
|
*/
|
||||||
|
export const celsiusToFahrenheit = (celsius: number): number => {
|
||||||
|
return (celsius * 9/5) + FAHRENHEIT_TO_CELSIUS_OFFSET;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert temperature from Fahrenheit to Celsius.
|
||||||
|
*
|
||||||
|
* @param fahrenheit temperature in Fahrenheit
|
||||||
|
* @returns equivalent temperature in Celsius
|
||||||
|
*/
|
||||||
|
export const fahrenheitToCelsius = (fahrenheit: number): number => {
|
||||||
|
return (fahrenheit - FAHRENHEIT_TO_CELSIUS_OFFSET) * 5/9;
|
||||||
|
}
|
||||||
37
src/parser.types.ts
Normal file
37
src/parser.types.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
export enum FieldType {
|
||||||
|
BITS = 0,
|
||||||
|
UINT8 = 1,
|
||||||
|
UINT16_LE = 2,
|
||||||
|
UINT16_BE = 3,
|
||||||
|
UINT32_LE = 4,
|
||||||
|
UINT32_BE = 5,
|
||||||
|
BYTES = 6, // 8-bits per value
|
||||||
|
WORDS = 7, // 16-bits per value
|
||||||
|
DWORDS = 8, // 32-bits per value
|
||||||
|
QWORDS = 9, // 64-bits per value
|
||||||
|
STRING = 10,
|
||||||
|
C_STRING = 11, // Null-terminated string
|
||||||
|
CHAR = 12, // Single ASCII character
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface for the parsed packet segments, used for debugging and testing.
|
||||||
|
export type PacketStructure = PacketSegment[];
|
||||||
|
|
||||||
|
export interface PacketSegment {
|
||||||
|
name: string;
|
||||||
|
data: Uint8Array;
|
||||||
|
fields: PacketField[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PacketField {
|
||||||
|
type: FieldType;
|
||||||
|
size: number; // Size in bytes
|
||||||
|
name?: string;
|
||||||
|
bits?: PacketFieldBit[]; // Only for bit fields in FieldType.BITS
|
||||||
|
value?: any; // Optional decoded value
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PacketFieldBit {
|
||||||
|
name: string;
|
||||||
|
size: number; // Size in bits
|
||||||
|
}
|
||||||
72
src/position.ts
Normal file
72
src/position.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { IPosition, ISymbol } from "./frame.types";
|
||||||
|
|
||||||
|
export class Symbol implements ISymbol {
|
||||||
|
table: string; // Symbol table identifier
|
||||||
|
code: string; // Symbol code
|
||||||
|
|
||||||
|
constructor(table: string, code?: string) {
|
||||||
|
if (code === undefined) {
|
||||||
|
if (table.length === 2) {
|
||||||
|
this.code = table[1];
|
||||||
|
this.table = table[0];
|
||||||
|
} else {
|
||||||
|
throw new Error(`Invalid symbol format: '${table}' (expected 2 characters if code is not provided)`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.table = table;
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public toString(): string {
|
||||||
|
return `${this.table}${this.code}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Position implements IPosition {
|
||||||
|
latitude: number; // Decimal degrees
|
||||||
|
longitude: number; // Decimal degrees
|
||||||
|
ambiguity?: number; // Position ambiguity (0-4)
|
||||||
|
altitude?: number; // Meters
|
||||||
|
speed?: number; // Speed in knots/kmh depending on source
|
||||||
|
course?: number; // Course in degrees
|
||||||
|
symbol?: Symbol;
|
||||||
|
comment?: string;
|
||||||
|
|
||||||
|
constructor(data: Partial<IPosition>) {
|
||||||
|
this.latitude = data.latitude ?? 0;
|
||||||
|
this.longitude = data.longitude ?? 0;
|
||||||
|
this.ambiguity = data.ambiguity;
|
||||||
|
this.altitude = data.altitude;
|
||||||
|
this.speed = data.speed;
|
||||||
|
this.course = data.course;
|
||||||
|
if (typeof data.symbol === 'string') {
|
||||||
|
this.symbol = new Symbol(data.symbol);
|
||||||
|
} else if (data.symbol) {
|
||||||
|
this.symbol = new Symbol(data.symbol.table, data.symbol.code);
|
||||||
|
}
|
||||||
|
this.comment = data.comment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public toString(): string {
|
||||||
|
const latStr = this.latitude.toFixed(5);
|
||||||
|
const lonStr = this.longitude.toFixed(5);
|
||||||
|
const altStr = this.altitude !== undefined ? `,${this.altitude}m` : '';
|
||||||
|
return `${latStr},${lonStr}${altStr}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public distanceTo(other: IPosition): number {
|
||||||
|
const R = 6371e3; // Earth radius in meters
|
||||||
|
const φ1 = this.latitude * Math.PI / 180;
|
||||||
|
const φ2 = other.latitude * Math.PI / 180;
|
||||||
|
const Δφ = (other.latitude - this.latitude) * Math.PI / 180;
|
||||||
|
const Δλ = (other.longitude - this.longitude) * Math.PI / 180;
|
||||||
|
|
||||||
|
const a = Math.sin(Δφ/2) * Math.sin(Δφ/2) +
|
||||||
|
Math.cos(φ1) * Math.cos(φ2) *
|
||||||
|
Math.sin(Δλ/2) * Math.sin(Δλ/2);
|
||||||
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
|
||||||
|
|
||||||
|
return R * c; // Distance in meters
|
||||||
|
}
|
||||||
|
}
|
||||||
1125
test/frame.test.ts
Normal file
1125
test/frame.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
89
test/parser.test.ts
Normal file
89
test/parser.test.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import {
|
||||||
|
base91ToNumber,
|
||||||
|
knotsToKmh,
|
||||||
|
kmhToKnots,
|
||||||
|
feetToMeters,
|
||||||
|
metersToFeet,
|
||||||
|
celsiusToFahrenheit,
|
||||||
|
fahrenheitToCelsius,
|
||||||
|
} from '../src/parser';
|
||||||
|
|
||||||
|
describe('parser utilities', () => {
|
||||||
|
describe('base91ToNumber', () => {
|
||||||
|
it('decodes all-! to 0', () => {
|
||||||
|
expect(base91ToNumber('!!!!')).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('decodes single character correctly', () => {
|
||||||
|
// 'A' === 65, digit = 65 - 33 = 32
|
||||||
|
expect(base91ToNumber('A')).toBe(32);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should decode multiple Base91 characters', () => {
|
||||||
|
// "!!" = 0 * 91 + 0 = 0
|
||||||
|
expect(base91ToNumber('!!')).toBe(0);
|
||||||
|
|
||||||
|
// "!#" = 0 * 91 + 2 = 2
|
||||||
|
expect(base91ToNumber('!#')).toBe(2);
|
||||||
|
|
||||||
|
// "#!" = 2 * 91 + 0 = 182
|
||||||
|
expect(base91ToNumber('#!')).toBe(182);
|
||||||
|
|
||||||
|
// "##" = 2 * 91 + 2 = 184
|
||||||
|
expect(base91ToNumber('##')).toBe(184);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should decode 4-character Base91 strings (used in APRS)', () => {
|
||||||
|
// Test with printable ASCII Base91 characters (33-123)
|
||||||
|
const testValue = base91ToNumber('!#%\'');
|
||||||
|
expect(testValue).toBeGreaterThan(0);
|
||||||
|
expect(testValue).toBeLessThan(91 * 91 * 91 * 91);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should decode maximum valid Base91 value', () => {
|
||||||
|
// Maximum is '{' (ASCII 123, digit 90) repeated
|
||||||
|
const maxValue = base91ToNumber('{{{{');
|
||||||
|
const expected = 90 * 91 * 91 * 91 + 90 * 91 * 91 + 90 * 91 + 90;
|
||||||
|
expect(maxValue).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle APRS compressed position example', () => {
|
||||||
|
// Using actual characters from APRS test vector
|
||||||
|
const latStr = '/:*E';
|
||||||
|
const lonStr = 'qZ=O';
|
||||||
|
|
||||||
|
const latValue = base91ToNumber(latStr);
|
||||||
|
const lonValue = base91ToNumber(lonStr);
|
||||||
|
|
||||||
|
// Just verify they decode without error and produce valid numbers
|
||||||
|
expect(typeof latValue).toBe('number');
|
||||||
|
expect(typeof lonValue).toBe('number');
|
||||||
|
expect(latValue).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(lonValue).toBeGreaterThanOrEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws on invalid character', () => {
|
||||||
|
expect(() => base91ToNumber(' ')).toThrow(); // space (code 32) is invalid
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('unit conversions', () => {
|
||||||
|
it('converts knots <-> km/h', () => {
|
||||||
|
expect(knotsToKmh(10)).toBeCloseTo(18.52, 5);
|
||||||
|
expect(kmhToKnots(18.52)).toBeCloseTo(10, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts feet <-> meters', () => {
|
||||||
|
expect(feetToMeters(10)).toBeCloseTo(3.048, 6);
|
||||||
|
expect(metersToFeet(3.048)).toBeCloseTo(10, 6);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts celsius <-> fahrenheit', () => {
|
||||||
|
expect(celsiusToFahrenheit(0)).toBeCloseTo(32, 6);
|
||||||
|
expect(fahrenheitToCelsius(32)).toBeCloseTo(0, 6);
|
||||||
|
expect(celsiusToFahrenheit(100)).toBeCloseTo(212, 6);
|
||||||
|
expect(fahrenheitToCelsius(212)).toBeCloseTo(100, 6);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
19
tsconfig.json
Normal file
19
tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2019",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": false,
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
"include": ["src", "test/crypto.test.ts", "test/identity.test.ts", "test/packet.test.ts", "test/parser.test.ts"],
|
||||||
|
"exclude": ["node_modules", "dist", "test"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user