fix: power is RM5, not RM4 — the CW keying was the experiment

An FM carrier could not answer it: constant by definition, so nothing to
correlate. CW at 100 W did, because the keying itself varies the output:

  key down: RM4=25 RM5=207 RM6=13
  key up:   RM4=25 RM5=0   RM6=0

RM5 follows the RF envelope exactly — it IS the power meter. RM4 sits near 25
whether the key is down or up, so it is not measuring output at all, and reading
it as power is what showed 8 W on a 100 W transmission. The operator's hunch was
right and my first reading of the ramp was wrong: what I took for a needle
rising was RM4 drifting, not tracking.

Watts are now derived from that meter (207 = 100 W, measured) instead of the
power SETTING scaled by a percentage — the setting says what was asked for, the
meter says what left the radio.

The bars also hold their peak with a gentle decay. In CW the meters genuinely
read zero between elements, so following the raw value made them flash to nothing
several times a second; a needle has inertia, and this only ever holds a value
the radio really reported.
This commit is contained in:
2026-07-29 13:56:59 +02:00
parent cbe571a742
commit 8e491544dd
4 changed files with 66 additions and 16 deletions
+57 -12
View File
@@ -58,7 +58,10 @@ type YaesuTXState struct {
// SWR is the RATIO (1.0, 1.5…), computed from the reflection coefficient —
// what an operator reads on the rig, not a percentage of meter travel.
SWR float64 `json:"swr"`
VOX bool `json:"vox"`
// PowerW is the output in WATTS, taken from the METER rather than from the
// power setting — the setting says what was asked for, not what came out.
PowerW float64 `json:"power_w"`
VOX bool `json:"vox"`
// CW-only controls. Read (and shown) only in CW, where MIC and VOX mean
// nothing and these are what an operator reaches for.
KeySpeed int `json:"key_speed"` // WPM
@@ -155,17 +158,24 @@ func (y *Yaesu) readPanel(mode string, split bool, txHz int64) {
debugLog.Printf("yaesu: meters at PC=%dW: %s (compare two different power settings)",
y.panel.RFPower, strings.Join(raw, " "))
}
// Measured on an FTDX10 (2026-07-29), a steady carrier for three seconds:
// Measured on an FTDX10 (2026-07-29). The FM carrier was inconclusive —
// constant by definition — so the answer came from CW at 100 W, where the
// keying itself is the experiment:
//
// RM1=0 RM2=unsupported RM3=0 RM4=9→18→21→22 RM5=208 flat RM6=0
// key down: RM4=25 RM5=207 RM6=13
// key up: RM4=25 RM5=0 RM6=0
//
// RM4 is the one that RAMPS with the output, so RM4 is the power meter.
// RM5 sat at 208 from the first sample to the last, unmoved by the power —
// that is not SWR (the operator's was 1.1) and not a meter that reads
// anything we can show; borrowing the FT-991A's table, which puts SWR
// there, is exactly what made the bar read 81.
if v, ok := y.askNum("RM4;", "RM4", 3); ok {
y.panel.PowerMeter = scale255(v)
// RM5 follows the RF envelope exactly, so RM5 is the POWER meter. RM4 sits
// near 25 whether the key is down or up, so it is not measuring output at
// all — reading it as power is what showed 8 W on a 100 W transmission.
// Peak-hold. In CW the key is up between elements and the meters genuinely
// read 0 there — the log shows RM5 alternating 207, 0, 207 — so following
// the raw value makes the bars flicker to nothing several times a second
// and the number unreadable. A real meter has needle inertia; this is the
// same idea, and it only ever holds a value the radio actually reported.
if v, ok := y.askNum("RM5;", "RM5", 3); ok {
y.panel.PowerMeter = holdPeak(y.panel.PowerMeter, scale255(v))
y.panel.PowerW = float64(holdPeak(int(y.panel.PowerW), int(yaesuWatts(v))))
}
// SWR is RM6, and a second measurement at a KNOWN mismatch settled both the
// index and the scale: 0 at SWR 1.1, then 52 at SWR 1.5. 52/255 = 0.204,
@@ -173,8 +183,12 @@ func (y *Yaesu) readPanel(mode string, split bool, txHz int64) {
// the raw value is rho scaled to 255, and the ratio follows from physics
// rather than from a fitted curve.
if v, ok := y.askNum("RM6;", "RM6", 3); ok {
y.panel.SWRMeter = scale255(v)
y.panel.SWR = swrFromReflection(v)
y.panel.SWRMeter = holdPeak(y.panel.SWRMeter, scale255(v))
// SWR only means something while power is going out: between CW elements
// the reading is 0, which would show as a perfect 1.0 match.
if v > 0 {
y.panel.SWR = swrFromReflection(v)
}
}
} else {
// Zeroed rather than frozen: a stale SWR bar from the last transmission
@@ -694,3 +708,34 @@ func swrFromReflection(raw int) float64 {
}
return swr
}
// yaesuWatts converts the power-meter reading to watts.
//
// Single-point calibration, measured rather than assumed: an FTDX10 keying CW at
// 100 W reads 207. The PO meter is close enough to linear in power for one point
// to carry the scale, and a figure taken from the METER beats the power SETTING,
// which says what was asked for and not what left the radio.
//
// A second point at low power would refine it; until then the number is right at
// 100 W and approximate below.
const yaesuPowerFullScale = 207.0 // raw reading measured at 100 W
func yaesuWatts(raw int) float64 {
if raw <= 0 {
return 0
}
return float64(raw) * 100.0 / yaesuPowerFullScale
}
// holdPeak gives a meter the inertia a needle has: it jumps to a higher reading
// at once and falls back gradually. Without it a CW transmission — key up
// between every element — makes the bars flash to zero several times a second.
//
// The decay is a quarter of the gap per poll, so a real drop in power still
// shows within about a second at the default rate.
func holdPeak(current, sample int) int {
if sample >= current {
return sample
}
return current - (current-sample)/4
}