icom-wlan-node 0.5.1 → 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,27 +49,10 @@ 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 DEFAULT_SCOPE_EDGE_SLOTS = [1, 2, 3, 4];
52
+ const IcomCivFrame_1 = require("./IcomCivFrame");
53
+ const IcomCivSpec_1 = require("./IcomCivSpec");
54
+ const IcomProfiles_1 = require("./IcomProfiles");
53
55
  const DEFAULT_SCOPE_SPANS_HZ = [25000000, 10000000, 5000000, 2500000, 1000000, 500000, 250000, 100000, 50000, 25000, 10000, 5000, 2500];
54
- const DEFAULT_SCOPE_FREQUENCY_RANGES = [
55
- { rangeId: 1, lowHz: 30000, highHz: 1600000 },
56
- { rangeId: 2, lowHz: 1600000, highHz: 2000000 },
57
- { rangeId: 3, lowHz: 2000000, highHz: 6000000 },
58
- { rangeId: 4, lowHz: 6000000, highHz: 8000000 },
59
- { rangeId: 5, lowHz: 8000000, highHz: 11000000 },
60
- { rangeId: 6, lowHz: 11000000, highHz: 15000000 },
61
- { rangeId: 7, lowHz: 15000000, highHz: 20000000 },
62
- { rangeId: 8, lowHz: 20000000, highHz: 22000000 },
63
- { rangeId: 9, lowHz: 22000000, highHz: 26000000 },
64
- { rangeId: 10, lowHz: 26000000, highHz: 30000000 },
65
- { rangeId: 11, lowHz: 30000000, highHz: 45000000 },
66
- { rangeId: 12, lowHz: 45000000, highHz: 60000000 },
67
- { rangeId: 13, lowHz: 60000000, highHz: 74800000 },
68
- { rangeId: 14, lowHz: 74800000, highHz: 108000000 },
69
- { rangeId: 15, lowHz: 108000000, highHz: 137000000 },
70
- { rangeId: 16, lowHz: 137000000, highHz: 200000000 },
71
- { rangeId: 17, lowHz: 400000000, highHz: 470000000 },
72
- ];
73
56
  function modeCodeToName(mode) {
74
57
  switch (mode) {
75
58
  case 0: return 'center';
@@ -92,6 +75,8 @@ class IcomControl {
92
75
  this.rigName = '';
93
76
  this.macAddress = Buffer.alloc(6);
94
77
  this.civAssembleBuf = Buffer.alloc(0); // CIV stream reassembler
78
+ this.activeProfile = (0, IcomProfiles_1.getProfileByModel)('generic-modern-icom');
79
+ this.lastFilter = 1;
95
80
  // Connection state machine (replaces old fragmented state flags)
96
81
  this.connectionSession = {
97
82
  phase: types_1.ConnectionPhase.IDLE,
@@ -109,7 +94,8 @@ class IcomControl {
109
94
  reconnectBaseDelay: 2000,
110
95
  reconnectMaxDelay: 30000
111
96
  };
112
- this.options = options;
97
+ this.options = { ...options, model: options.model ?? 'auto' };
98
+ this.activeProfile = (0, IcomProfiles_1.resolveIcomProfile)({ requestedModel: this.options.model });
113
99
  // Setup control session
114
100
  this.sess = new Session_1.Session({ ip: options.control.ip, port: options.control.port }, {
115
101
  onData: (data) => this.onData(data),
@@ -137,6 +123,25 @@ class IcomControl {
137
123
  this.scope.on('scopeFrame', (frame) => this.ev.emit('scopeFrame', frame));
138
124
  }
139
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
+ }
140
145
  // ============================================================================
141
146
  // State Machine Management
142
147
  // ============================================================================
@@ -575,10 +580,10 @@ class IcomControl {
575
580
  if (!resp || resp.length < 13) {
576
581
  return null;
577
582
  }
578
- const spanHz = (0, IcomScopeParser_1.parseIcomBcdFreqLE)(resp.subarray(7, 12));
583
+ const encodedSpanHz = (0, IcomScopeParser_1.parseIcomBcdFreqLE)(resp.subarray(7, 12));
579
584
  return {
580
585
  receiver,
581
- spanHz,
586
+ spanHz: encodedSpanHz * 2,
582
587
  };
583
588
  }
584
589
  async setScopeSpan(spanHz, options) {
@@ -628,7 +633,9 @@ class IcomControl {
628
633
  }
629
634
  async setScopeEdge(edgeSlot, options) {
630
635
  const receiver = options?.receiver ?? 0;
631
- const safeEdgeSlot = Math.max(1, Math.min(4, Math.trunc(edgeSlot)));
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)));
632
639
  const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
633
640
  const rigAddr = this.civ.civAddress & 0xff;
634
641
  this.sendCiv(IcomScopeCommands_1.IcomScopeCommands.setScopeEdge(ctrAddr, rigAddr, safeEdgeSlot, receiver));
@@ -670,14 +677,14 @@ class IcomControl {
670
677
  if (!targetFrequency) {
671
678
  throw new Error('Unable to resolve scope frequency range without operating frequency');
672
679
  }
673
- const matched = DEFAULT_SCOPE_FREQUENCY_RANGES.find((range) => targetFrequency >= range.lowHz && targetFrequency < range.highHz);
680
+ const matched = this.activeProfile.scopeRanges.find((range) => targetFrequency >= range.lowHz && targetFrequency < range.highHz);
674
681
  if (!matched) {
675
682
  throw new Error(`No scope frequency range matches ${targetFrequency} Hz`);
676
683
  }
677
684
  return matched.rangeId;
678
685
  }
679
686
  getScopeSupportedEdgeSlots() {
680
- return [...DEFAULT_SCOPE_EDGE_SLOTS];
687
+ return [...this.activeProfile.scopeEdgeSlots];
681
688
  }
682
689
  async getSpectrumDisplayState(options) {
683
690
  const receiver = options?.receiver ?? 0;
@@ -706,8 +713,8 @@ class IcomControl {
706
713
  supportedModes: ['center', 'fixed', 'scroll-center', 'scroll-fixed'],
707
714
  supportedSpans: [...DEFAULT_SCOPE_SPANS_HZ],
708
715
  supportedEdgeSlots: this.getScopeSupportedEdgeSlots(),
709
- supportsFixedEdges: true,
710
- supportsEdgeSlotSelection: true,
716
+ supportsFixedEdges: this.activeProfile.scopeRanges.length > 0,
717
+ supportsEdgeSlotSelection: this.activeProfile.scopeEdgeSlots.length > 0,
711
718
  };
712
719
  }
713
720
  async configureSpectrumDisplay(config = {}) {
@@ -800,7 +807,11 @@ class IcomControl {
800
807
  async setFrequency(hz) {
801
808
  const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
802
809
  const rigAddr = this.civ.civAddress & 0xff;
803
- 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);
804
815
  }
805
816
  /**
806
817
  * Set operating mode
@@ -816,11 +827,16 @@ class IcomControl {
816
827
  const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
817
828
  const rigAddr = this.civ.civAddress & 0xff;
818
829
  const modeCode = typeof mode === 'string' ? (0, IcomConstants_1.getModeCode)(mode) : mode;
819
- if (options?.dataMode) {
820
- 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));
821
837
  }
822
838
  else {
823
- this.sendCiv(IcomRigCommands_1.IcomRigCommands.setMode(ctrAddr, rigAddr, modeCode));
839
+ this.sendCiv(IcomRigCommands_1.IcomRigCommands.setMode(ctrAddr, rigAddr, modeCode, filter));
824
840
  }
825
841
  }
826
842
  /**
@@ -835,12 +851,16 @@ class IcomControl {
835
851
  const timeoutMs = options?.timeout ?? 3000;
836
852
  const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
837
853
  const rigAddr = this.civ.civAddress & 0xff;
838
- const req = IcomRigCommands_1.IcomRigCommands.readOperatingFrequency(ctrAddr, rigAddr);
839
- 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));
840
861
  if (!resp)
841
862
  return null;
842
- const freq = IcomControl.parseIcomFreqFromReply(resp);
843
- return freq;
863
+ return IcomControl.parseFrequencyReply(resp, useX25 ? 1 : 0);
844
864
  }
845
865
  /**
846
866
  * Read current operating mode and filter
@@ -850,78 +870,56 @@ class IcomControl {
850
870
  const timeoutMs = options?.timeout ?? 3000;
851
871
  const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
852
872
  const rigAddr = this.civ.civAddress & 0xff;
853
- const req = IcomRigCommands_1.IcomRigCommands.readOperatingMode(ctrAddr, rigAddr);
854
- 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));
855
878
  if (!resp)
856
879
  return null;
857
- // Expect FE FE [ctr] [rig] 0x04 [mode] [filter] FD (some rigs may omit filter)
858
- const mode = resp.length > 5 ? resp[5] : undefined;
859
- 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);
860
883
  if (mode === undefined)
861
884
  return null;
862
- // Map names using constants
885
+ if (filter === 1 || filter === 2 || filter === 3)
886
+ this.lastFilter = filter;
863
887
  const { getModeString, getFilterString } = await Promise.resolve().then(() => __importStar(require('./IcomConstants')));
864
888
  const modeName = getModeString(mode);
865
889
  const filterName = getFilterString(filter);
866
- return { mode, filter, modeName, filterName };
890
+ return { mode, filter, modeName, filterName, dataMode };
867
891
  }
868
892
  /**
869
893
  * Read current transmit frequency (when TX)
870
894
  */
871
895
  async readTransmitFrequency(options) {
896
+ if (!this.activeProfile.supportsX1C03TxFreq)
897
+ return null;
872
898
  const timeoutMs = options?.timeout ?? 3000;
873
899
  const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
874
900
  const rigAddr = this.civ.civAddress & 0xff;
875
901
  const req = IcomRigCommands_1.IcomRigCommands.readTransmitFrequency(ctrAddr, rigAddr);
876
- 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));
877
903
  if (!resp)
878
904
  return null;
879
- // Parse BCD like readOperatingFrequency, but starting after [0x1c, 0x03]
880
- // Find 0x1c position and read next 2 bytes (0x03 + 5 BCD bytes)
881
- let idx = resp.indexOf(0x1c, 4);
882
- if (idx < 0 || idx + 6 >= resp.length)
883
- idx = 4;
884
- if (idx + 6 >= resp.length)
885
- return null;
886
- // After 0x1c 0x03, we expect 5 BCD bytes
887
- if (resp[idx + 1] !== 0x03)
888
- return null;
889
- const d0 = resp[idx + 2];
890
- const d1 = resp[idx + 3];
891
- const d2 = resp[idx + 4];
892
- const d3 = resp[idx + 5];
893
- const d4 = resp[idx + 6];
894
- const bcdToInt = (b) => ((b >> 4) & 0x0f) * 10 + (b & 0x0f);
895
- const v0 = bcdToInt(d0);
896
- const v1 = bcdToInt(d1);
897
- const v2 = bcdToInt(d2);
898
- const v3 = bcdToInt(d3);
899
- const v4 = bcdToInt(d4);
900
- const hz = v0 + v1 * 100 + v2 * 10000 + v3 * 1000000 + v4 * 100000000;
901
- return hz;
905
+ return IcomControl.parseFrequencyReply(resp, 1);
902
906
  }
903
- /**
904
- * Read transceiver state (TX/RX) via 0x1A 0x00 0x48
905
- * Note: Java comments mark this as not recommended; use with caution.
906
- */
907
- async readTransceiverState(options) {
907
+ async readPtt(options) {
908
908
  const timeoutMs = options?.timeout ?? 3000;
909
909
  const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
910
910
  const rigAddr = this.civ.civAddress & 0xff;
911
- const req = IcomRigCommands_1.IcomRigCommands.readTransceiverState(ctrAddr, rigAddr);
912
- const resp = await this.waitForCivFrame((frame) => IcomControl.matchCommandFrame(frame, 0x1a, [0x00, 0x48], ctrAddr, rigAddr), timeoutMs, () => this.sendCiv(req));
913
- 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)
914
921
  return null;
915
- // Heuristic: take first data byte after subcmd2 as state
916
- const pos = 5 + 2; // after 0x1a [0x00,0x48]
917
- const state = resp.length > pos ? resp[pos] : undefined;
918
- if (state === undefined)
919
- return 'UNKNOWN';
920
- if (state === 0x01)
921
- return 'TX';
922
- if (state === 0x00)
923
- return 'RX';
924
- return 'UNKNOWN';
922
+ return ptt ? 'TX' : 'RX';
925
923
  }
926
924
  /**
927
925
  * Read band edge data (0x02). Format may vary by rig; returns raw data bytes after command.
@@ -958,8 +956,8 @@ class IcomControl {
958
956
  return null;
959
957
  return {
960
958
  raw,
961
- swr: raw / 100,
962
- 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
963
961
  };
964
962
  }
965
963
  /**
@@ -983,8 +981,8 @@ class IcomControl {
983
981
  return null;
984
982
  return {
985
983
  raw,
986
- percent: (raw / IcomConstants_1.METER_THRESHOLDS.ALC_MAX) * 100,
987
- 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
988
986
  };
989
987
  }
990
988
  /**
@@ -998,12 +996,15 @@ class IcomControl {
998
996
  * }
999
997
  */
1000
998
  async getConnectorWLanLevel(options) {
999
+ const ext = this.activeProfile.vendorExtensions.connectorWlanLevel;
1000
+ if (!ext)
1001
+ return null;
1001
1002
  const timeoutMs = options?.timeout ?? 3000;
1002
1003
  const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
1003
1004
  const rigAddr = this.civ.civAddress & 0xff;
1004
- const req = IcomRigCommands_1.IcomRigCommands.getConnectorWLanLevel(ctrAddr, rigAddr);
1005
- const resp = await this.waitForCivFrame((frame) => IcomControl.matchCommandFrame(frame, 0x1a, [0x05, 0x01, 0x17], ctrAddr, rigAddr), timeoutMs, () => this.sendCiv(req));
1006
- 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);
1007
1008
  if (raw === null)
1008
1009
  return null;
1009
1010
  return {
@@ -1040,18 +1041,44 @@ class IcomControl {
1040
1041
  if (data.length === 0)
1041
1042
  return null;
1042
1043
  const raw = data[data.length - 1] & 0xff; // use low byte as 0-255 level
1043
- // Convert raw value to S-meter reading with physical units
1044
- // Uses IC-705 calibration by default (can be extended to support other models)
1045
- return (0, smeter_1.rawToSMeter)(raw, 'IC-705');
1044
+ return (0, smeter_1.rawToSMeter)(raw, this.activeProfile.calibrations.sMeterModel);
1046
1045
  }
1047
1046
  /**
1048
1047
  * Set WLAN connector audio level
1049
1048
  * @param level - Audio level (0-255)
1050
1049
  */
1051
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
+ }
1052
1078
  const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
1053
1079
  const rigAddr = this.civ.civAddress & 0xff;
1054
- 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));
1055
1082
  }
