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 {
|
||||
|
||||
Reference in New Issue
Block a user