icom-wlan-node 0.5.0 → 0.6.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.
@@ -49,12 +49,34 @@ const smeter_1 = require("../utils/smeter");
49
49
  const IcomScopeCommands_1 = require("../scope/IcomScopeCommands");
50
50
  const IcomScopeParser_1 = require("../scope/IcomScopeParser");
51
51
  const IcomScopeService_1 = require("../scope/IcomScopeService");
52
+ const IcomCivFrame_1 = require("./IcomCivFrame");
53
+ const IcomCivSpec_1 = require("./IcomCivSpec");
54
+ const IcomProfiles_1 = require("./IcomProfiles");
55
+ const DEFAULT_SCOPE_SPANS_HZ = [25000000, 10000000, 5000000, 2500000, 1000000, 500000, 250000, 100000, 50000, 25000, 10000, 5000, 2500];
56
+ function modeCodeToName(mode) {
57
+ switch (mode) {
58
+ case 0: return 'center';
59
+ case 1: return 'fixed';
60
+ case 2: return 'scroll-center';
61
+ case 3: return 'scroll-fixed';
62
+ }
63
+ }
64
+ function modeNameToCode(mode) {
65
+ switch (mode) {
66
+ case 'center': return 0;
67
+ case 'fixed': return 1;
68
+ case 'scroll-center': return 2;
69
+ case 'scroll-fixed': return 3;
70
+ }
71
+ }
52
72
  class IcomControl {
53
73
  constructor(options) {
54
74
  this.ev = new events_1.EventEmitter();
55
75
  this.rigName = '';
56
76
  this.macAddress = Buffer.alloc(6);
57
77
  this.civAssembleBuf = Buffer.alloc(0); // CIV stream reassembler
78
+ this.activeProfile = (0, IcomProfiles_1.getProfileByModel)('generic-modern-icom');
79
+ this.lastFilter = 1;
58
80
  // Connection state machine (replaces old fragmented state flags)
59
81
  this.connectionSession = {
60
82
  phase: types_1.ConnectionPhase.IDLE,
@@ -72,7 +94,8 @@ class IcomControl {
72
94
  reconnectBaseDelay: 2000,
73
95
  reconnectMaxDelay: 30000
74
96
  };
75
- this.options = options;
97
+ this.options = { ...options, model: options.model ?? 'auto' };
98
+ this.activeProfile = (0, IcomProfiles_1.resolveIcomProfile)({ requestedModel: this.options.model });
76
99
  // Setup control session
77
100
  this.sess = new Session_1.Session({ ip: options.control.ip, port: options.control.port }, {
78
101
  onData: (data) => this.onData(data),
@@ -100,6 +123,25 @@ class IcomControl {
100
123
  this.scope.on('scopeFrame', (frame) => this.ev.emit('scopeFrame', frame));
101
124
  }
102
125
  get events() { return this.ev; }
126
+ get profile() { return this.activeProfile; }
127
+ resolveActiveProfile(context = {}) {
128
+ const next = (0, IcomProfiles_1.resolveIcomProfile)({
129
+ requestedModel: this.options.model,
130
+ rigName: context.rigName ?? this.rigName,
131
+ civAddress: context.civAddress ?? this.civ.civAddress,
132
+ });
133
+ if (this.options.model && this.options.model !== 'auto' && context.civAddress !== undefined && next.defaultCivAddress !== (context.civAddress & 0xff)) {
134
+ (0, debug_1.dbg)(`Configured profile ${next.modelId} default CI-V 0x${next.defaultCivAddress.toString(16)} differs from radio CI-V 0x${(context.civAddress & 0xff).toString(16)}`);
135
+ }
136
+ if (next.modelId !== this.activeProfile.modelId) {
137
+ (0, debug_1.dbg)(`ICOM profile selected: ${next.profileName}`);
138
+ }
139
+ this.activeProfile = next;
140
+ this.lastFilter = next.defaultFilter;
141
+ }
142
+ getProfileModelId() {
143
+ return this.activeProfile.modelId;
144
+ }
103
145
  // ============================================================================
104
146
  // State Machine Management
105
147
  // ============================================================================
@@ -538,10 +580,10 @@ class IcomControl {
538
580
  if (!resp || resp.length < 13) {
539
581
  return null;
540
582
  }
541
- const spanHz = (0, IcomScopeParser_1.parseIcomBcdFreqLE)(resp.subarray(7, 12));
583
+ const encodedSpanHz = (0, IcomScopeParser_1.parseIcomBcdFreqLE)(resp.subarray(7, 12));
542
584
  return {
543
585
  receiver,
544
- spanHz,
586
+ spanHz: encodedSpanHz * 2,
545
587
  };
546
588
  }
547
589
  async setScopeSpan(spanHz, options) {
@@ -550,6 +592,182 @@ class IcomControl {
550
592
  const rigAddr = this.civ.civAddress & 0xff;
551
593
  this.sendCiv(IcomScopeCommands_1.IcomScopeCommands.setScopeSpan(ctrAddr, rigAddr, spanHz, receiver));
552
594
  }
595
+ async readScopeMode(options) {
596
+ const timeoutMs = options?.timeout ?? 3000;
597
+ const receiver = options?.receiver ?? 0;
598
+ const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
599
+ const rigAddr = this.civ.civAddress & 0xff;
600
+ const req = IcomScopeCommands_1.IcomScopeCommands.readScopeMode(ctrAddr, rigAddr, receiver);
601
+ const resp = await this.waitForCivFrame((frame) => IcomControl.matchCommandFrame(frame, 0x27, [0x14, receiver], ctrAddr, rigAddr), timeoutMs, () => this.sendCiv(req));
602
+ if (!resp || resp.length < 9) {
603
+ return null;
604
+ }
605
+ const mode = resp[7];
606
+ return {
607
+ receiver,
608
+ mode,
609
+ modeName: modeCodeToName(mode),
610
+ };
611
+ }
612
+ async setScopeMode(mode, options) {
613
+ const receiver = options?.receiver ?? 0;
614
+ const modeCode = typeof mode === 'string' ? modeNameToCode(mode) : mode;
615
+ const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
616
+ const rigAddr = this.civ.civAddress & 0xff;
617
+ this.sendCiv(IcomScopeCommands_1.IcomScopeCommands.setScopeMode(ctrAddr, rigAddr, modeCode, receiver));
618
+ }
619
+ async readScopeEdge(options) {
620
+ const timeoutMs = options?.timeout ?? 3000;
621
+ const receiver = options?.receiver ?? 0;
622
+ const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
623
+ const rigAddr = this.civ.civAddress & 0xff;
624
+ const req = IcomScopeCommands_1.IcomScopeCommands.readScopeEdge(ctrAddr, rigAddr, receiver);
625
+ const resp = await this.waitForCivFrame((frame) => IcomControl.matchCommandFrame(frame, 0x27, [0x16, receiver], ctrAddr, rigAddr), timeoutMs, () => this.sendCiv(req));
626
+ if (!resp || resp.length < 9) {
627
+ return null;
628
+ }
629
+ return {
630
+ receiver,
631
+ edgeSlot: resp[7],
632
+ };
633
+ }
634
+ async setScopeEdge(edgeSlot, options) {
635
+ const receiver = options?.receiver ?? 0;
636
+ const slots = this.activeProfile.scopeEdgeSlots;
637
+ const maxSlot = slots.length ? Math.max(...slots) : 4;
638
+ const safeEdgeSlot = Math.max(1, Math.min(maxSlot, Math.trunc(edgeSlot)));
639
+ const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
640
+ const rigAddr = this.civ.civAddress & 0xff;
641
+ this.sendCiv(IcomScopeCommands_1.IcomScopeCommands.setScopeEdge(ctrAddr, rigAddr, safeEdgeSlot, receiver));
642
+ }
643
+ async readScopeFixedEdge(rangeId, edgeSlot, options) {
644
+ const timeoutMs = options?.timeout ?? 3000;
645
+ const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
646
+ const rigAddr = this.civ.civAddress & 0xff;
647
+ const req = IcomScopeCommands_1.IcomScopeCommands.readScopeFixedEdge(ctrAddr, rigAddr, rangeId, edgeSlot);
648
+ const resp = await this.waitForCivFrame((frame) => IcomControl.matchCommandFrame(frame, 0x27, [0x1e, rangeId, edgeSlot], ctrAddr, rigAddr), timeoutMs, () => this.sendCiv(req));
649
+ if (!resp || resp.length < 18) {
650
+ return null;
651
+ }
652
+ return {
653
+ rangeId,
654
+ edgeSlot,
655
+ lowHz: (0, IcomScopeParser_1.parseIcomBcdFreqLE)(resp.subarray(8, 13)),
656
+ highHz: (0, IcomScopeParser_1.parseIcomBcdFreqLE)(resp.subarray(13, 18)),
657
+ };
658
+ }
659
+ async setScopeFixedEdge(options) {
660
+ const rangeId = options.rangeId ?? await this.resolveScopeFrequencyRangeId();
661
+ const edgeInfo = options.edgeSlot
662
+ ? { edgeSlot: options.edgeSlot }
663
+ : await this.readScopeEdge({ receiver: 0, timeout: 3000 });
664
+ const edgeSlot = edgeInfo?.edgeSlot ?? 1;
665
+ const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
666
+ const rigAddr = this.civ.civAddress & 0xff;
667
+ this.sendCiv(IcomScopeCommands_1.IcomScopeCommands.setScopeFixedEdge(ctrAddr, rigAddr, rangeId, edgeSlot, options.lowHz, options.highHz));
668
+ return {
669
+ rangeId,
670
+ edgeSlot,
671
+ lowHz: options.lowHz,
672
+ highHz: options.highHz,
673
+ };
674
+ }
675
+ async resolveScopeFrequencyRangeId(frequencyHz) {
676
+ const targetFrequency = frequencyHz ?? await this.readOperatingFrequency({ timeout: 3000 });
677
+ if (!targetFrequency) {
678
+ throw new Error('Unable to resolve scope frequency range without operating frequency');
679
+ }
680
+ const matched = this.activeProfile.scopeRanges.find((range) => targetFrequency >= range.lowHz && targetFrequency < range.highHz);
681
+ if (!matched) {
682
+ throw new Error(`No scope frequency range matches ${targetFrequency} Hz`);
683
+ }
684
+ return matched.rangeId;
685
+ }
686
+ getScopeSupportedEdgeSlots() {
687
+ return [...this.activeProfile.scopeEdgeSlots];
688
+ }
689
+ async getSpectrumDisplayState(options) {
690
+ const receiver = options?.receiver ?? 0;
691
+ const [modeInfo, spanInfo, edgeInfo] = await Promise.all([
692
+ this.readScopeMode({ ...options, receiver }),
693
+ this.readScopeSpan({ ...options, receiver }),
694
+ this.readScopeEdge({ ...options, receiver }),
695
+ ]);
696
+ let fixedEdgeInfo = null;
697
+ if (modeInfo?.modeName === 'fixed' || modeInfo?.modeName === 'scroll-fixed') {
698
+ try {
699
+ const rangeId = await this.resolveScopeFrequencyRangeId();
700
+ fixedEdgeInfo = await this.readScopeFixedEdge(rangeId, edgeInfo?.edgeSlot ?? 1, options);
701
+ }
702
+ catch (_) {
703
+ fixedEdgeInfo = null;
704
+ }
705
+ }
706
+ return {
707
+ mode: modeInfo?.modeName ?? null,
708
+ modeCode: modeInfo?.mode ?? null,
709
+ spanHz: spanInfo?.spanHz ?? (fixedEdgeInfo ? fixedEdgeInfo.highHz - fixedEdgeInfo.lowHz : null),
710
+ edgeSlot: edgeInfo?.edgeSlot ?? null,
711
+ edgeLowHz: fixedEdgeInfo?.lowHz ?? null,
712
+ edgeHighHz: fixedEdgeInfo?.highHz ?? null,
713
+ supportedModes: ['center', 'fixed', 'scroll-center', 'scroll-fixed'],
714
+ supportedSpans: [...DEFAULT_SCOPE_SPANS_HZ],
715
+ supportedEdgeSlots: this.getScopeSupportedEdgeSlots(),
716
+ supportsFixedEdges: this.activeProfile.scopeRanges.length > 0,
717
+ supportsEdgeSlotSelection: this.activeProfile.scopeEdgeSlots.length > 0,
718
+ };
719
+ }
720
+ async configureSpectrumDisplay(config = {}) {
721
+ const receiver = config.receiver ?? 0;
722
+ if (config.mode !== undefined) {
723
+ await this.setScopeMode(config.mode, { receiver });
724
+ }
725
+ if (config.edgeSlot !== undefined) {
726
+ await this.setScopeEdge(config.edgeSlot, { receiver });
727
+ }
728
+ if (config.spanHz !== undefined && (!config.mode || config.mode === 'center' || config.mode === 'scroll-center')) {
729
+ await this.setScopeSpan(config.spanHz, { receiver });
730
+ }
731
+ if (config.edgeLowHz !== undefined && config.edgeHighHz !== undefined && (!config.mode || config.mode === 'fixed' || config.mode === 'scroll-fixed')) {
732
+ await this.setScopeFixedEdge({
733
+ rangeId: config.rangeId,
734
+ edgeSlot: config.edgeSlot,
735
+ lowHz: config.edgeLowHz,
736
+ highHz: config.edgeHighHz,
737
+ });
738
+ }
739
+ return this.getSpectrumDisplayState({ receiver, timeout: 3000 });
740
+ }
741
+ async getSpectrumMode(options) {
742
+ return (await this.readScopeMode(options))?.modeName ?? null;
743
+ }
744
+ async setSpectrumMode(mode, options) {
745
+ await this.setScopeMode(mode, options);
746
+ }
747
+ async getSpectrumSpan(options) {
748
+ return (await this.readScopeSpan(options))?.spanHz ?? null;
749
+ }
750
+ async setSpectrumSpan(spanHz, options) {
751
+ await this.setScopeSpan(spanHz, options);
752
+ }
753
+ async getSpectrumEdgeSlot(options) {
754
+ return (await this.readScopeEdge(options))?.edgeSlot ?? null;
755
+ }
756
+ async setSpectrumEdgeSlot(edgeSlot, options) {
757
+ await this.setScopeEdge(edgeSlot, options);
758
+ }
759
+ async getSpectrumFixedEdges(options) {
760
+ const rangeId = options?.rangeId ?? await this.resolveScopeFrequencyRangeId();
761
+ const edgeSlot = options?.edgeSlot ?? await this.getSpectrumEdgeSlot(options) ?? 1;
762
+ const info = await this.readScopeFixedEdge(rangeId, edgeSlot, options);
763
+ if (!info) {
764
+ return null;
765
+ }
766
+ return info;
767
+ }
768
+ async setSpectrumFixedEdges(options) {
769
+ return this.setScopeFixedEdge(options);
770
+ }
553
771
  async waitForScopeFrame(options) {
554
772
  const timeoutMs = options?.timeout ?? 3000;
555
773
  return this.scope.waitForScopeFrame(timeoutMs);
@@ -589,7 +807,11 @@ class IcomControl {
589
807
  async setFrequency(hz) {
590
808
  const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
591
809
  const rigAddr = this.civ.civAddress & 0xff;
592
- this.sendCiv(IcomRigCommands_1.IcomRigCommands.setFrequency(ctrAddr, rigAddr, hz));
810
+ const bcdBytes = this.activeProfile.frequencyBcdBytes(hz);
811
+ const frame = this.activeProfile.supportsX25X26
812
+ ? IcomRigCommands_1.IcomRigCommands.setSelectedFrequency(ctrAddr, rigAddr, hz, bcdBytes, 0)
813
+ : IcomRigCommands_1.IcomRigCommands.setFrequency(ctrAddr, rigAddr, hz, bcdBytes);
814
+ this.sendCiv(frame);
593
815
  }
594
816
  /**
595
817
  * Set operating mode
@@ -605,11 +827,16 @@ class IcomControl {
605
827
  const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
606
828
  const rigAddr = this.civ.civAddress & 0xff;
607
829
  const modeCode = typeof mode === 'string' ? (0, IcomConstants_1.getModeCode)(mode) : mode;
608
- if (options?.dataMode) {
609
- this.sendCiv(IcomRigCommands_1.IcomRigCommands.setOperationDataMode(ctrAddr, rigAddr, modeCode));
830
+ const filter = options?.filter ?? this.lastFilter ?? this.activeProfile.defaultFilter;
831
+ if (this.activeProfile.supportsX25X26 && this.activeProfile.modeWithFilter) {
832
+ this.sendCiv(IcomRigCommands_1.IcomRigCommands.setSelectedMode(ctrAddr, rigAddr, modeCode, !!options?.dataMode, filter, 0));
833
+ return;
834
+ }
835
+ if (options?.dataMode && this.activeProfile.dataModeSupported) {
836
+ this.sendCiv(IcomRigCommands_1.IcomRigCommands.setOperationDataMode(ctrAddr, rigAddr, modeCode, filter));
610
837
  }
611
838
  else {
612
- this.sendCiv(IcomRigCommands_1.IcomRigCommands.setMode(ctrAddr, rigAddr, modeCode));
839
+ this.sendCiv(IcomRigCommands_1.IcomRigCommands.setMode(ctrAddr, rigAddr, modeCode, filter));
613
840
  }
614
841
  }
615
842
  /**
@@ -624,12 +851,16 @@ class IcomControl {
624
851
  const timeoutMs = options?.timeout ?? 3000;
625
852
  const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
626
853
  const rigAddr = this.civ.civAddress & 0xff;
627
- const req = IcomRigCommands_1.IcomRigCommands.readOperatingFrequency(ctrAddr, rigAddr);
628
- const resp = await this.waitForCivFrame((frame) => IcomControl.isReplyOf(frame, 0x03, ctrAddr, rigAddr), timeoutMs, () => this.sendCiv(req));
854
+ const useX25 = this.activeProfile.supportsX25X26;
855
+ const req = useX25
856
+ ? IcomRigCommands_1.IcomRigCommands.readSelectedFrequency(ctrAddr, rigAddr, 0)
857
+ : IcomRigCommands_1.IcomRigCommands.readOperatingFrequency(ctrAddr, rigAddr);
858
+ const resp = await this.waitForCivFrame((frame) => useX25
859
+ ? IcomControl.matchCommandFrame(frame, IcomCivSpec_1.CIV.C_SEND_SEL_FREQ, [0x00], ctrAddr, rigAddr)
860
+ : IcomControl.matchCommandFrame(frame, IcomCivSpec_1.CIV.C_RD_FREQ, [], ctrAddr, rigAddr), timeoutMs, () => this.sendCiv(req));
629
861
  if (!resp)
630
862
  return null;
631
- const freq = IcomControl.parseIcomFreqFromReply(resp);
632
- return freq;
863
+ return IcomControl.parseFrequencyReply(resp, useX25 ? 1 : 0);
633
864
  }
634
865
  /**
635
866
  * Read current operating mode and filter
@@ -639,78 +870,56 @@ class IcomControl {
639
870
  const timeoutMs = options?.timeout ?? 3000;
640
871
  const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
641
872
  const rigAddr = this.civ.civAddress & 0xff;
642
- const req = IcomRigCommands_1.IcomRigCommands.readOperatingMode(ctrAddr, rigAddr);
643
- const resp = await this.waitForCivFrame((frame) => IcomControl.matchCommandFrame(frame, 0x04, [], ctrAddr, rigAddr), timeoutMs, () => this.sendCiv(req));
873
+ const useX26 = this.activeProfile.supportsX25X26 && this.activeProfile.modeWithFilter;
874
+ const req = useX26 ? IcomRigCommands_1.IcomRigCommands.readSelectedMode(ctrAddr, rigAddr, 0) : IcomRigCommands_1.IcomRigCommands.readOperatingMode(ctrAddr, rigAddr);
875
+ const resp = await this.waitForCivFrame((frame) => useX26
876
+ ? IcomControl.matchCommandFrame(frame, IcomCivSpec_1.CIV.C_SEND_SEL_MODE, [0x00], ctrAddr, rigAddr)
877
+ : IcomControl.matchCommandFrame(frame, IcomCivSpec_1.CIV.C_RD_MODE, [], ctrAddr, rigAddr), timeoutMs, () => this.sendCiv(req));
644
878
  if (!resp)
645
879
  return null;
646
- // Expect FE FE [ctr] [rig] 0x04 [mode] [filter] FD (some rigs may omit filter)
647
- const mode = resp.length > 5 ? resp[5] : undefined;
648
- const filter = resp.length > 6 ? resp[6] : undefined;
880
+ const mode = useX26 ? resp[6] : resp[5];
881
+ const dataMode = useX26 ? resp[7] !== 0x00 : undefined;
882
+ const filter = useX26 ? resp[8] : (resp.length > 6 ? resp[6] : undefined);
649
883
  if (mode === undefined)
650
884
  return null;
651
- // Map names using constants
885
+ if (filter === 1 || filter === 2 || filter === 3)
886
+ this.lastFilter = filter;
652
887
  const { getModeString, getFilterString } = await Promise.resolve().then(() => __importStar(require('./IcomConstants')));
653
888
  const modeName = getModeString(mode);
654
889
  const filterName = getFilterString(filter);
655
- return { mode, filter, modeName, filterName };
890
+ return { mode, filter, modeName, filterName, dataMode };
656
891
  }
657
892
  /**
658
893
  * Read current transmit frequency (when TX)
659
894
  */
660
895
  async readTransmitFrequency(options) {
896
+ if (!this.activeProfile.supportsX1C03TxFreq)
897
+ return null;
661
898
  const timeoutMs = options?.timeout ?? 3000;
662
899
  const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
663
900
  const rigAddr = this.civ.civAddress & 0xff;
664
901
  const req = IcomRigCommands_1.IcomRigCommands.readTransmitFrequency(ctrAddr, rigAddr);
665
- const resp = await this.waitForCivFrame((frame) => IcomControl.matchCommandFrame(frame, 0x1c, [0x03], ctrAddr, rigAddr), timeoutMs, () => this.sendCiv(req));
902
+ const resp = await this.waitForCivFrame((frame) => IcomControl.matchCommandFrame(frame, IcomCivSpec_1.CIV.C_CTL_PTT, [IcomCivSpec_1.CIV.S_RD_TX_FREQ], ctrAddr, rigAddr), timeoutMs, () => this.sendCiv(req));
666
903
  if (!resp)
667
904
  return null;
668
- // Parse BCD like readOperatingFrequency, but starting after [0x1c, 0x03]
669
- // Find 0x1c position and read next 2 bytes (0x03 + 5 BCD bytes)
670
- let idx = resp.indexOf(0x1c, 4);
671
- if (idx < 0 || idx + 6 >= resp.length)
672
- idx = 4;
673
- if (idx + 6 >= resp.length)
674
- return null;
675
- // After 0x1c 0x03, we expect 5 BCD bytes
676
- if (resp[idx + 1] !== 0x03)
677
- return null;
678
- const d0 = resp[idx + 2];
679
- const d1 = resp[idx + 3];
680
- const d2 = resp[idx + 4];
681
- const d3 = resp[idx + 5];
682
- const d4 = resp[idx + 6];
683
- const bcdToInt = (b) => ((b >> 4) & 0x0f) * 10 + (b & 0x0f);
684
- const v0 = bcdToInt(d0);
685
- const v1 = bcdToInt(d1);
686
- const v2 = bcdToInt(d2);
687
- const v3 = bcdToInt(d3);
688
- const v4 = bcdToInt(d4);
689
- const hz = v0 + v1 * 100 + v2 * 10000 + v3 * 1000000 + v4 * 100000000;
690
- return hz;
905
+ return IcomControl.parseFrequencyReply(resp, 1);
691
906
  }
692
- /**
693
- * Read transceiver state (TX/RX) via 0x1A 0x00 0x48
694
- * Note: Java comments mark this as not recommended; use with caution.
695
- */
696
- async readTransceiverState(options) {
907
+ async readPtt(options) {
697
908
  const timeoutMs = options?.timeout ?? 3000;
698
909
  const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
699
910
  const rigAddr = this.civ.civAddress & 0xff;
700
- const req = IcomRigCommands_1.IcomRigCommands.readTransceiverState(ctrAddr, rigAddr);
701
- const resp = await this.waitForCivFrame((frame) => IcomControl.matchCommandFrame(frame, 0x1a, [0x00, 0x48], ctrAddr, rigAddr), timeoutMs, () => this.sendCiv(req));
702
- if (!resp)
911
+ const req = IcomRigCommands_1.IcomRigCommands.readPTT(ctrAddr, rigAddr);
912
+ const resp = await this.waitForCivFrame((frame) => IcomControl.matchCommandFrame(frame, IcomCivSpec_1.CIV.C_CTL_PTT, [IcomCivSpec_1.CIV.S_PTT], ctrAddr, rigAddr), timeoutMs, () => this.sendCiv(req));
913
+ if (!resp || resp.length < 7)
914
+ return null;
915
+ return resp[6] !== 0x00;
916
+ }
917
+ /** Read transceiver state (TX/RX) using standard Hamlib-aligned PTT status. */
918
+ async readTransceiverState(options) {
919
+ const ptt = await this.readPtt(options);
920
+ if (ptt === null)
703
921
  return null;
704
- // Heuristic: take first data byte after subcmd2 as state
705
- const pos = 5 + 2; // after 0x1a [0x00,0x48]
706
- const state = resp.length > pos ? resp[pos] : undefined;
707
- if (state === undefined)
708
- return 'UNKNOWN';
709
- if (state === 0x01)
710
- return 'TX';
711
- if (state === 0x00)
712
- return 'RX';
713
- return 'UNKNOWN';
922
+ return ptt ? 'TX' : 'RX';
714
923
  }
715
924
  /**
716
925
  * Read band edge data (0x02). Format may vary by rig; returns raw data bytes after command.
@@ -747,8 +956,8 @@ class IcomControl {
747
956
  return null;
748
957
  return {
749
958
  raw,
750
- swr: raw / 100,
751
- alert: raw >= IcomConstants_1.METER_THRESHOLDS.SWR_ALERT
959
+ swr: (0, IcomProfiles_1.interpolateCalibration)(raw, this.activeProfile.calibrations.swr),
960
+ alert: (0, IcomProfiles_1.interpolateCalibration)(raw, this.activeProfile.calibrations.swr) >= 3.0
752
961
  };
753
962
  }
754
963
  /**
@@ -772,8 +981,8 @@ class IcomControl {
772
981
  return null;
773
982
  return {
774
983
  raw,
775
- percent: (raw / IcomConstants_1.METER_THRESHOLDS.ALC_MAX) * 100,
776
- alert: raw > IcomConstants_1.METER_THRESHOLDS.ALC_ALERT_MAX
984
+ percent: (0, IcomProfiles_1.interpolateCalibration)(raw, this.activeProfile.calibrations.alc),
985
+ alert: (0, IcomProfiles_1.interpolateCalibration)(raw, this.activeProfile.calibrations.alc) > 100
777
986
  };
778
987
  }
779
988
  /**
@@ -787,12 +996,15 @@ class IcomControl {
787
996
  * }
788
997
  */
789
998
  async getConnectorWLanLevel(options) {
999
+ const ext = this.activeProfile.vendorExtensions.connectorWlanLevel;
1000
+ if (!ext)
1001
+ return null;
790
1002
  const timeoutMs = options?.timeout ?? 3000;
791
1003
  const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
792
1004
  const rigAddr = this.civ.civAddress & 0xff;
793
- const req = IcomRigCommands_1.IcomRigCommands.getConnectorWLanLevel(ctrAddr, rigAddr);
794
- const resp = await this.waitForCivFrame((frame) => IcomControl.matchCommandFrame(frame, 0x1a, [0x05, 0x01, 0x17], ctrAddr, rigAddr), timeoutMs, () => this.sendCiv(req));
795
- const raw = IcomControl.extractMeterData(resp);
1005
+ const req = IcomRigCommands_1.IcomRigCommands.getConnectorWLanLevel(ctrAddr, rigAddr, ext.subext);
1006
+ const resp = await this.waitForCivFrame((frame) => IcomControl.matchCommandFrame(frame, ext.command, [ext.subcmd, ...ext.subext], ctrAddr, rigAddr), timeoutMs, () => this.sendCiv(req));
1007
+ const raw = IcomControl.extractTrailingBcd(resp, ext.dataBytes);
796
1008
  if (raw === null)
797
1009
  return null;
798
1010
  return {
@@ -829,18 +1041,44 @@ class IcomControl {
829
1041
  if (data.length === 0)
830
1042
  return null;
831
1043
  const raw = data[data.length - 1] & 0xff; // use low byte as 0-255 level
832
- // Convert raw value to S-meter reading with physical units
833
- // Uses IC-705 calibration by default (can be extended to support other models)
834
- return (0, smeter_1.rawToSMeter)(raw, 'IC-705');
1044
+ return (0, smeter_1.rawToSMeter)(raw, this.activeProfile.calibrations.sMeterModel);
835
1045
  }
836
1046
  /**
837
1047
  * Set WLAN connector audio level
838
1048
  * @param level - Audio level (0-255)
839
1049
  */
840
1050
  async setConnectorWLanLevel(level) {
1051
+ const ext = this.activeProfile.vendorExtensions.connectorWlanLevel;
1052
+ if (!ext) {
1053
+ throw new errors_1.UnsupportedCommandError({ modelId: this.getProfileModelId(), commandName: 'setConnectorWLanLevel', civCommand: '0x1a/0x05', reason: 'No vendor WLAN level extension for active profile' });
1054
+ }
1055
+ const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
1056
+ const rigAddr = this.civ.civAddress & 0xff;
1057
+ this.sendCiv(IcomRigCommands_1.IcomRigCommands.setConnectorWLanLevel(ctrAddr, rigAddr, level, ext.subext));
1058
+ }
1059
+ async getUsbAfLevel(options) {
1060
+ const ext = this.activeProfile.extParams.usbAfLevel;
1061
+ if (!ext)
1062
+ return null;
1063
+ const timeoutMs = options?.timeout ?? 3000;
1064
+ const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
1065
+ const rigAddr = this.civ.civAddress & 0xff;
1066
+ const req = IcomRigCommands_1.IcomRigCommands.getUsbAfLevel(ctrAddr, rigAddr, ext.subext);
1067
+ const resp = await this.waitForCivFrame((frame) => IcomControl.matchCommandFrame(frame, ext.command, [ext.subcmd, ...ext.subext], ctrAddr, rigAddr), timeoutMs, () => this.sendCiv(req));
1068
+ const raw = IcomControl.extractTrailingBcd(resp, ext.dataBytes);
1069
+ if (raw === null)
1070
+ return null;
1071
+ return { raw, percent: (raw / 255) * 100 };
1072
+ }
1073
+ async setUsbAfLevel(level) {
1074
+ const ext = this.activeProfile.extParams.usbAfLevel;
1075
+ if (!ext) {
1076
+ throw new errors_1.UnsupportedCommandError({ modelId: this.getProfileModelId(), commandName: 'setUsbAfLevel', civCommand: '0x1a/0x05', reason: 'No USB AF level extension for active profile' });
1077
+ }
841
1078
  const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
842
1079
  const rigAddr = this.civ.civAddress & 0xff;
843
- this.sendCiv(IcomRigCommands_1.IcomRigCommands.setConnectorWLanLevel(ctrAddr, rigAddr, level));
1080
+ const raw = Math.max(0, Math.min(255, Math.round(level)));
1081
+ this.sendCiv(IcomRigCommands_1.IcomRigCommands.setUsbAfLevel(ctrAddr, rigAddr, raw, ext.subext));
844
1082
  }
845
1083
  /**
846
1084
  * Set connector data routing mode
@@ -850,10 +1088,14 @@ class IcomControl {
850
1088
  * await rig.setConnectorDataMode('WLAN');
851
1089
  */
852
1090
  async setConnectorDataMode(mode) {
1091
+ const ext = this.activeProfile.vendorExtensions.connectorDataMode;
1092
+ if (!ext) {
1093
+ throw new errors_1.UnsupportedCommandError({ modelId: this.getProfileModelId(), commandName: 'setConnectorDataMode', civCommand: '0x1a/0x05', reason: 'No vendor connector data-mode extension for active profile' });
1094
+ }
853
1095
  const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
854
1096
  const rigAddr = this.civ.civAddress & 0xff;
855
1097
  const modeCode = typeof mode === 'string' ? (0, IcomConstants_1.getConnectorModeCode)(mode) : mode;
856
- this.sendCiv(IcomRigCommands_1.IcomRigCommands.setConnectorDataMode(ctrAddr, rigAddr, modeCode));
1098
+ this.sendCiv(IcomRigCommands_1.IcomRigCommands.setConnectorDataMode(ctrAddr, rigAddr, modeCode, ext.subext));
857
1099
  }
858
1100
  /**
859
1101
  * ==============================
@@ -861,7 +1103,7 @@ class IcomControl {
861
1103
  * ==============================
862
1104
  */
863
1105
  /**
864
- * Read antenna tuner status (CI-V 0x1A/0x00)
1106
+ * Read antenna tuner status (CI-V 0x1C/0x01)
865
1107
  * 00=OFF, 01=ON, 02=TUNING
866
1108
  */
867
1109
  async readTunerStatus(options) {
@@ -869,10 +1111,10 @@ class IcomControl {
869
1111
  const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
870
1112
  const rigAddr = this.civ.civAddress & 0xff;
871
1113
  const req = IcomRigCommands_1.IcomRigCommands.getTunerStatus(ctrAddr, rigAddr);
872
- const resp = await this.waitForCivFrame((frame) => IcomControl.matchCommandFrame(frame, 0x1a, [0x00], ctrAddr, rigAddr), timeoutMs, () => this.sendCiv(req));
1114
+ const resp = await this.waitForCivFrame((frame) => IcomControl.matchCommandFrame(frame, IcomCivSpec_1.CIV.C_CTL_PTT, [IcomCivSpec_1.CIV.S_ANT_TUN], ctrAddr, rigAddr), timeoutMs, () => this.sendCiv(req));
873
1115
  if (!resp)
874
1116
  return null;
875
- // Expect FE FE [ctr] [rig] 0x1A 0x00 [status] FD
1117
+ // Expect FE FE [ctr] [rig] 0x1C 0x01 [status] FD
876
1118
  const raw = resp.length > 6 ? (resp[6] & 0xff) : undefined;
877
1119
  if (raw === undefined)
878
1120
  return null;
@@ -880,7 +1122,7 @@ class IcomControl {
880
1122
  return { raw, state };
881
1123
  }
882
1124
  /**
883
- * Enable or disable internal antenna tuner (CI-V 0x1A/0x01)
1125
+ * Enable or disable internal antenna tuner (CI-V 0x1C/0x01)
884
1126
  * @param enabled true to enable, false to disable
885
1127
  */
886
1128
  async setTunerEnabled(enabled) {
@@ -889,7 +1131,7 @@ class IcomControl {
889
1131
  this.sendCiv(IcomRigCommands_1.IcomRigCommands.setTunerEnabled(ctrAddr, rigAddr, enabled));
890
1132
  }
891
1133
  /**
892
- * Start a manual tuning cycle (same as [TUNE] key) (CI-V 0x1A/0x02/0x00)
1134
+ * Start a manual tuning cycle (same as [TUNE] key) (CI-V 0x1C/0x01/0x02)
893
1135
  */
894
1136
  async startManualTune() {
895
1137
  const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
@@ -934,14 +1176,25 @@ class IcomControl {
934
1176
  }
935
1177
  /** Get microphone gain. Returns 0.0–1.0, or null on timeout. */
936
1178
  async getMicGain(options) {
937
- const v = await this.read0x14Level(0x0f, options);
1179
+ const v = await this.read0x14Level(IcomCivSpec_1.CIV.S_LVL_MICGAIN, options);
938
1180
  if (v === null)
939
1181
  return null;
940
1182
  return { raw: Math.round(v * 255), normalized: v };
941
1183
  }
942
1184
  /** Set microphone gain. Value 0.0–1.0. */
943
1185
  setMicGain(value) {
944
- this.write0x14Level(0x0f, value);
1186
+ this.write0x14Level(IcomCivSpec_1.CIV.S_LVL_MICGAIN, value);
1187
+ }
1188
+ /** Get break-in delay. Returns 0.0–1.0, or null on timeout. */
1189
+ async getBreakInDelay(options) {
1190
+ const v = await this.read0x14Level(IcomCivSpec_1.CIV.S_LVL_BKINDL, options);
1191
+ if (v === null)
1192
+ return null;
1193
+ return { raw: Math.round(v * 255), normalized: v };
1194
+ }
1195
+ /** Set break-in delay. Value 0.0–1.0. */
1196
+ setBreakInDelay(value) {
1197
+ this.write0x14Level(IcomCivSpec_1.CIV.S_LVL_BKINDL, value);
945
1198
  }
946
1199
  /** Get noise blanker level. 0.0 = off, >0.0 = on with strength. */
947
1200
  async getNBLevel(options) {
@@ -956,14 +1209,14 @@ class IcomControl {
956
1209
  }
957
1210
  /** Get noise reduction level. 0.0 = off, >0.0 = on with strength. */
958
1211
  async getNRLevel(options) {
959
- const v = await this.read0x14Level(0x13, options);
1212
+ const v = await this.read0x14Level(IcomCivSpec_1.CIV.S_LVL_NR, options);
960
1213
  if (v === null)
961
1214
  return null;
962
1215
  return { raw: Math.round(v * 255), normalized: v };
963
1216
  }
964
1217
  /** Set noise reduction level. Value 0.0 (off) – 1.0. */
965
1218
  setNRLevel(value) {
966
- this.write0x14Level(0x13, value);
1219
+ this.write0x14Level(IcomCivSpec_1.CIV.S_LVL_NR, value);
967
1220
  }
968
1221
  /**
969
1222
  * Read squelch status (noise/signal gate state)
@@ -1056,9 +1309,12 @@ class IcomControl {
1056
1309
  const raw = IcomControl.extractMeterData(resp);
1057
1310
  if (raw === null)
1058
1311
  return null;
1312
+ const watts = (0, IcomProfiles_1.interpolateCalibration)(raw, this.activeProfile.calibrations.rfPowerWatts);
1313
+ const maxWatts = this.activeProfile.calibrations.rfPowerWatts[this.activeProfile.calibrations.rfPowerWatts.length - 1]?.value ?? 100;
1059
1314
  return {
1060
1315
  raw,
1061
- percent: (0, IcomConstants_1.rawToPowerPercent)(raw)
1316
+ percent: maxWatts > 0 ? Math.min(100, (watts / maxWatts) * 100) : 0,
1317
+ watts
1062
1318
  };
1063
1319
  }
1064
1320
  /**
@@ -1080,9 +1336,11 @@ class IcomControl {
1080
1336
  const raw = IcomControl.extractMeterData(resp);
1081
1337
  if (raw === null)
1082
1338
  return null;
1339
+ const db = (0, IcomProfiles_1.interpolateCalibration)(raw, this.activeProfile.calibrations.compDb);
1083
1340
  return {
1084
1341
  raw,
1085
- percent: (raw / 255) * 100
1342
+ percent: (db / 30) * 100,
1343
+ db
1086
1344
  };
1087
1345
  }
1088
1346
  /**
@@ -1106,7 +1364,7 @@ class IcomControl {
1106
1364
  return null;
1107
1365
  return {
1108
1366
  raw,
1109
- volts: (0, IcomConstants_1.rawToVoltage)(raw)
1367
+ volts: (0, IcomProfiles_1.interpolateCalibration)(raw, this.activeProfile.calibrations.voltage)
1110
1368
  };
1111
1369
  }
1112
1370
  /**
@@ -1130,7 +1388,7 @@ class IcomControl {
1130
1388
  return null;
1131
1389
  return {
1132
1390
  raw,
1133
- amps: (0, IcomConstants_1.rawToCurrent)(raw)
1391
+ amps: (0, IcomProfiles_1.interpolateCalibration)(raw, this.activeProfile.calibrations.current)
1134
1392
  };
1135
1393
  }
1136
1394
  static isReplyOf(frame, cmd, ctrAddr, rigAddr) {
@@ -1146,9 +1404,17 @@ class IcomControl {
1146
1404
  static extractMeterData(frame) {
1147
1405
  if (!frame || frame.length < 9)
1148
1406
  return null;
1149
- // Extract 2-byte BCD data at position 6-7 of the CI-V frame (FE FE [ctr] [rig] 0x15 [sub] [b0] [b1] FD)
1150
- const bcdData = frame.subarray(6, 8);
1151
- return (0, bcd_1.parseTwoByteBcd)(bcdData);
1407
+ // FE FE [ctr] [rig] 0x15 [sub] [bcd_hi] [bcd_lo] FD
1408
+ return (0, IcomCivFrame_1.decodeBcdBE)(frame.subarray(6, 8));
1409
+ }
1410
+ static extractTrailingBcd(frame, byteLength) {
1411
+ if (!frame || frame.length < 6 + byteLength)
1412
+ return null;
1413
+ const end = frame.length - 1;
1414
+ const start = end - byteLength;
1415
+ if (start < 5)
1416
+ return null;
1417
+ return (0, IcomCivFrame_1.decodeBcdBE)(frame.subarray(start, end));
1152
1418
  }
1153
1419
  static matchCommand(frame, cmd, tail) {
1154
1420
  // FE FE ?? ?? cmd ... tail... FD
@@ -1218,34 +1484,23 @@ class IcomControl {
1218
1484
  }, timeoutMs);
1219
1485
  });
1220
1486
  }
1221
- // Parse CI-V reply for command 0x03 (read operating frequency)
1222
- static parseIcomFreqFromReply(frame) {
1223
- // Expect: FE FE [ctr] [rig] 0x03 [bcd0..bcd4] FD
1487
+ static parseFrequencyReply(frame, payloadOffsetAfterCommand, byteLength) {
1224
1488
  if (!(frame && frame.length >= 7))
1225
1489
  return null;
1226
- if (frame[0] !== 0xfe || frame[1] !== 0xfe)
1490
+ if (frame[0] !== 0xfe || frame[1] !== 0xfe || frame[frame.length - 1] !== 0xfd)
1227
1491
  return null;
1228
- if (frame[4] !== 0x03)
1492
+ const start = 5 + payloadOffsetAfterCommand;
1493
+ const maxBytes = frame.length - 1 - start;
1494
+ const len = byteLength ?? (maxBytes >= 6 ? 6 : 5);
1495
+ if (maxBytes < len || len <= 0)
1229
1496
  return null;
1230
- // Some radios may include extra bytes; find 0x03 and read next 5 bytes
1231
- let idx = frame.indexOf(0x03, 5);
1232
- if (idx < 0 || idx + 5 >= frame.length)
1233
- idx = 4; // fallback to standard position
1234
- if (idx + 5 >= frame.length)
1497
+ return (0, IcomCivFrame_1.decodeFrequencyBcdLE)(frame.subarray(start, start + len));
1498
+ }
1499
+ // Parse standard CI-V 0x03 read-frequency replies.
1500
+ static parseIcomFreqFromReply(frame) {
1501
+ if (!IcomControl.matchCommandFrame(frame, IcomCivSpec_1.CIV.C_RD_FREQ, []))
1235
1502
  return null;
1236
- const d0 = frame[idx + 1];
1237
- const d1 = frame[idx + 2];
1238
- const d2 = frame[idx + 3];
1239
- const d3 = frame[idx + 4];
1240
- const d4 = frame[idx + 5];
1241
- const bcdToInt = (b) => ((b >> 4) & 0x0f) * 10 + (b & 0x0f);
1242
- const v0 = bcdToInt(d0);
1243
- const v1 = bcdToInt(d1);
1244
- const v2 = bcdToInt(d2);
1245
- const v3 = bcdToInt(d3);
1246
- const v4 = bcdToInt(d4);
1247
- const hz = v0 + v1 * 100 + v2 * 10000 + v3 * 1000000 + v4 * 100000000;
1248
- return hz;
1503
+ return IcomControl.parseFrequencyReply(frame, 0);
1249
1504
  }
1250
1505
  sendAudioFloat32(samples, addLeadingBuffer = false) {
1251
1506
  this.audio.enqueueFloat32(samples, addLeadingBuffer);
@@ -1380,11 +1635,15 @@ class IcomControl {
1380
1635
  audioName: IcomPackets_1.RadioCapPacket.getAudioName(cap),
1381
1636
  supportTX: IcomPackets_1.RadioCapPacket.getSupportTX(cap)
1382
1637
  };
1383
- if (info.civAddress != null)
1638
+ if (info.civAddress != null) {
1384
1639
  this.civ.civAddress = info.civAddress;
1640
+ this.resolveActiveProfile({ civAddress: info.civAddress });
1641
+ info.modelId = this.activeProfile.modelId;
1642
+ info.profileName = this.activeProfile.profileName;
1643
+ }
1385
1644
  if (info.supportTX != null)
1386
1645
  this.civ.supportTX = info.supportTX;
1387
- (0, debug_1.dbgV)('CAP <= civAddr=', info.civAddress, 'audioName=', info.audioName, 'supportTX=', info.supportTX);
1646
+ (0, debug_1.dbgV)('CAP <= civAddr=', info.civAddress, 'audioName=', info.audioName, 'supportTX=', info.supportTX, 'profile=', info.modelId);
1388
1647
  this.ev.emit('capabilities', info);
1389
1648
  }
1390
1649
  break;
@@ -1396,7 +1655,8 @@ class IcomControl {
1396
1655
  const busy = IcomPackets_1.ConnInfoPacket.getBusy(buf);
1397
1656
  this.macAddress = IcomPackets_1.ConnInfoPacket.getMacAddress(buf);
1398
1657
  this.rigName = IcomPackets_1.ConnInfoPacket.getRigName(buf);
1399
- (0, debug_1.dbg)('CONNINFO <= busy=', busy, 'rigName=', this.rigName);
1658
+ this.resolveActiveProfile({ rigName: this.rigName });
1659
+ (0, debug_1.dbg)('CONNINFO <= busy=', busy, 'rigName=', this.rigName, 'profile=', this.activeProfile.modelId);
1400
1660
  if (busy) {
1401
1661
  (0, debug_1.dbg)('CONNINFO busy=true detected - likely reconnecting while rig still has old session');
1402
1662
  (0, debug_1.dbg)('Sending ConnInfo reply anyway to allow STATUS packet delivery');