1056
1083
  /**
1057
1084
  * Set connector data routing mode
@@ -1061,10 +1088,14 @@ class IcomControl {
1061
1088
  * await rig.setConnectorDataMode('WLAN');
1062
1089
  */
1063
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
+ }
1064
1095
  const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
1065
1096
  const rigAddr = this.civ.civAddress & 0xff;
1066
1097
  const modeCode = typeof mode === 'string' ? (0, IcomConstants_1.getConnectorModeCode)(mode) : mode;
1067
- this.sendCiv(IcomRigCommands_1.IcomRigCommands.setConnectorDataMode(ctrAddr, rigAddr, modeCode));
1098
+ this.sendCiv(IcomRigCommands_1.IcomRigCommands.setConnectorDataMode(ctrAddr, rigAddr, modeCode, ext.subext));
1068
1099
  }
1069
1100
  /**
1070
1101
  * ==============================
@@ -1072,7 +1103,7 @@ class IcomControl {
1072
1103
  * ==============================
1073
1104
  */
1074
1105
  /**
1075
- * Read antenna tuner status (CI-V 0x1A/0x00)
1106
+ * Read antenna tuner status (CI-V 0x1C/0x01)
1076
1107
  * 00=OFF, 01=ON, 02=TUNING
1077
1108
  */
