@springfield/ham-radio-utils 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +212 -0
  2. package/README.md +92 -0
  3. package/dist/db/bands.json +149 -0
  4. package/dist/db/license-classes.json +22 -0
  5. package/dist/index.d.ts +11 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +11 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/memory/segmented-memory.d.ts +12 -0
  10. package/dist/memory/segmented-memory.d.ts.map +1 -0
  11. package/dist/memory/segmented-memory.js +39 -0
  12. package/dist/memory/segmented-memory.js.map +1 -0
  13. package/dist/test-utils/radio-channel-factory.d.ts +5 -0
  14. package/dist/test-utils/radio-channel-factory.d.ts.map +1 -0
  15. package/dist/test-utils/radio-channel-factory.js +16 -0
  16. package/dist/test-utils/radio-channel-factory.js.map +1 -0
  17. package/dist/test-utils/radio-programmed-channel-factory.d.ts +4 -0
  18. package/dist/test-utils/radio-programmed-channel-factory.d.ts.map +1 -0
  19. package/dist/test-utils/radio-programmed-channel-factory.js +7 -0
  20. package/dist/test-utils/radio-programmed-channel-factory.js.map +1 -0
  21. package/dist/utils/band-plan.d.ts +24 -0
  22. package/dist/utils/band-plan.d.ts.map +1 -0
  23. package/dist/utils/band-plan.js +27 -0
  24. package/dist/utils/band-plan.js.map +1 -0
  25. package/dist/utils/bcd-to-number.d.ts +2 -0
  26. package/dist/utils/bcd-to-number.d.ts.map +1 -0
  27. package/dist/utils/bcd-to-number.js +9 -0
  28. package/dist/utils/bcd-to-number.js.map +1 -0
  29. package/dist/utils/frequency-display.d.ts +3 -0
  30. package/dist/utils/frequency-display.d.ts.map +1 -0
  31. package/dist/utils/frequency-display.js +5 -0
  32. package/dist/utils/frequency-display.js.map +1 -0
  33. package/dist/utils/memory-channel-utils.d.ts +2 -0
  34. package/dist/utils/memory-channel-utils.d.ts.map +1 -0
  35. package/dist/utils/memory-channel-utils.js +6 -0
  36. package/dist/utils/memory-channel-utils.js.map +1 -0
  37. package/dist/utils/memory-segment-utils.d.ts +3 -0
  38. package/dist/utils/memory-segment-utils.d.ts.map +1 -0
  39. package/dist/utils/memory-segment-utils.js +6 -0
  40. package/dist/utils/memory-segment-utils.js.map +1 -0
  41. package/dist/utils/number-to-bcd.d.ts +2 -0
  42. package/dist/utils/number-to-bcd.d.ts.map +1 -0
  43. package/dist/utils/number-to-bcd.js +12 -0
  44. package/dist/utils/number-to-bcd.js.map +1 -0
  45. package/dist/utils/to-hex-words.d.ts +2 -0
  46. package/dist/utils/to-hex-words.d.ts.map +1 -0
  47. package/dist/utils/to-hex-words.js +12 -0
  48. package/dist/utils/to-hex-words.js.map +1 -0
  49. package/package.json +68 -0
  50. package/src/db/bands.json +149 -0
  51. package/src/db/license-classes.json +22 -0
  52. package/src/index.ts +12 -0
  53. package/src/memory/segmented-memory.ts +52 -0
  54. package/src/test-utils/radio-channel-factory.ts +17 -0
  55. package/src/test-utils/radio-programmed-channel-factory.ts +8 -0
  56. package/src/utils/band-plan.ts +36 -0
  57. package/src/utils/bcd-to-number.ts +10 -0
  58. package/src/utils/frequency-display.ts +6 -0
  59. package/src/utils/memory-channel-utils.ts +6 -0
  60. package/src/utils/memory-segment-utils.ts +7 -0
  61. package/src/utils/number-to-bcd.ts +15 -0
  62. package/src/utils/to-hex-words.ts +14 -0
