fix(sunspec): lire les float32 Fronius en big endian, comme le matériel
byteOrder() renvoyait ByteOrderLittleEndian au motif qu'on « supposait le défaut SunSpec ». La supposition confondait le défaut de la SPÉCIFICATION avec celui de libnymea-sunspec, qui est son contraire : convertToUInt32 lit registers.at(1) puis .at(0) en little endian. Garder ce défaut revient à lire tous les float32 avec leurs deux mots permutés. Mesuré le 2026-08-07 sur un Symo réel, en lecture seule : le compteur 213 sur l'unité 200 sert Hz = 4248 0000, soit 50,0000 en big endian et 2,3777e-41 en mots inversés. L'onduleur 113 concorde. Le coût n'était pas un décalage mais une absurdité. Reproduit sur banc avec le simulateur émettant l'ordre du matériel : un compteur important 2,7 kW rapporté comme exportant 107 MW, et l'arbitre enclenchant toutes les charges sur un « Surplus PV 107622496 W ». Le thing reste connected, sans erreur Modbus. Conséquence sur l'ordre des correctifs : meterPowerSign() reste juste mais n'est observable qu'une fois celui-ci appliqué — corriger le signe d'un dénormal ne produit rien. solarEdge force déjà big endian de son côté et le générique garde son paramètre utilisateur : aucun des deux chemins n'est touché. test_AC_18 fige le raisonnement plutôt que la seule valeur. Voir DIVERGENCES.md D-8. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
63abc15b44
commit
d08cf506ee
@ -200,3 +200,46 @@ garde de présence doit donc être rejouée sur banc, pas seulement en unitaire.
|
||||
|
||||
**Comment lever.** Étendre `tests/plugin`, qui instancie réellement le plugin, à
|
||||
un scénario de setup initial avec un modèle 124 à zéro. Hors périmètre ici.
|
||||
|
||||
---
|
||||
|
||||
## D-8 — ordre des mots des float32 : le défaut de la bibliothèque n'est pas celui de la spec (MESURÉ)
|
||||
|
||||
**Contexte.** `FroniusRules::byteOrder()` renvoyait `ByteOrderLittleEndian`, avec
|
||||
pour justification « non documenté par le constructeur, on suppose le défaut
|
||||
SunSpec ».
|
||||
|
||||
**L'écart.** La supposition confondait deux « défauts ». Un float32 SunSpec
|
||||
occupe deux registres, **mot de poids fort d'abord**. Or
|
||||
`SunSpecDataPoint::convertToUInt32` lit `registers.at(0)` puis `.at(1)` en
|
||||
`ByteOrderBigEndian`, et l'**inverse** en `ByteOrderLittleEndian` — qui est le
|
||||
défaut de libnymea-sunspec. Garder ce défaut revient à lire tous les float32 de
|
||||
la personnalité Fronius avec leurs deux mots permutés.
|
||||
|
||||
**Mesure.** Relevé du 2026-08-07 sur un Fronius Symo réel, en lecture seule
|
||||
(`sunspec/tools/read-float-order.py`) :
|
||||
|
||||
| point | registres bruts | big endian | mots inversés |
|
||||
|---|---|---|---|
|
||||
| compteur 213 u200 `Hz` | `4248 0000` | **50,0000** | 2,3777e−41 |
|
||||
| compteur 213 u200 `PhVphA` | `436a 8000` | **234,5000** | −2,4184e−41 |
|
||||
| onduleur 113 u1 `Hz` | `4247 fc48` | **49,9964** | −4,1592e+36 |
|
||||
|
||||
**Ce que cela coûtait.** Pas un décalage : une absurdité. Reproduit sur banc avec
|
||||
le simulateur émettant l'ordre du matériel — un compteur **important 2,7 kW** a
|
||||
été rapporté comme **exportant 107 MW**, `Current load on root meter:
|
||||
-1.07621e+08W`, et l'arbitre a enclenché toutes les charges sur un « Surplus PV
|
||||
107622496 W ». Un tel appareil se présente `connected = true`, sans aucune
|
||||
erreur Modbus.
|
||||
|
||||
**Ordre des correctifs.** `meterPowerSign()` (D-5) reste juste, mais n'est
|
||||
**observable** qu'une fois l'ordre des mots corrigé : corriger le signe d'un
|
||||
dénormal ne produit rien.
|
||||
|
||||
**Non touché.** Le vendor solarEdge force déjà big endian sur son propre chemin ;
|
||||
le vendor générique conserve son paramètre `endianness` réglable par
|
||||
l'utilisateur. Aucun des deux n'est modifié.
|
||||
|
||||
**Verrou.** `test_AC_6` affirme désormais `ByteOrderBigEndian`, et
|
||||
`test_AC_18_swapped_words_yield_denormals` fige le raisonnement : les registres
|
||||
réels lus à l'envers donnent un dénormal, jamais une mesure plausible.
|
||||
|
||||
@ -52,7 +52,9 @@ bool hasBattery(double batteryVoltage, double availableCharge)
|
||||
|
||||
SunSpecDataPoint::ByteOrder byteOrder()
|
||||
{
|
||||
return SunSpecDataPoint::ByteOrderLittleEndian;
|
||||
/* Big endian is what the hardware emits, and what SunSpec specifies. The
|
||||
* library's default is the opposite; see the header for the measurement. */
|
||||
return SunSpecDataPoint::ByteOrderBigEndian;
|
||||
}
|
||||
|
||||
MeterConnectionDecision decideMeterConnection(int meterSlaveId, int inverterSlaveId)
|
||||
|
||||
@ -129,10 +129,35 @@ double meterPowerSign(bool froniusMeter);
|
||||
* a GEN24 with a battery before this ships. See DIVERGENCES.md. */
|
||||
bool hasBattery(double batteryVoltage, double availableCharge);
|
||||
|
||||
/* INFERRED: the byte order of the GEN24 is not documented by the manufacturer,
|
||||
* so the SunSpec default is assumed. The forced big endian of the SolarEdge
|
||||
* vendor is an anomaly specific to SolarEdge and must not be extended here.
|
||||
* To be confirmed on the bench, then on hardware. */
|
||||
/* Word order of the float32 points, measured on hardware.
|
||||
*
|
||||
* This used to read "INFERRED: not documented by the manufacturer, so the
|
||||
* SunSpec default is assumed. To be confirmed on the bench, then on hardware."
|
||||
* It has now been confirmed, and the assumption was wrong: what was assumed to
|
||||
* be "the SunSpec default" was in fact libnymea-sunspec's own default, which is
|
||||
* the opposite of the specification.
|
||||
*
|
||||
* A float32 spans two registers. SunSpec puts the high word first.
|
||||
* SunSpecDataPoint::convertToUInt32 reads registers.at(0) then .at(1) for
|
||||
* ByteOrderBigEndian, and the reverse for ByteOrderLittleEndian — and little
|
||||
* endian is the library's default. Keeping that default on a Fronius means
|
||||
* reading every float32 with its two words swapped.
|
||||
*
|
||||
* Measured 2026-08-07 on a Fronius Symo, read only:
|
||||
* meter 213, unit 200 : Hz = 4248 0000 -> 50.0000 big endian
|
||||
* PhVphA = 436a 8000 -> 234.5000
|
||||
* inverter 113, unit 1: Hz = 4247 fc48 -> 49.9964
|
||||
* Swapped, those same registers give 2.4e-41 and -4.2e+36: denormals, the
|
||||
* signature of a wrong interpretation rather than a measurement.
|
||||
*
|
||||
* What it costs to get this wrong is not a small offset. Reproduced on the
|
||||
* bench with the simulator emitting the hardware's order: a meter importing
|
||||
* 2.7 kW was reported as exporting 107 MW, and the arbiter switched every load
|
||||
* on. Correcting the SIGN of a denormal changes nothing, so meterPowerSign()
|
||||
* only becomes observable once this is right.
|
||||
*
|
||||
* The SolarEdge vendor already forces big endian on its own path; the generic
|
||||
* vendor keeps its user-settable parameter. Neither is affected. */
|
||||
SunSpecDataPoint::ByteOrder byteOrder();
|
||||
|
||||
int clampUpdateInterval(int requestedSeconds);
|
||||
|
||||
@ -167,6 +167,8 @@ private slots:
|
||||
|
||||
void test_AC_16_three_phase_meter_export_on_every_phase();
|
||||
|
||||
void test_AC_18_swapped_words_yield_denormals();
|
||||
|
||||
void test_AC_17_no_battery_means_no_storage_child();
|
||||
void test_AC_17_an_installed_battery_is_still_detected();
|
||||
void test_AC_17_presence_does_not_depend_on_state_of_charge();
|
||||
@ -421,10 +423,44 @@ void SunSpecFroniusTest::test_AC_5_manufacturer_filter_rejects_other_manufacture
|
||||
|
||||
void SunSpecFroniusTest::test_AC_6_fronius_uses_sunspec_default_byte_order()
|
||||
{
|
||||
// INFERE : non documente par le constructeur, on suppose le defaut SunSpec.
|
||||
// Le ByteOrderBigEndian force du vendor solarEdge est une anomalie propre a
|
||||
// SolarEdge et ne doit pas etre etendue.
|
||||
QCOMPARE(FroniusRules::byteOrder(), SunSpecDataPoint::ByteOrderLittleEndian);
|
||||
// MESURE le 2026-08-07 sur un Symo reel, en lecture seule : l'appareil emet
|
||||
// en big endian, mot de poids fort d'abord, conformement a la specification
|
||||
// SunSpec.
|
||||
//
|
||||
// compteur 213 unite 200 : Hz = 4248 0000 -> 50.0000
|
||||
// onduleur 113 unite 1 : Hz = 4247 fc48 -> 49.9964
|
||||
//
|
||||
// Ce test affirmait l'inverse, avec pour justification « on suppose le
|
||||
// defaut SunSpec ». La supposition confondait le defaut de la SPEC avec
|
||||
// celui de libnymea-sunspec, qui est son contraire. Le nom du test est
|
||||
// conserve pour ne pas casser les references, mais « sunspec_default »
|
||||
// designe desormais le defaut de la specification, pas celui de la lib.
|
||||
QCOMPARE(FroniusRules::byteOrder(), SunSpecDataPoint::ByteOrderBigEndian);
|
||||
}
|
||||
|
||||
void SunSpecFroniusTest::test_AC_18_swapped_words_yield_denormals()
|
||||
{
|
||||
// Antidote : verrouille le RAISON du choix, pas seulement le choix.
|
||||
// Les registres ci-dessous sont ceux releves sur le Symo. Lus dans le bon
|
||||
// ordre ils donnent une frequence reseau ; lus mots inverses, un denormal.
|
||||
// Si quelqu'un rebascule byteOrder() en little endian, c'est cette lecture
|
||||
// absurde que le plugin produira sur du materiel reel.
|
||||
const quint16 hi = 0x4248, lo = 0x0000; // Hz du compteur 213
|
||||
|
||||
auto asFloat = [](quint16 first, quint16 second) {
|
||||
const quint32 raw = (quint32(first) << 16) | second;
|
||||
float value;
|
||||
memcpy(&value, &raw, sizeof(raw));
|
||||
return value;
|
||||
};
|
||||
|
||||
// Big endian : le registre de poids fort d'abord.
|
||||
QVERIFY(qAbs(asFloat(hi, lo) - 50.0f) < 0.01f);
|
||||
|
||||
// Mots inverses : un denormal, jamais une mesure plausible.
|
||||
const float swapped = asFloat(lo, hi);
|
||||
QVERIFY(swapped > 0.0f);
|
||||
QVERIFY(swapped < 1.0e-40f);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user