1078
1109
  async readTunerStatus(options) {
@@ -1080,10 +1111,10 @@ class IcomControl {
1080
1111
  const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
1081
1112
  const rigAddr = this.civ.civAddress & 0xff;
1082
1113
  const req = IcomRigCommands_1.IcomRigCommands.getTunerStatus(ctrAddr, rigAddr);
1083
- 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));
1084
1115
  if (!resp)
1085
1116
  return null;
1086
- // Expect FE FE [ctr] [rig] 0x1A 0x00 [status] FD
1117
+ // Expect FE FE [ctr] [rig] 0x1C 0x01 [status] FD
1087
1118
  const raw = resp.length > 6 ? (resp[6] & 0xff) : undefined;
1088
1119
  if (raw === undefined)
1089
1120
  return null;
@@ -1091,7 +1122,7 @@ class IcomControl {
1091
1122
  return { raw, state };
1092
1123
  }
1093
1124
  /**
1094
- * Enable or disable internal antenna tuner (CI-V 0x1A/0x01)
1125
+ * Enable or disable internal antenna tuner (CI-V 0x1C/0x01)
1095
1126
  * @param enabled true to enable, false to disable
1096
1127
  */
1097
1128
  async setTunerEnabled(enabled) {
@@ -1100,7 +1131,7 @@ class IcomControl {
1100
1131
  this.sendCiv(IcomRigCommands_1.IcomRigCommands.setTunerEnabled(ctrAddr, rigAddr, enabled));
1101
1132
  }
1102
1133
  /**
1103
- * 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)
1104
1135
  */
1105
1136
  async startManualTune() {
1106
1137
  const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
@@ -1145,14 +1176,25 @@ class IcomControl {
1145
1176
  }
1146
1177
  /** Get microphone gain. Returns 0.0–1.0, or null on timeout. */
1147
1178
  async getMicGain(options) {
1148
- const v = await this.read0x14Level(0x0f, options);
1179
+ const v = await this.read0x14Level(IcomCivSpec_1.CIV.S_LVL_MICGAIN, options);
1149
1180
  if (v === null)
1150
1181
  return null;
1151
1182
  return { raw: Math.round(v * 255), normalized: v };
1152
1183
  }
1153
1184
  /** Set microphone gain. Value 0.0–1.0. */
1154
1185
  setMicGain(value) {
1155
- 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);
1156
1198
  }
1157
1199
  /** Get noise blanker level. 0.0 = off, >0.0 = on with strength. */
1158
1200
  async getNBLevel(options) {
@@ -1167,14 +1209,14 @@ class IcomControl {
1167
1209
  }
1168
1210
  /** Get noise reduction level. 0.0 = off, >0.0 = on with strength. */
1169
1211
  async getNRLevel(options) {
1170
- const v = await this.read0x14Level(0x13, options);
1212
+ const v = await this.read0x14Level(IcomCivSpec_1.CIV.S_LVL_NR, options);
1171
1213
  if (v === null)
1172
1214
  return null;
1173
1215
  return { raw: Math.round(v * 255), normalized: v };
1174
1216
  }
1175
1217
  /** Set noise reduction level. Value 0.0 (off) – 1.0. */
1176
1218
  setNRLevel(value) {
1177
- this.write0x14Level(0x13, value);
1219
+ this.write0x14Level(IcomCivSpec_1.CIV.S_LVL_NR, value);
1178
1220
  }
1179
1221
  /**
1180
1222
  * Read squelch status (noise/signal gate state)
@@ -1267,9 +1309,12 @@ class IcomControl {
1267
1309
  const raw = IcomControl.extractMeterData(resp);
1268
1310
  if (raw === null)
1269
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;
1270
1314
  return {
1271
1315
  raw,
1272
- percent: (0, IcomConstants_1.rawToPowerPercent)(raw)
1316
+ percent: maxWatts > 0 ? Math.min(100, (watts / maxWatts) * 100) : 0,
1317
+ watts
1273
1318
  };
1274
1319
  }
1275
1320
  /**
@@ -1291,9 +1336,11 @@ class IcomControl {
1291
1336
  const raw = IcomControl.extractMeterData(resp);
1292
1337
  if (raw === null)
1293
1338
  return null;
1339
+ const db = (0, IcomProfiles_1.interpolateCalibration)(raw, this.activeProfile.calibrations.compDb);
1294
1340
  return {
1295
1341
  raw,
1296
- percent: (raw / 255) * 100
1342
+ percent: (db / 30) * 100,
1343
+ db
1297
1344
  };
1298
1345
  }
1299
1346
  /**
@@ -1317,7 +1364,7 @@ class IcomControl {
1317
1364
  return null;
1318
1365
  return {
1319
1366
  raw,
1320
- volts: (0, IcomConstants_1.rawToVoltage)(raw)
1367
+ volts: (0, IcomProfiles_1.interpolateCalibration)(raw, this.activeProfile.calibrations.voltage)
1321
1368
  };
1322
1369
  }
1323
1370
  /**
@@ -1341,7 +1388,7 @@ class IcomControl {
1341
1388
  return null;
1342
1389
  return {
1343
1390
  raw,
1344
- amps: (0, IcomConstants_1.rawToCurrent)(raw)
1391
+ amps: (0, IcomProfiles_1.interpolateCalibration)(raw, this.activeProfile.calibrations.current)
1345
1392
  };
1346
1393
  }
1347
1394
  static isReplyOf(frame, cmd, ctrAddr, rigAddr) {
@@ -1357,9 +1404,17 @@ class IcomControl {
1357
1404
  static extractMeterData(frame) {
1358
1405
  if (!frame || frame.length < 9)
1359
1406
  return null;
1360
- // Extract 2-byte BCD data at position 6-7 of the CI-V frame (FE FE [ctr] [rig] 0x15 [sub] [b0] [b1] FD)
1361
- const bcdData = frame.subarray(6, 8);
1362
- 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));
1363
1418
  }
1364
1419
  static matchCommand(frame, cmd, tail) {
1365
1420
  // FE FE ?? ?? cmd ... tail... FD
@@ -1429,34 +1484,23 @@ class IcomControl {
1429
1484
  }, timeoutMs);
1430
1485
  });
1431
1486
  }
1432
- // Parse CI-V reply for command 0x03 (read operating frequency)
1433
- static parseIcomFreqFromReply(frame) {
1434
- // Expect: FE FE [ctr] [rig] 0x03 [bcd0..bcd4] FD
1487
+ static parseFrequencyReply(frame, payloadOffsetAfterCommand, byteLength) {
1435
1488
  if (!(frame && frame.length >= 7))
1436
1489
  return null;
1437
- if (frame[0] !== 0xfe || frame[1] !== 0xfe)
1490
+ if (frame[0] !== 0xfe || frame[1] !== 0xfe || frame[frame.length - 1] !== 0xfd)
1438
1491
  return null;
1439
- 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)
1440
1496
  return null;
1441
- // Some radios may include extra bytes; find 0x03 and read next 5 bytes
1442
- let idx = frame.indexOf(0x03, 5);
1443
- if (idx < 0 || idx + 5 >= frame.length)
1444
- idx = 4; // fallback to standard position
1445
- 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, []))
1446
1502
  return null;
1447
- const d0 = frame[idx + 1];
1448
- const d1 = frame[idx + 2];
1449
- const d2 = frame[idx + 3];
1450
- const d3 = frame[idx + 4];
1451
- const d4 = frame[idx + 5];
1452
- const bcdToInt = (b) => ((b >> 4) & 0x0f) * 10 + (b & 0x0f);
1453
- const v0 = bcdToInt(d0);
1454
- const v1 = bcdToInt(d1);
1455
- const v2 = bcdToInt(d2);
1456
- const v3 = bcdToInt(d3);
1457
- const v4 = bcdToInt(d4);
1458
- const hz = v0 + v1 * 100 + v2 * 10000 + v3 * 1000000 + v4 * 100000000;
1459
- return hz;
1503
+ return IcomControl.parseFrequencyReply(frame, 0);
1460
1504
  }
1461
1505
  sendAudioFloat32(samples, addLeadingBuffer = false) {
1462
1506
  this.audio.enqueueFloat32(samples, addLeadingBuffer);
@@ -1591,11 +1635,15 @@ class IcomControl {
1591
1635
  audioName: IcomPackets_1.RadioCapPacket.getAudioName(cap),
1592
1636
  supportTX: IcomPackets_1.RadioCapPacket.getSupportTX(cap)
1593
1637
  };
1594
- if (info.civAddress != null)
1638
+ if (info.civAddress != null) {
1595
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
+ }
1596
1644
  if (info.supportTX != null)
1597
1645
  this.civ.supportTX = info.supportTX;
1598
- (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);
1599
1647
  this.ev.emit('capabilities', info);
1600
1648
  }
1601
1649
  break;
@@ -1607,7 +1655,8 @@ class IcomControl {
1607
1655
  const busy = IcomPackets_1.ConnInfoPacket.getBusy(buf);
1608
1656
  this.macAddress = IcomPackets_1.ConnInfoPacket.getMacAddress(buf);
1609
1657
  this.rigName = IcomPackets_1.ConnInfoPacket.getRigName(buf);
1610
- (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);
1611
1660
  if (busy) {
1612
1661
  (0, debug_1.dbg)('CONNINFO busy=true detected - likely reconnecting while rig still has old session');
1613
1662
  (0, debug_1.dbg)('Sending ConnInfo reply anyway to allow STATUS packet delivery');