@@ -0,0 +1,24 @@
1
+ import { SpectrumBand } from '@springfield/ham-radio-api';
2
+ interface LicenseClass {
3
+ id: string;
4
+ name: string;
5
+ }
6
+ export declare class BandPlan {
7
+ private bands;
8
+ private licenseClasses;
9
+ constructor();
10
+ /**
11
+ * Finds the spectrum band that contains the given frequency
12
+ * @param frequency The frequency in Hz to find the band for
13
+ * @returns The spectrum band containing the frequency, or undefined if no band contains it
14
+ */
15
+ findBandByFrequency(frequency: number): SpectrumBand | undefined;
16
+ /**
17
+ * Finds a license class by its ID
18
+ * @param id The ID of the license class to find
19
+ * @returns The license class with the given ID, or undefined if not found
20
+ */
21
+ findPrivilegeById(id: string): LicenseClass | undefined;
22
+ }
23
+ export {};
24
+ //# sourceMappingURL=band-plan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"band-plan.d.ts","sourceRoot":"","sources":["../../src/utils/band-plan.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAI1D,UAAU,YAAY;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd;AAED,qBAAa,QAAQ;IACnB,OAAO,CAAC,KAAK,CAAiB;IAC9B,OAAO,CAAC,cAAc,CAA4B;;IAOlD;;;;OAIG;IACH,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS;IAIhE;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS;CAGxD"}
@@ -0,0 +1,27 @@
1
+ import bandsData from '../db/bands.json' with { type: 'json' };
2
+ import licenseClassesData from '../db/license-classes.json' with { type: 'json' };
3
+ export class BandPlan {
4
+ bands;
5
+ licenseClasses;
6
+ constructor() {
7
+ this.bands = bandsData;
8
+ this.licenseClasses = new Map(licenseClassesData.map((licenseClass) => [licenseClass.id, licenseClass]));
9
+ }
10
+ /**
11
+ * Finds the spectrum band that contains the given frequency
12
+ * @param frequency The frequency in Hz to find the band for
13
+ * @returns The spectrum band containing the frequency, or undefined if no band contains it
14
+ */
15
+ findBandByFrequency(frequency) {
16
+ return this.bands.find((band) => frequency >= band.lowerFrequency && frequency <= band.upperFrequency);
17
+ }
18
+ /**
19
+ * Finds a license class by its ID
20
+ * @param id The ID of the license class to find
21
+ * @returns The license class with the given ID, or undefined if not found
22
+ */
23
+ findPrivilegeById(id) {
24
+ return this.licenseClasses.get(id);
25
+ }
26
+ }
27
+ //# sourceMappingURL=band-plan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"band-plan.js","sourceRoot":"","sources":["../../src/utils/band-plan.ts"],"names":[],"mappings":"AACA,OAAO,SAAS,MAAM,kBAAkB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAC;AAC/D,OAAO,kBAAkB,MAAM,4BAA4B,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAC;AAOlF,MAAM,OAAO,QAAQ;IACX,KAAK,CAAiB;IACtB,cAAc,CAA4B;IAElD;QACE,IAAI,CAAC,KAAK,GAAG,SAA2B,CAAC;QACzC,IAAI,CAAC,cAAc,GAAG,IAAI,GAAG,CAAE,kBAAqC,CAAC,GAAG,CAAC,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC,YAAY,CAAC,EAAE,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC;IAC/H,CAAC;IAED;;;;OAIG;IACH,mBAAmB,CAAC,SAAiB;QACnC,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,SAAS,IAAI,IAAI,CAAC,cAAc,IAAI,SAAS,IAAI,IAAI,CAAC,cAAc,CAAC,CAAC;IACzG,CAAC;IAED;;;;OAIG;IACH,iBAAiB,CAAC,EAAU;QAC1B,OAAO,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACrC,CAAC;CACF","sourcesContent":["import { SpectrumBand } from '@springfield/ham-radio-api';\nimport bandsData from '../db/bands.json' with { type: 'json' };\nimport licenseClassesData from '../db/license-classes.json' with { type: 'json' };\n\ninterface LicenseClass {\n id: string;\n name: string;\n}\n\nexport class BandPlan {\n private bands: SpectrumBand[];\n private licenseClasses: Map<string, LicenseClass>;\n\n constructor() {\n this.bands = bandsData as SpectrumBand[];\n this.licenseClasses = new Map((licenseClassesData as LicenseClass[]).map((licenseClass) => [licenseClass.id, licenseClass]));\n }\n\n /**\n * Finds the spectrum band that contains the given frequency\n * @param frequency The frequency in Hz to find the band for\n * @returns The spectrum band containing the frequency, or undefined if no band contains it\n */\n findBandByFrequency(frequency: number): SpectrumBand | undefined {\n return this.bands.find((band) => frequency >= band.lowerFrequency && frequency <= band.upperFrequency);\n }\n\n /**\n * Finds a license class by its ID\n * @param id The ID of the license class to find\n * @returns The license class with the given ID, or undefined if not found\n */\n findPrivilegeById(id: string): LicenseClass | undefined {\n return this.licenseClasses.get(id);\n }\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export declare function bcdToNumber(data: Uint8Array, offset?: number, length?: number): number;
2
+ //# sourceMappingURL=bcd-to-number.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bcd-to-number.d.ts","sourceRoot":"","sources":["../../src/utils/bcd-to-number.ts"],"names":[],"mappings":"AAAA,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAStF"}
@@ -0,0 +1,9 @@
1
+ export function bcdToNumber(data, offset, length) {
2
+ let bcd = 0;
3
+ const size = length ?? data.length;
4
+ for (let i = offset ?? 0; i < size; i++) {
5
+ bcd |= data[i] << (8 * i);
6
+ }
7
+ return parseInt(bcd.toString(16), 10) * 10;
8
+ }
9
+ //# sourceMappingURL=bcd-to-number.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bcd-to-number.js","sourceRoot":"","sources":["../../src/utils/bcd-to-number.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,WAAW,CAAC,IAAgB,EAAE,MAAe,EAAE,MAAe;IAC5E,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,MAAM,IAAI,GAAG,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC;IAEnC,KAAK,IAAI,CAAC,GAAG,MAAM,IAAI,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC;QACxC,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5B,CAAC;IAED,OAAO,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,EAAE,CAAC;AAC7C,CAAC","sourcesContent":["export function bcdToNumber(data: Uint8Array, offset?: number, length?: number): number {\n let bcd = 0;\n const size = length ?? data.length;\n\n for (let i = offset ?? 0; i < size; i++) {\n bcd |= data[i] << (8 * i);\n }\n\n return parseInt(bcd.toString(16), 10) * 10;\n}\n"]}
@@ -0,0 +1,3 @@
1
+ import { Frequency, SpectrumBand } from '@springfield/ham-radio-api';
2
+ export declare function frequencyDisplay(frequency: Frequency, band: SpectrumBand): string;
3
+ //# sourceMappingURL=frequency-display.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frequency-display.d.ts","sourceRoot":"","sources":["../../src/utils/frequency-display.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAErE,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE,YAAY,GAAG,MAAM,CAGjF"}
@@ -0,0 +1,5 @@
1
+ export function frequencyDisplay(frequency, band) {
2
+ const frequencyDisplay = frequency / band.frequencyDisplayBaseMultiplier;
3
+ return frequencyDisplay.toFixed(band.frequencyDisplayNumberDecimals);
4
+ }
5
+ //# sourceMappingURL=frequency-display.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frequency-display.js","sourceRoot":"","sources":["../../src/utils/frequency-display.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,gBAAgB,CAAC,SAAoB,EAAE,IAAkB;IACvE,MAAM,gBAAgB,GAAG,SAAS,GAAG,IAAI,CAAC,8BAA8B,CAAC;IACzE,OAAO,gBAAgB,CAAC,OAAO,CAAC,IAAI,CAAC,8BAA8B,CAAC,CAAC;AACvE,CAAC","sourcesContent":["import { Frequency, SpectrumBand } from '@springfield/ham-radio-api';\n\nexport function frequencyDisplay(frequency: Frequency, band: SpectrumBand): string {\n const frequencyDisplay = frequency / band.frequencyDisplayBaseMultiplier;\n return frequencyDisplay.toFixed(band.frequencyDisplayNumberDecimals);\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export declare function formatChannel(channelNumber: number, data: Uint8Array): string;
2
+ //# sourceMappingURL=memory-channel-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-channel-utils.d.ts","sourceRoot":"","sources":["../../src/utils/memory-channel-utils.ts"],"names":[],"mappings":"AAGA,wBAAgB,aAAa,CAAC,aAAa,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CAE7E"}
@@ -0,0 +1,6 @@
1
+ import { sprintf } from 'sprintf-js';
2
+ import { toHexWords } from './to-hex-words.js';
3
+ export function formatChannel(channelNumber, data) {
4
+ return sprintf('%2d : %s', channelNumber, toHexWords(data));
5
+ }
6
+ //# sourceMappingURL=memory-channel-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-channel-utils.js","sourceRoot":"","sources":["../../src/utils/memory-channel-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACrC,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAE/C,MAAM,UAAU,aAAa,CAAC,aAAqB,EAAE,IAAgB;IACnE,OAAO,OAAO,CAAC,UAAU,EAAE,aAAa,EAAE,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;AAC9D,CAAC","sourcesContent":["import { sprintf } from 'sprintf-js';\nimport { toHexWords } from './to-hex-words.js';\n\nexport function formatChannel(channelNumber: number, data: Uint8Array): string {\n return sprintf('%2d : %s', channelNumber, toHexWords(data));\n}\n"]}
@@ -0,0 +1,3 @@
1
+ import { RadioMemorySegment } from '@springfield/ham-radio-api';
2
+ export declare function formatSegment(segment: RadioMemorySegment): string;
3
+ //# sourceMappingURL=memory-segment-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-segment-utils.d.ts","sourceRoot":"","sources":["../../src/utils/memory-segment-utils.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAEhE,wBAAgB,aAAa,CAAC,OAAO,EAAE,kBAAkB,GAAG,MAAM,CAEjE"}
@@ -0,0 +1,6 @@
1
+ import { sprintf } from 'sprintf-js';
2
+ import { toHexWords } from './to-hex-words.js';
3
+ export function formatSegment(segment) {
4
+ return sprintf('%04X : %s', segment.startAddress, toHexWords(segment.data));
5
+ }
6
+ //# sourceMappingURL=memory-segment-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-segment-utils.js","sourceRoot":"","sources":["../../src/utils/memory-segment-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACrC,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAG/C,MAAM,UAAU,aAAa,CAAC,OAA2B;IACvD,OAAO,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC,YAAY,EAAE,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;AAC9E,CAAC","sourcesContent":["import { sprintf } from 'sprintf-js';\nimport { toHexWords } from './to-hex-words.js';\nimport { RadioMemorySegment } from '@springfield/ham-radio-api';\n\nexport function formatSegment(segment: RadioMemorySegment): string {\n return sprintf('%04X : %s', segment.startAddress, toHexWords(segment.data));\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export declare function numberToBcd(value: number): Uint8Array;
2
+ //# sourceMappingURL=number-to-bcd.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"number-to-bcd.d.ts","sourceRoot":"","sources":["../../src/utils/number-to-bcd.ts"],"names":[],"mappings":"AAAA,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAcrD"}
@@ -0,0 +1,12 @@
1
+ export function numberToBcd(value) {
2
+ const digits = value.toString(10);
3
+ const numberOfBytes = Math.ceil(digits.length / 2.0);
4
+ const data = new Uint8Array(numberOfBytes);
5
+ let bcd = parseInt(digits, 16);
6
+ for (let i = 0; i < numberOfBytes; i++) {
7
+ data[i] = bcd & 0xff;
8
+ bcd >>= 8;
9
+ }
10
+ return data;
11
+ }
12
+ //# sourceMappingURL=number-to-bcd.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"number-to-bcd.js","sourceRoot":"","sources":["../../src/utils/number-to-bcd.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,MAAM,MAAM,GAAG,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IAClC,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC;IAErD,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,aAAa,CAAC,CAAC;IAE3C,IAAI,GAAG,GAAG,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAE/B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,aAAa,EAAE,CAAC,EAAE,EAAE,CAAC;QACvC,IAAI,CAAC,CAAC,CAAC,GAAG,GAAG,GAAG,IAAI,CAAC;QACrB,GAAG,KAAK,CAAC,CAAC;IACZ,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC","sourcesContent":["export function numberToBcd(value: number): Uint8Array {\n const digits = value.toString(10);\n const numberOfBytes = Math.ceil(digits.length / 2.0);\n\n const data = new Uint8Array(numberOfBytes);\n\n let bcd = parseInt(digits, 16);\n\n for (let i = 0; i < numberOfBytes; i++) {\n data[i] = bcd & 0xff;\n bcd >>= 8;\n }\n\n return data;\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export declare function toHexWords(data: Uint8Array): string;
2
+ //# sourceMappingURL=to-hex-words.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"to-hex-words.d.ts","sourceRoot":"","sources":["../../src/utils/to-hex-words.ts"],"names":[],"mappings":"AAAA,wBAAgB,UAAU,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAanD"}
@@ -0,0 +1,12 @@
1
+ export function toHexWords(data) {
2
+ const hexData = Buffer.from(data).toString('hex');
3
+ let formattedData = '';
4
+ for (let i = 0; i < hexData.length; i += 4) {
5
+ if (i != 0) {
6
+ formattedData += ' ';
7
+ }
8
+ formattedData += hexData.substring(i, i + 4);
9
+ }
10
+ return formattedData;
11
+ }
12
+ //# sourceMappingURL=to-hex-words.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"to-hex-words.js","sourceRoot":"","sources":["../../src/utils/to-hex-words.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,UAAU,CAAC,IAAgB;IACzC,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAClD,IAAI,aAAa,GAAG,EAAE,CAAC;IAEvB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAC3C,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACX,aAAa,IAAI,GAAG,CAAC;QACvB,CAAC;QAED,aAAa,IAAI,OAAO,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC/C,CAAC;IAED,OAAO,aAAa,CAAC;AACvB,CAAC","sourcesContent":["export function toHexWords(data: Uint8Array): string {\n const hexData = Buffer.from(data).toString('hex');\n let formattedData = '';\n\n for (let i = 0; i < hexData.length; i += 4) {\n if (i != 0) {\n formattedData += ' ';\n }\n\n formattedData += hexData.substring(i, i + 4);\n }\n\n return formattedData;\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@springfield/ham-radio-utils",
3
+ "version": "1.0.0",
4
+ "author": "Bryan Hunt",
5
+ "packageManager": "yarn@4.9.1",
6
+ "readme": "README.md",
7
+ "type": "module",
8
+ "main": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "files": [
11
+ "src/",
12
+ "dist/"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc && cp src/db/* dist/db/",
16
+ "commitlint": "commitlint --edit",
17
+ "release": "semantic-release",
18
+ "prepare": "husky"
19
+ },
20
+ "release": {
21
+ "branches": [
22
+ "main",
23
+ {
24
+ "name": "beta",
25
+ "prerelease": true
26
+ }
27
+ ],
28
+ "plugins": [
29
+ [
30
+ "@semantic-release/commit-analyzer",
31
+ {
32
+ "preset": "conventionalcommits"
33
+ }
34
+ ],
35
+ [
36
+ "@semantic-release/release-notes-generator",
37
+ {
38
+ "preset": "conventionalcommits"
39
+ }
40
+ ],
41
+ "@semantic-release/changelog",
42
+ "semantic-release-yarn",
43
+ "@semantic-release/gitlab",
44
+ "@semantic-release/git"
45
+ ]
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "dependencies": {
51
+ "@springfield/ham-radio-api": "^12.0.1",
52
+ "fishery": "^2.3.1",
53
+ "loglayer": "^6.4.2",
54
+ "sprintf-js": "^1.1.3"
55
+ },
56
+ "devDependencies": {
57
+ "@commitlint/cli": "19.8.1",
58
+ "@commitlint/config-conventional": "19.8.1",
59
+ "@semantic-release/changelog": "6.0.3",
60
+ "@semantic-release/git": "10.0.1",
61
+ "@semantic-release/gitlab": "13.2.5",
62
+ "@types/sprintf-js": "1.1.4",
63
+ "husky": "9.1.7",
64
+ "semantic-release": "23.1.1",
65
+ "semantic-release-yarn": "3.0.2",
66
+ "typescript": "5.8.3"
67
+ }
68
+ }
@@ -0,0 +1,149 @@
1
+ [
2
+ {
3
+ "id": "d1a5c7e8-3f9b-4a6c-8d2e-1b3f7a9c5e8d",
4
+ "name": "160 Meter",
5
+ "wavelength": 160,
6
+ "lowerFrequency": 1800000000,
7
+ "upperFrequency": 2000000000,
8
+ "frequencyDisplayBaseMultiplier": 1000000,
9
+ "frequencyDisplayNumberDecimals": 3,
10
+ "privileges": [
11
+ "b7c9d9e4-5f0a-4f2d-9c2d-1e3b8e7a8b5f",
12
+ "f3a6c1d2-8e7f-4a5b-9d3e-2c4a9b6e7f8d",
13
+ "d6e8f7a2-3b4c-5d9e-1f2a-8c3b7e4a9f6d"
14
+ ]
15
+ },
16
+ {
17
+ "id": "e3b7a9c5-d1a5-4f8d-2e1b-c7e83f9b6a4c",
18
+ "name": "80 Meter",
19
+ "wavelength": 80,
20
+ "lowerFrequency": 3500000000,
21
+ "upperFrequency": 4000000000,
22
+ "frequencyDisplayBaseMultiplier": 1000000,
23
+ "frequencyDisplayNumberDecimals": 3,
24
+ "privileges": [
25
+ "b7c9d9e4-5f0a-4f2d-9c2d-1e3b8e7a8b5f",
26
+ "f3a6c1d2-8e7f-4a5b-9d3e-2c4a9b6e7f8d",
27
+ "d6e8f7a2-3b4c-5d9e-1f2a-8c3b7e4a9f6d"
28
+ ]
29
+ },
30
+ {
31
+ "id": "f0a4c5e8-d1b3-c7e83f9b-a6c8-d2e1b9c5f7ea",
32
+ "name": "60 Meter",
33
+ "wavelength": 60,
34
+ "lowerFrequency": 5332000,
35
+ "upperFrequency": 5405000,
36
+ "frequencyDisplayBaseMultiplier": 1000,
37
+ "frequencyDisplayNumberDecimals": 1,
38
+ "privileges": [
39
+ "f3a6c1d2-8e7f-4a5b-9d3e-2c4a9b6e7f8d",
40
+ "d6e8f7a2-3b4c-5d9e-1f2a-8c3b7e4a9f6d"
41
+ ],
42
+ "channels": [
43
+ {
44
+ "name": "Channel 1",
45
+ "frequency": 5332
46
+ },
47
+ {
48
+ "name": "Channel 2",
49
+ "frequency": 5348
50
+ },
51
+ {
52
+ "name": "Channel 3",
53
+ "frequency": 5358.5
54
+ },
55
+ {
56
+ "name": "Channel 4",
57
+ "frequency": 5373
58
+ },
59
+ {
60
+ "name": "Channel 5",
61
+ "frequency": 5405
62
+ }
63
+ ]
64
+ },
65
+ {
66
+ "id": "c5ea-f0a4-d1b39-c7ec83fa-a6cd82eb19cf",
67
+ "name": "40 Meter",
68
+ "wavelength": 40,
69
+ "lowerFrequency": 7000000000,
70
+ "upperFrequency": 7300000000,
71
+ "frequencyDisplayBaseMultiplier": 1000000,
72
+ "frequencyDisplayNumberDecimals": 3,
73
+ "privileges": [
74
+ "b7c9d9e4-5f0a-4f2d-9c2d-1e3b8e7a8b5f",
75
+ "f3a6c1d2-8e7f-4a5b-9d3e-2c4a9b6e7f8d",
76
+ "d6e8f7a2-3b4c-5d9e-1f2a-8c3b7e4a9f6d"
77
+ ]
78
+ },
79
+ {
80
+ "id": "fa83ec-d12ab39-c75ea-a46cd82eb19cf",
81
+ "name": "20 Meter",
82
+ "wavelength": 20,
83
+ "lowerFrequency": 14000000000,
84
+ "upperFrequency": 14350000000,
85
+ "frequencyDisplayBaseMultiplier": 1000000,
86
+ "frequencyDisplayNumberDecimals": 3,
87
+ "privileges": [
88
+ "f3a6c1d2-8e7f-4a5b-9d3e-2c4a9b6e7f8d",
89
+ "d6e8f7a2-3b4c-5d9e-1f2a-8c3b7e4a9f6d"
90
+ ]
91
+ },
92
+ {
93
+ "id": "a1b2c3d4-e5f6-7g8h-9i0j-k1l2m3n4o5p6",
94
+ "name": "15 Meter",
95
+ "wavelength": 15,
96
+ "lowerFrequency": 21000000,
97
+ "upperFrequency": 21450000,
98
+ "frequencyDisplayBaseMultiplier": 1000,
99
+ "frequencyDisplayNumberDecimals": 0,
100
+ "privileges": [
101
+ "b7c9d9e4-5f0a-4f2d-9c2d-1e3b8e7a8b5f",
102
+ "f3a6c1d2-8e7f-4a5b-9d3e-2c4a9b6e7f8d",
103
+ "d6e8f7a2-3b4c-5d9e-1f2a-8c3b7e4a9f6d",
104
+ "a2c4b6f8-7e9d-3f5b-1c2a-9e7f8d3b6a4c"
105
+ ]
106
+ },
107
+ {
108
+ "id": "p6o5n4m3-l2k1-j0i9-h8g7-f6e5-d4c3b2a1",
109
+ "name": "10 Meter",
110
+ "wavelength": 10,
111
+ "lowerFrequency": 28000000,
112
+ "upperFrequency": 29700000,
113
+ "frequencyDisplayBaseMultiplier": 1000,
114
+ "frequencyDisplayNumberDecimals": 0,
115
+ "privileges": [
116
+ "b7c9d9e4-5f0a-4f2d-9c2d-1e3b8e7a8b5f",
117
+ "f3a6c1d2-8e7f-4a5b-9d3e-2c4a9b6e7f8d",
118
+ "d6e8f7a2-3b4c-5d9e-1f2a-8c3b7e4a9f6d"
119
+ ]
120
+ },
121
+ {
122
+ "id": "z1y2x3w4-v5u6-t7s8-r9q0-p1o2n3m4l5k6",
123
+ "name": "2 Meter",
124
+ "wavelength": 2,
125
+ "lowerFrequency": 144000000,
126
+ "upperFrequency": 148000000,
127
+ "frequencyDisplayBaseMultiplier": 1000,
128
+ "frequencyDisplayNumberDecimals": 0,
129
+ "privileges": [
130
+ "b7c9d9e4-5f0a-4f2d-9c2d-1e3b8e7a8b5f",
131
+ "f3a6c1d2-8e7f-4a5b-9d3e-2c4a9b6e7f8d",
132
+ "d6e8f7a2-3b4c-5d9e-1f2a-8c3b7e4a9f6d"
133
+ ]
134
+ },
135
+ {
136
+ "id": "k6l5m4n3-o2p1-q0r9-s8t7-u6v5-w4x3y2z1",
137
+ "name": "70 Centimeter",
138
+ "wavelength": 0.7,
139
+ "lowerFrequency": 420000000,
140
+ "upperFrequency": 450000000,
141
+ "frequencyDisplayBaseMultiplier": 1000,
142
+ "frequencyDisplayNumberDecimals": 0,
143
+ "privileges": [
144
+ "b7c9d9e4-5f0a-4f2d-9c2d-1e3b8e7a8b5f",
145
+ "f3a6c1d2-8e7f-4a5b-9d3e-2c4a9b6e7f8d",
146
+ "d6e8f7a2-3b4c-5d9e-1f2a-8c3b7e4a9f6d"
147
+ ]
148
+ }
149
+ ]
@@ -0,0 +1,22 @@
1
+ [
2
+ {
3
+ "id": "b7c9d9e4-5f0a-4f2d-9c2d-1e3b8e7a8b5f",
4
+ "name": "Technician"
5
+ },
6
+ {
7
+ "id": "f3a6c1d2-8e7f-4a5b-9d3e-2c4a9b6e7f8d",
8
+ "name": "General"
9
+ },
10
+ {
11
+ "id": "d6e8f7a2-3b4c-5d9e-1f2a-8c3b7e4a9f6d",
12
+ "name": "Amateur Extra"
13
+ },
14
+ {
15
+ "id": "a2c4b6f8-7e9d-3f5b-1c2a-9e7f8d3b6a4c",
16
+ "name": "Advanced (Grandfathered)"
17
+ },
18
+ {
19
+ "id": "e7f9d3b6-a2c4-5f1a-8c3b-6f8d2a4b9e7f",
20
+ "name": "Novice (Grandfathered)"
21
+ }
22
+ ]
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ export * from './memory/segmented-memory.js';
2
+
3
+ export * from './test-utils/radio-channel-factory.js';
4
+ export * from './test-utils/radio-programmed-channel-factory.js';
5
+
6
+ export * from './utils/bcd-to-number.js';
7
+ export * from './utils/band-plan.js';
8
+ export * from './utils/frequency-display.js';
9
+ export * from './utils/memory-channel-utils.js';
10
+ export * from './utils/memory-segment-utils.js';
11
+ export * from './utils/number-to-bcd.js';
12
+ export * from './utils/to-hex-words.js';
@@ -0,0 +1,52 @@
1
+ import { RadioMemorySegment } from '@springfield/ham-radio-api';
2
+
3
+ export class RadioSegmentedMemory {
4
+ private segments: Map<number, RadioMemorySegment>;
5
+
6
+ constructor(segments?: RadioMemorySegment[]) {
7
+ this.segments = new Map();
8
+
9
+ if (segments != undefined) {
10
+ for (const segment of segments) {
11
+ this.segments.set(segment.startAddress, segment);
12
+ }
13
+ }
14
+ }
15
+
16
+ public addSegment(segment: RadioMemorySegment): void {
17
+ this.segments.set(segment.startAddress, segment);
18
+ }
19
+
20
+ public getSegments(): IterableIterator<RadioMemorySegment> {
21
+ return this.segments.values();
22
+ }
23
+
24
+ public getSegment(startAddress: number): RadioMemorySegment | undefined {
25
+ return this.segments.get(startAddress);
26
+ }
27
+
28
+ public segmentCount(): number {
29
+ return this.segments.size;
30
+ }
31
+
32
+ public findSegment(address: number): RadioMemorySegment | undefined {
33
+ const iterator = this.segments.values();
34
+ let result = iterator.next();
35
+
36
+ while (!result.done) {
37
+ const segment = result.value;
38
+
39
+ if (segment.startAddress <= address && segment.startAddress + segment.length > address) {
40
+ return segment;
41
+ }
42
+
43
+ result = iterator.next();
44
+ }
45
+
46
+ return undefined;
47
+ }
48
+
49
+ public toArray(): RadioMemorySegment[] {
50
+ return Array.from(this.segments.values());
51
+ }
52
+ }
@@ -0,0 +1,17 @@
1
+ import { CTCSS, Frequency, RadioChannel, RadioChannelId, RadioToneType } from '@springfield/ham-radio-api';
2
+ import { Factory } from 'fishery';
3
+
4
+ export function getRandomCTCSS(): CTCSS {
5
+ const values = Object.values(CTCSS); // Get all values from the enum
6
+ const randomIndex = Math.floor(Math.random() * values.length); // Generate a random index
7
+ return values[randomIndex] as CTCSS; // Return the randomly selected value
8
+ }
9
+
10
+ export const radioChannelFactory = Factory.define<RadioChannel>(({ sequence }) => ({
11
+ id: RadioChannelId(sequence.toString()),
12
+ name: `c-${sequence}`,
13
+ transmitFrequency: Frequency(144000),
14
+ receiveFrequency: Frequency(144000),
15
+ transmitTone: { type: RadioToneType.CTCSS, tone: getRandomCTCSS() },
16
+ receiveTone: { type: RadioToneType.CTCSS, tone: getRandomCTCSS() },
17
+ }));
@@ -0,0 +1,8 @@
1
+ import { RadioProgrammedChannel } from '@springfield/ham-radio-api';
2
+ import { Factory } from 'fishery';
3
+ import { radioChannelFactory } from './radio-channel-factory.js';
4
+
5
+ export const radioProgrammedChannelFactory = Factory.define<RadioProgrammedChannel>(({ sequence }) => ({
6
+ channelNumber: sequence,
7
+ radioChannel: radioChannelFactory.build(),
8
+ }));
@@ -0,0 +1,36 @@
1
+ import { SpectrumBand } from '@springfield/ham-radio-api';
2
+ import bandsData from '../db/bands.json' with { type: 'json' };
3
+ import licenseClassesData from '../db/license-classes.json' with { type: 'json' };
4
+
5
+ interface LicenseClass {
6
+ id: string;
7
+ name: string;
8
+ }
9
+
10
+ export class BandPlan {
11
+ private bands: SpectrumBand[];
12
+ private licenseClasses: Map<string, LicenseClass>;
13
+
14
+ constructor() {
15
+ this.bands = bandsData as SpectrumBand[];
16
+ this.licenseClasses = new Map((licenseClassesData as LicenseClass[]).map((licenseClass) => [licenseClass.id, licenseClass]));
17
+ }
18
+
19
+ /**
20
+ * Finds the spectrum band that contains the given frequency
21
+ * @param frequency The frequency in Hz to find the band for
22
+ * @returns The spectrum band containing the frequency, or undefined if no band contains it
23
+ */
24
+ findBandByFrequency(frequency: number): SpectrumBand | undefined {
25
+ return this.bands.find((band) => frequency >= band.lowerFrequency && frequency <= band.upperFrequency);
26
+ }
27
+
28
+ /**
29
+ * Finds a license class by its ID
30
+ * @param id The ID of the license class to find
31
+ * @returns The license class with the given ID, or undefined if not found
32
+ */
33
+ findPrivilegeById(id: string): LicenseClass | undefined {
34
+ return this.licenseClasses.get(id);
35
+ }
36
+ }
@@ -0,0 +1,10 @@
1
+ export function bcdToNumber(data: Uint8Array, offset?: number, length?: number): number {
2
+ let bcd = 0;
3
+ const size = length ?? data.length;
4
+
5
+ for (let i = offset ?? 0; i < size; i++) {
6
+ bcd |= data[i] << (8 * i);
7
+ }
8
+
9
+ return parseInt(bcd.toString(16), 10) * 10;
10
+ }
@@ -0,0 +1,6 @@
1
+ import { Frequency, SpectrumBand } from '@springfield/ham-radio-api';
2
+
3
+ export function frequencyDisplay(frequency: Frequency, band: SpectrumBand): string {
4
+ const frequencyDisplay = frequency / band.frequencyDisplayBaseMultiplier;
5
+ return frequencyDisplay.toFixed(band.frequencyDisplayNumberDecimals);
6
+ }
@@ -0,0 +1,6 @@
1
+ import { sprintf } from 'sprintf-js';
2
+ import { toHexWords } from './to-hex-words.js';
3
+
4
+ export function formatChannel(channelNumber: number, data: Uint8Array): string {
5
+ return sprintf('%2d : %s', channelNumber, toHexWords(data));
6
+ }
@@ -0,0 +1,7 @@
1
+ import { sprintf } from 'sprintf-js';
2
+ import { toHexWords } from './to-hex-words.js';
3
+ import { RadioMemorySegment } from '@springfield/ham-radio-api';
4
+
5
+ export function formatSegment(segment: RadioMemorySegment): string {
6
+ return sprintf('%04X : %s', segment.startAddress, toHexWords(segment.data));
7
+ }
@@ -0,0 +1,15 @@
1
+ export function numberToBcd(value: number): Uint8Array {
2
+ const digits = value.toString(10);
3
+ const numberOfBytes = Math.ceil(digits.length / 2.0);
4
+
5
+ const data = new Uint8Array(numberOfBytes);
6
+
7
+ let bcd = parseInt(digits, 16);
8
+
9
+ for (let i = 0; i < numberOfBytes; i++) {
10
+ data[i] = bcd & 0xff;
11
+ bcd >>= 8;
12
+ }
13
+
14
+ return data;
15
+ }
@@ -0,0 +1,14 @@
1
+ export function toHexWords(data: Uint8Array): string {
2
+ const hexData = Buffer.from(data).toString('hex');
3
+ let formattedData = '';
4
+
5
+ for (let i = 0; i < hexData.length; i += 4) {
6
+ if (i != 0) {
7
+ formattedData += ' ';
8
+ }
9
+
10
+ formattedData += hexData.substring(i, i + 4);
11
+ }
12
+
13
+ return formattedData;
14
+ }