fix: the Yaesu power meter is not linear — calibrate it on three points
One point could not reveal the curve. Scaling 207 = 100 W straight down read 30 W where the radio showed 10, and 75 where it showed 50 — wrong everywhere except at the single point it was fitted to. Three readings taken against the rig's own display give the shape: raw 62 → 10 W raw 155 → 50 W raw 207 → 100 W Interpolating between them reproduces the radio exactly at those points and stays close in between. I did not fit a formula: three samples can be made to support several curves, and the operator can check a table against their own meter. Above the top the last segment's slope continues rather than clamping, so a rig driving an amplifier does not sit pinned at 100 W. A test pins the measured pairs and the monotonicity, so a later change that breaks them fails against the radio rather than against taste.
This commit is contained in:
+42
-11
@@ -713,20 +713,52 @@ func swrFromReflection(raw int) float64 {
|
||||
|
||||
// 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.
|
||||
// The meter is NOT linear in power, which one calibration point could never
|
||||
// reveal: scaling 207 = 100 W straight down read 30 W where the rig showed 10,
|
||||
// and 75 where it showed 50. Three points measured against the radio's own
|
||||
// display settle the curve:
|
||||
//
|
||||
// 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
|
||||
// raw 62 → 10 W
|
||||
// raw 155 → 50 W
|
||||
// raw 207 → 100 W
|
||||
//
|
||||
// Interpolating between them reproduces the rig exactly at those points and
|
||||
// stays close in between — better than fitting a formula to three samples and
|
||||
// pretending it holds everywhere. Above the last point it keeps the final
|
||||
// segment's slope, so an amplifier-driving rig does not flatten at 100 W.
|
||||
//
|
||||
// Measured on an FTDX10 (2026-07-29). Another model may well need its own row.
|
||||
var yaesuPowerCurve = []struct {
|
||||
raw int
|
||||
watts float64
|
||||
}{
|
||||
{0, 0},
|
||||
{62, 10},
|
||||
{155, 50},
|
||||
{207, 100},
|
||||
}
|
||||
|
||||
func yaesuWatts(raw int) float64 {
|
||||
if raw <= 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(raw) * 100.0 / yaesuPowerFullScale
|
||||
for i := 1; i < len(yaesuPowerCurve); i++ {
|
||||
hi := yaesuPowerCurve[i]
|
||||
if raw <= hi.raw {
|
||||
lo := yaesuPowerCurve[i-1]
|
||||
span := float64(hi.raw - lo.raw)
|
||||
if span <= 0 {
|
||||
return hi.watts
|
||||
}
|
||||
f := float64(raw-lo.raw) / span
|
||||
return lo.watts + f*(hi.watts-lo.watts)
|
||||
}
|
||||
}
|
||||
// Past the top of the curve: extend the last segment rather than clamp.
|
||||
n := len(yaesuPowerCurve)
|
||||
lo, hi := yaesuPowerCurve[n-2], yaesuPowerCurve[n-1]
|
||||
slope := (hi.watts - lo.watts) / float64(hi.raw-lo.raw)
|
||||
return hi.watts + float64(raw-hi.raw)*slope
|
||||
}
|
||||
|
||||
// meterPeak gives a meter the inertia a needle has: it jumps to a higher reading
|
||||
@@ -735,8 +767,7 @@ func yaesuWatts(raw int) float64 {
|
||||
// The hold is the part that matters on the air. Between CW elements the gap is
|
||||
// milliseconds, but between the words of a CQ — in CW as in SSB — it is most of
|
||||
// a second, and a meter that decays straight away reads 0 in every one of those
|
||||
// gaps. The operator sees a bar flashing rather than the power and SWR they are
|
||||
// actually running.
|
||||
// gaps: the operator sees a bar flashing rather than the power they are running.
|
||||
type meterPeak struct {
|
||||
val int
|
||||
at time.Time
|
||||
@@ -757,7 +788,7 @@ func (m *meterPeak) update(sample int, now time.Time) int {
|
||||
return m.val // still inside the hold — the needle has not started to fall
|
||||
}
|
||||
// At least one step down, always. A proportional decay on integers stalls
|
||||
// near the end — with the needle at 13 and the truth at 10, a quarter of the
|
||||
// near the end: with the needle at 13 and the truth at 10, a quarter of the
|
||||
// gap rounds to zero and the meter sits three units high for ever.
|
||||
step := (m.val - sample) / meterDecay
|
||||
if step < 1 {
|
||||
|
||||
@@ -118,3 +118,46 @@ func TestMeterPeakHold(t *testing.T) {
|
||||
t.Errorf("settled at %d, want 10 — the meter must converge on the truth", v)
|
||||
}
|
||||
}
|
||||
|
||||
// The power curve, pinned to the readings taken against the rig's own display.
|
||||
//
|
||||
// It is NOT linear, and one calibration point could not show that: scaling
|
||||
// 207 = 100 W straight down read 30 W where the radio showed 10, and 75 where it
|
||||
// showed 50. These are the three measured pairs, so a change to the curve that
|
||||
// breaks them is a regression against the radio, not against a preference.
|
||||
func TestYaesuPowerCurve(t *testing.T) {
|
||||
cases := []struct {
|
||||
raw int
|
||||
watts float64
|
||||
tol float64
|
||||
}{
|
||||
{0, 0, 0.1},
|
||||
{62, 10, 0.5}, // measured
|
||||
{155, 50, 0.5}, // measured
|
||||
{207, 100, 0.5}, // measured
|
||||
// Between the measured points it interpolates, so it must land inside the
|
||||
// bracket rather than shooting past it.
|
||||
{100, 30, 10},
|
||||
{180, 75, 10},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := yaesuWatts(c.raw)
|
||||
if got < c.watts-c.tol || got > c.watts+c.tol {
|
||||
t.Errorf("yaesuWatts(%d) = %.1f W, want %.1f ±%.1f", c.raw, got, c.watts, c.tol)
|
||||
}
|
||||
}
|
||||
// Monotonic: more meter must never mean less power.
|
||||
prev := -1.0
|
||||
for raw := 0; raw <= 255; raw++ {
|
||||
v := yaesuWatts(raw)
|
||||
if v < prev {
|
||||
t.Fatalf("power fell from %.1f to %.1f at raw=%d", prev, v, raw)
|
||||
}
|
||||
prev = v
|
||||
}
|
||||
// Above the top of the curve it keeps rising rather than flattening at 100 W —
|
||||
// a rig driving an amplifier can read past its own full scale.
|
||||
if v := yaesuWatts(230); v <= 100 {
|
||||
t.Errorf("yaesuWatts(230) = %.1f, want more than 100 — the curve should extend", v)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user