fix(focas): decode servo/spindle meters by their base/exponent envelope; MaxRpm -> MotorSpeedPct
Servo (0x0056) and spindle (0x0040) meter records are 8 bytes:
[i32 value][u16 base][u16 exponent], the same self-describing envelope as the
position blocks.
Servo: offset 4 was read as a decimal count. It is the BASE (a steady 10), so
every load was divided by 10^10 and published ~0 -- the sister ignitionoee panel
read servo 0% on every axis of a Makino cutting at 20,000 rpm. Loads are now
value / base^exponent, a direct percentage.
Spindle: the raw meter is a 1/16384 fraction of full scale. Settled on the real
Z-34184 by regressing the type-1 meter against cnc_rddynamic2 spindle speed over
596..20,000 rpm (rpm = 1.220749 x meter; 16384 x 1.220749 = 20000.75). So:
- Spindle/{name}/Load publishes % of rated load (Float64; was the raw Int32
count -- a reading of 9155 meant 55.9%).
- Spindle/{name}/MaxRpm is RENAMED Spindle/{name}/MotorSpeedPct (Float64, % of
maximum speed) and is polled with Load. The old node was actively wrong: the
type-1 meter is the live motor speed, and it was read once at bootstrap and
served forever as if it were a max-RPM constant. Client-visible: FIXED: tags
on .../MaxRpm must be re-pointed.
- The spindle read no longer stops at the first zero after slot 1 (which dropped
an idle second spindle and kept non-zero residue); every slot is returned.
A record whose envelope is undescribable (base not 2/10, exponent > 18 -- e.g.
residue past the configured axis/spindle count) publishes BadNoData, never a Good 0.
Ported from the sister ignitionoee FOCAS driver v0.17.2/v0.17.3 (Focas.Wire-019);
live captures live-spmeter-{load,maxrpm}-0x0040.bin added as fixtures.
This commit is contained in:
@@ -140,9 +140,10 @@ public interface IFocasClient : IDisposable
|
||||
// ---- Fixed-tree T3.5 (servo meters) ----
|
||||
|
||||
/// <summary>
|
||||
/// Read the servo-load meter percentages across all configured axes.
|
||||
/// Values are percentages (scaled by <c>10^Dec</c>). Empty list on a
|
||||
/// disconnected session or unsupported CNC.
|
||||
/// Read the servo-load meter percentages across all configured axes. Each record's own
|
||||
/// <c>value / base^exponent</c> envelope is applied (servo loads are direct percentages);
|
||||
/// a record whose envelope describes no value carries a null <see cref="FocasServoLoad.LoadPercent"/>.
|
||||
/// Empty list on a disconnected session or unsupported CNC.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The servo-load meter percentages across all configured axes.</returns>
|
||||
@@ -151,26 +152,31 @@ public interface IFocasClient : IDisposable
|
||||
// ---- Fixed-tree T3.6 (spindle meters) ----
|
||||
|
||||
/// <summary>
|
||||
/// Read per-spindle load percentages. Result list index corresponds to
|
||||
/// spindle index from <see cref="GetSpindleNamesAsync"/>. Empty list on a
|
||||
/// disconnected session or when the CNC doesn't support the call (older
|
||||
/// series like 16i may return EW_FUNC).
|
||||
/// Read per-spindle load as a percentage of rated load (<c>cnc_rdspmeter</c> type 0; the
|
||||
/// raw meter is a 1/16384 fraction of full scale). Result list index corresponds to
|
||||
/// spindle index from <see cref="GetSpindleNamesAsync"/>; a slot whose record describes no
|
||||
/// value is null. Empty list on a disconnected session or when the CNC doesn't support the
|
||||
/// call (older series like 16i may return EW_FUNC).
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Per-spindle load percentages, indexed as per <see cref="GetSpindleNamesAsync"/>.</returns>
|
||||
Task<IReadOnlyList<int>> GetSpindleLoadsAsync(CancellationToken cancellationToken);
|
||||
Task<IReadOnlyList<double?>> GetSpindleLoadsAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Read per-spindle maximum RPM values. Static configuration, fetched once at
|
||||
/// bootstrap. Index alignment as per <see cref="GetSpindleLoadsAsync"/>.
|
||||
/// Read per-spindle motor speed as a percentage of maximum speed (<c>cnc_rdspmeter</c>
|
||||
/// type 1, historically labelled <c>cnc_rdspmaxrpm</c>). A live value, not a max-RPM
|
||||
/// constant. Index alignment and null semantics as per <see cref="GetSpindleLoadsAsync"/>.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Per-spindle maximum RPM values, indexed as per <see cref="GetSpindleLoadsAsync"/>.</returns>
|
||||
Task<IReadOnlyList<int>> GetSpindleMaxRpmsAsync(CancellationToken cancellationToken);
|
||||
/// <returns>Per-spindle motor-speed percentages, indexed as per <see cref="GetSpindleLoadsAsync"/>.</returns>
|
||||
Task<IReadOnlyList<double?>> GetSpindleMotorSpeedsAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>One servo-meter entry — one axis's current load percentage.</summary>
|
||||
public sealed record FocasServoLoad(string AxisName, double LoadPercent);
|
||||
/// <summary>
|
||||
/// One servo-meter entry — one axis's current load percentage; null when the CNC's record
|
||||
/// described no usable value (never a fabricated 0).
|
||||
/// </summary>
|
||||
public sealed record FocasServoLoad(string AxisName, double? LoadPercent);
|
||||
|
||||
/// <summary>Which cumulative counter <see cref="IFocasClient.GetTimerAsync"/> reads.</summary>
|
||||
public enum FocasTimerKind
|
||||
@@ -297,19 +303,16 @@ public sealed record FocasDynamicSnapshot(
|
||||
/// </param>
|
||||
public readonly record struct FocasPosition(int Raw, int Base = 0, int Exponent = 0, bool HasScale = false)
|
||||
{
|
||||
/// <summary>
|
||||
/// Largest exponent that can describe a real coordinate. An i32 carries at most ten digits,
|
||||
/// so anything beyond is a misread block — and left unbounded, <c>raw / 10^400</c> is
|
||||
/// <c>raw / Infinity = 0.0</c>, a Good reading of a machine parked at the origin.
|
||||
/// </summary>
|
||||
public const int MaxExponent = 18;
|
||||
|
||||
/// <summary>
|
||||
/// False when the block says it holds no usable position: the <c>0xFFFF</c> vacant marker, a
|
||||
/// base other than 2 or 10, or an absurd exponent. A per-node quality decision — that node
|
||||
/// publishes Bad, its siblings are unaffected.
|
||||
/// </summary>
|
||||
public bool IsValid => !HasScale || (Exponent <= MaxExponent && Base is 2 or 10);
|
||||
/// <remarks>
|
||||
/// The exponent bound matters: left unbounded, <c>raw / 10^400</c> is
|
||||
/// <c>raw / Infinity = 0.0</c> — a Good reading of a machine parked at the origin.
|
||||
/// </remarks>
|
||||
public bool IsValid => !HasScale || Wire.FocasValueEnvelope.IsValid(Base, Exponent);
|
||||
|
||||
/// <summary>
|
||||
/// The position in engineering units: <c>Raw / Base^Exponent</c> when the block carried its
|
||||
@@ -321,7 +324,7 @@ public readonly record struct FocasPosition(int Raw, int Base = 0, int Exponent
|
||||
public double? ToEngineering(int fallbackDecimalPlaces)
|
||||
{
|
||||
if (!IsValid) return null;
|
||||
if (HasScale) return Exponent == 0 ? Raw : Raw / Math.Pow(Base, Exponent);
|
||||
if (HasScale) return Wire.FocasValueEnvelope.Scale(Raw, Base, Exponent);
|
||||
var dp = Math.Max(0, fallbackDecimalPlaces);
|
||||
return dp > 0 ? Raw / Math.Pow(10, dp) : Raw;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user