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
a4bf8aace3
commit
86fb244b9d
@ -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, à
|
**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.
|
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()
|
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)
|
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. */
|
* a GEN24 with a battery before this ships. See DIVERGENCES.md. */
|
||||||
bool hasBattery(double batteryVoltage, double availableCharge);
|
bool hasBattery(double batteryVoltage, double availableCharge);
|
||||||
|
|
||||||
/* INFERRED: the byte order of the GEN24 is not documented by the manufacturer,
|
/* Word order of the float32 points, measured on hardware.
|
||||||
* 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.
|
* This used to read "INFERRED: not documented by the manufacturer, so the
|
||||||
* To be confirmed on the bench, then on hardware. */
|
* 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();
|
SunSpecDataPoint::ByteOrder byteOrder();
|
||||||
|
|
||||||
int clampUpdateInterval(int requestedSeconds);
|
int clampUpdateInterval(int requestedSeconds);
|
||||||
|
|||||||
@ -167,6 +167,8 @@ private slots:
|
|||||||
|
|
||||||
void test_AC_16_three_phase_meter_export_on_every_phase();
|
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_no_battery_means_no_storage_child();
|
||||||
void test_AC_17_an_installed_battery_is_still_detected();
|
void test_AC_17_an_installed_battery_is_still_detected();
|
||||||
void test_AC_17_presence_does_not_depend_on_state_of_charge();
|
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()
|
void SunSpecFroniusTest::test_AC_6_fronius_uses_sunspec_default_byte_order()
|
||||||
{
|
{
|
||||||
// INFERE : non documente par le constructeur, on suppose le defaut SunSpec.
|
// MESURE le 2026-08-07 sur un Symo reel, en lecture seule : l'appareil emet
|
||||||
// Le ByteOrderBigEndian force du vendor solarEdge est une anomalie propre a
|
// en big endian, mot de poids fort d'abord, conformement a la specification
|
||||||
// SolarEdge et ne doit pas etre etendue.
|
// SunSpec.
|
||||||
QCOMPARE(FroniusRules::byteOrder(), SunSpecDataPoint::ByteOrderLittleEndian);
|
//
|
||||||
|
// 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