feat: Add MIT-5000-8T (MIT_8CH) inverter support + RF capture mode - #2962
feat: Add MIT-5000-8T (MIT_8CH) inverter support + RF capture mode#2962Geoffn-Hub wants to merge 11 commits into
Conversation
|
Thank you! |
|
I'm going to submit a dedicated PR for the changes to the receive loop - I figured you might want to have the parsing logic for the MIT inverter packaged seperately from code that alters the core code of the project in more fundamental ways |
|
FWIW - I did take a look at the AhoyDTU implementation which is in the form of a state machine, but don't think it works in this case either |
Add passive RF capture mode to the CMT2300A radio for protocol reverse-engineering. When enabled via the web API, the radio hops across all legal EU channels (50ms dwell) and streams decoded packets over a WebSocket endpoint. Key changes: - CMT radio starts in RX mode on init for passive listening - Channel hopping across legal frequency range in capture mode - FIFO drain loop processes all queued packets per iteration - WebSocket endpoint for real-time packet streaming - Web API endpoints to enable/disable capture mode - Documentation in docs/CaptureMode.md Also fixes FIFO burst reception: the hardware FIFO drain now runs unconditionally and uses break (not continue) on buffer full, ensuring back-to-back MIT response fragments are not lost. Increases MAX_RETRANSMIT_COUNT to 20 for MIT inverters which respond with 6 rapid fragments per poll.
Add initial support for the Hoymiles MIT-5000-8T microinverter. The MIT-5000-8T presents as a 4-channel device at the RF level (each MPPT has dual panel inputs aggregated internally). - New MIT_8CH class extending HMT_Abstract - 4-channel AC/DC data parsing from validated RF captures - Auto-detection by serial number prefix in Hoymiles.cpp
MIT-5000-8T sends SystemConfigPara across 3 fragments (vs 2 for HM/HMT), requiring a larger buffer. Increase expected size to 48 bytes and add validation for the MIT response format.
2744569 to
016c9af
Compare
| if (memcmp(&f.fragment[5], &dtuId.b[1], 4) == 0) { | ||
| // The CMT RF module does not filter foreign packages by itself. | ||
| // Has to be done manually here. | ||
| if (memcmp(&f.fragment[5], &dtuId.b[1], 4) == 0) { |
There was a problem hiding this comment.
So here we could consider to run the Capture mode in promiscuous mode if we simply log received data regardless of the DTU ID.
There was a problem hiding this comment.
Good idea — capture mode already logs all valid CRC-checked frames regardless of source/destination serial, so it's effectively promiscuous already. The DTU serial filtering happens later in the normal processing path (the dumpFragment/command matching), which capture mode bypasses by logging before that stage. We could make this more explicit by skipping the _inverters lookup entirely when in capture mode, but functionally it already captures everything on the current channel.
The MIT-5000-8T sends response fragments at ~835ms intervals instead of the ~50ms typical for HMS/HMT inverters. The default 500ms RX timeout causes premature retransmit requests after receiving only 1-2 of 6 fragments, resulting in 13-15x retransmit ratios and ~55% success. Discovery: capture mode showed all 6 MIT fragments arriving correctly at ~835ms spacing. The DTU-Pro (which works) likely uses longer timeouts. Changes: - RealTimeRunDataCommand: 6000ms timeout for 0x1520 (MIT) serials - AlarmDataCommand: 12000ms timeout for 0x1520 serials - SingleDataCommand: 2000ms retransmit timeout for 0x1520 serials - Clean up debug logging from HoymilesRadio_CMT Note: OTA updates via /api/firmware/update appear to silently fail (device reports old git hash). Flash via USB/serial to test.
The CMT2300A radio automatically exits RX mode (goes to STBY) after receiving a packet. The read() function was reading the FIFO and clearing interrupts, but never putting the radio back into RX mode. This caused subsequent fragments in a burst to be missed — the radio was in STBY when they arrived. This explains why only 2 of 6 MIT-5000-8T fragments were received: frag 1 arrives, radio exits RX, re-enters RX only after the drain loop completes, missing frags 2-3. Then catches frag 4, misses 5-6. The HMS-4CH (Shed) worked because its fragments arrive at ~50ms intervals which is fast enough that the drain loop + next poll caught them. The MIT's ~835ms (capture mode) / ~50ms (normal mode) timing hit a window where the radio was in STBY. Fix: after reading each packet, immediately re-enter RX mode by clearing the FIFO and calling GoRx(). This keeps the radio continuously listening during multi-fragment bursts.
…ch read The previous fix (GoRx after ReadFifo) improved reception from 2/6 to 3/6 fragments, but the GoRx state transition takes too long — fragments arriving during the STBY→RFS→RX transition are still missed. Better approach: set the RX_AUTO_EXIT_DIS bit (0x20) in MODE_CTL when entering RX mode. This tells the CMT2300A to stay in RX after receiving a packet, eliminating the state transition gap entirely. The radio remains continuously listening throughout the entire fragment burst.
The previous commit set RX_AUTO_EXIT_DIS in startListening() before calling GoRx(), but GoRx() writes the bare GO_RX command (0x08) to MODE_CTL, overwriting the 0x20 bit we just set. Fix: OR the RX_AUTO_EXIT_DIS bit into the GoRx command itself within CMT2300A_AutoSwitchStatus(), so both bits are written atomically. Also fix the TX/RX status check comparisons to mask out the new bit.
…instead RX_AUTO_EXIT_DIS caused zero packets to be received — the radio likely needs the RX→STBY transition to properly latch FIFO data. New approach: after reading each packet, write GO_RX directly to MODE_CTL with a single register write (no polling, no FIFO clear). This is the fastest possible RX re-entry — just one SPI transaction instead of the full startListening() sequence.
Instead of draining only what's immediately available and returning, keep polling for 80ms after the last received packet. This catches burst fragments arriving at ~50ms intervals — the radio re-enters RX after each read (fast GoRx write), and the polling loop checks for the next fragment before the loop() cycle completes. Previously, the drain loop would exit after reading 1 packet, set _packetReceived=false, and not check again until the next loop() iteration — by which time the radio had been in STBY and missed the next fragment.
The bare GoRx write (single register write to MODE_CTL) corrupted FIFO reads — 19 'Frame kaputt' CRC failures per cycle. The radio needs the full GoStby → EnableReadFifo → ClearRxFifo → GoRx sequence to properly reset its internal state between packets. Combined with the 80ms polling drain loop, this should catch burst fragments: read packet → full RX re-entry → poll for next packet within 80ms window.
Every approach to re-enter RX after reading a packet has failed: - Bare GoRx write: corrupts FIFO reads (Frame kaputt flood) - Full startListening (GoStby+EnableReadFifo+ClearRxFifo+GoRx): also corrupts because ClearRxFifo destroys in-flight packet data - RX_AUTO_EXIT_DIS bit: radio receives nothing at all The CMT2300A's single-packet FIFO design means it can only hold one packet at a time. After receiving a packet, the radio must exit RX, have the FIFO read, then re-enter RX for the next packet. This inherently creates a gap where packets are missed. Falling back to the extended timeout approach: the radio naturally receives 2-3 of 6 fragments per burst, and retransmit requests (with 6000ms/2000ms timeouts for MIT) eventually collect all fragments. This gives ~75-83% first-try success rate.
| // processed per loop() call, and only when no new packet was arriving. | ||
| // Processing the entire buffer each iteration reduces latency and prevents | ||
| // the software buffer from growing unboundedly during bursts. | ||
| while (!_rxBuffer.empty()) { |
There was a problem hiding this comment.
@Geoffn-Hub if you have received anything from the Radio, you are trying to flush everything immediately out of the Ring-Buffer again until it is empty.
Is this eventually occupying the loop method for too long to fetch the next received bits from the Radio into the buffer ?
I think you had some code to break off the loop early already in Step 1 if something was received, i.e. somewhat before Step 2.
There was a problem hiding this comment.
Fair concern, but in practice the ring buffer stays very small — typical burst is 5-6 fragments, and processing each one (CRC check + dumpFragment) is microsecond-level work. The CMT2300A's single-packet FIFO means we only ever have 1 packet waiting in hardware at a time; the ~50-835ms gap between fragments (depending on inverter type) gives plenty of time for both drain and process.
The original code only processed one packet per loop() call and skipped processing entirely while _packetReceived was true — meaning the software buffer grew unboundedly during bursts. This change fixes that by processing everything each iteration.
That said, if you'd prefer a hybrid approach (process N packets per iteration, or yield back to Step 1 periodically), that's easy to add. In testing with the MIT-5000-8T (6 fragments at ~835ms intervals) and HMS-4CH (5 fragments at ~50ms), the buffer never exceeded ~3-4 entries.
There was a problem hiding this comment.
I am trying to think ahead of us here 😉 as I have even longer Command Sequences in mind, which we still may need to handle:
- AlarmData 0x11 15-16 packets
- AlarmUpdate 0x12
- DOWN_PRO 0x0E used for Firmware Update
- DOWN_DAT 0x0A used for Grid Profile Update
Geoffn-Hub
left a comment
There was a problem hiding this comment.
Good point about longer sequences — worth thinking ahead. A few observations from digging deeper:
The ring buffer drain is fast (microsecond-level per fragment), so 15-16 packets aren't a problem from a buffering perspective.
The bigger factor for MIT is per-packet frequency hopping. Looking at the CMT2300A datasheet, this is actually a designed hardware feature — "fast manual frequency hopping" via FH_CHANNEL/FH_OFFSET registers, with a dedicated app note (AN197) covering RX-side hopping including AFC reconfiguration.
OpenDTU already sets FH_OFFSET=100 (250 kHz steps) during init — which matches exactly what we observed on the MIT. The current RX hop implementation in this PR uses the heavyweight stopListening→setChannel→startListening cycle. We can optimise this to a single FH_CHANNEL register write, which is what the hardware FH feature is designed for.
We also need to add CMT2300A_SetAfcOvfTh() per AN197 for proper RX-side hopping — without it the AFC circuit may not be properly calibrated after channel switches.
For HMS/HMT, longer sequences should work fine on a single channel — the per-packet hopping appears to be MIT-specific behaviour (HMS/HMT consistently deliver all fragments on the same frequency). But having the FH infrastructure in place would also benefit the frequency-drift issue (#2137) down the road.
|
Adding @lumapu @DanielR92 to the party 🎉 as they have done the initial research and CMT implementation for HMS. Found the documentation you mentioned under 2. AFC Parameter Setting for RX Frequency Hopping: Details
and 3. The Overall Process of Rapid Manual Frequency Hopping
|
|
To save hunting back through the comments...this is what we are seeing from the captures CAPTURE 863.50 MHz | frag 01 (src=a025566b) The MIT IS hopping across 3 frequencies: 863.50, 863.75, 864.00 MHz — 250 kHz steps. The pattern per fragment ID: |
|
@Geoffn-Hub any news from your adventures in MIT Frequency Hopping ? |
|
@stefan123t - es könnte sein das HM das auch in die neuere Chargen der HM1600 eingebaut hat S/N 1164A02... siehe Discord Opendtu Channel, wir haben geloggt - da werden nur ein paar Frames empfangen - kannst dich ja mal im Discord melden dann sende ich dir die Logs. Ist aber alles nur Vermutung im Moment. An der Firmware kann es nicht liegen, wir haben da auf dem WR ein Downgrade auf 1.1.12 veranlasst - die sollten funktioniern - fragt sich ob die im Funkmodul was an der Firmware oder Timings geschraubt haben. |
|
Hallo zusammen Gibt es von eueren Versuchen schon eine BETA-Version ? lg Alf |
|
@Zurrmaxe Du bist im PR von Geoffn-Hub da sind alle Änderungen drin. Einfach clonen, ggf den Branch wechseln und den Code bauen... |
|
@stefan123t LG |
|
Ich habe einen nagelneuen MIT5000 diese Woche angeschlossen. Nach Ostern gehts weiter... |
Hallo Stefan, ich hab das ebenfalls getestet, ich bekomme auch Werte vom MIT5000 auf der openDTU - allerdings funktioniert dann die DTU S Pro nicht mehr - auch wenn ich der OpenDTU die selbe ID gebe - ist das normal ? mit diesen Settings funktioniert es bei mir (branch capture_mode): |
|
Das mit dem Schwachlichtverhaltenverhalten mit der Firmware V01.01.12-01.01.08 kann ich bestätigen. Es gibt eine neue Firmware von Hoymiles V01.04.00-01.04.00. Die neue Firmware wird nicht in der DTU Pro S angezeigt. Das neue Firmware merkt man sofort, die PV Leistung steigt um 5% und morgens beginnt der MIT-5000-8T fast schon früher wie die HMS und HMT Serie. Zu der Startspannung kann ich aber noch nichts genaues sagen. Grüße |
|
@OlafWalther1969 Ich betreibe meinen MIT5000 mit jeweils drei "älteren" 395W-Platten. |
|
@Zurrmaxe Das hat sich mit der neuen Firmware V01.04.00-01.04.00 vollkommen geändert. |
|
I have wired-up an 5000-MIT-8T today as a quick test if it is basically functional after having 3 30V modules at a single lane not making it blink yesterday. Today it first remained dead, but having those three modules at three different lanes then had it blink red, so it appears to fire up also without all lanes connected. I then added a fourth module, which I think to have sped up the blinking frequency. Connecting it to the grid then had the device blink green. I had a look at @Geoffn-Hub's two PRs and liked them. Is there a way for me to help bringing this forward? It should be reasonably possible for me to rebase, compile and test these PRs, but there is only little incentive to just do it for myself. |
|
@Geoffn-Hub I followed up on my offer and prepared three handoff branches, moving both PR histories from their old common base (
The combined branch intentionally places #2963 first and then applies all 11 commits from #2962. #2962 already duplicates three parts of #2963—the FIFO drain, processing all buffered packets, and breaking when the buffer is full—but lacks #2963’s As a cross-check, the combined tree differs from the rebased #2962 tree only by that missing #2963 behavior. The only conflict between #2962 and current master was the web API route registration. Current master’s handler casts were retained for the new capture endpoints. All three branches pass a complete PlatformIO Credit where it is due: OpenAI Codex did the heavy lifting here—history inspection, both rebases, conflict and overlap resolution, and build verification—under my direction and review. I have not knowingly changed either of your PR branches; these are review and handoff pointers. |
CMT burst processing reads and removes different queue elementsCodex found a queue-ordering bug in the CMT burst-drain implementation. When fragment_t f;
memset(f.fragment, 0xcc, MAX_RF_PAYLOAD_SIZE);
f.len = std::min<uint8_t>(
_radio->getDynamicPayloadSize(),
MAX_RF_PAYLOAD_SIZE);
f.channel = _radio->getChannel();
f.rssi = _radio->getRssiDBm();
_radio->read(f.fragment, f.len);
_rxBuffer.push(f);The important insertion is: _rxBuffer.push(f);
The processing loop instead accessed fragment_t f = _rxBuffer.back();
// process f
_rxBuffer.pop();Consequently, with queued fragments
A and B are never processed. This directly defeats the purpose of draining multi-packet bursts and can prevent multi-fragment responses from being reassembled. The minimal correction is to access the same oldest element that - fragment_t f = _rxBuffer.back();
+ fragment_t f = _rxBuffer.front();This is about to be committed on The complete |
SystemConfig buffer capacity became an unintended response requirementCodex found a regression caused by using one size for two different purposes in Before the MIT changes, the SystemConfig buffer was 16 bytes and -#define SYSTEM_CONFIG_PARA_SIZE 16
+#define SYSTEM_CONFIG_PARA_SIZE 48It also introduced a configurable expected byte count, but initialized it from that enlarged buffer size: uint8_t _expectedByteCount = SYSTEM_CONFIG_PARA_SIZE;The command does not compare this value for equality. It treats it as a lower bound: const uint8_t fragmentsSize =
getTotalFragmentSize(fragment, max_fragment_id);
if (fragmentsSize < expectedSize) {
return false;
}Consequently, every inverter other than MIT inherited a minimum response requirement of 48 bytes. Valid legacy HM/HMS/HMT responses satisfying the previous 16-byte minimum could therefore be rejected. These are separate concepts:
The patch names and configures them separately: -#define SYSTEM_CONFIG_PARA_SIZE 48
+#define SYSTEM_CONFIG_PARA_BUFFER_SIZE 48
+#define SYSTEM_CONFIG_PARA_DEFAULT_MIN_RESPONSE_SIZE 16-uint8_t _expectedByteCount = SYSTEM_CONFIG_PARA_SIZE;
+uint8_t _minimumResponseSize =
+ SYSTEM_CONFIG_PARA_DEFAULT_MIN_RESPONSE_SIZE;The API and validation code are renamed to describe the actual lower-bound semantics: -uint8_t getExpectedByteCount() const;
-void setExpectedByteCount(uint8_t count);
+uint8_t getMinimumResponseSize() const;
+void setMinimumResponseSize(uint8_t size);MIT retains its model-specific requirement: SystemConfigPara()->setMinimumResponseSize(38);The resulting behavior is:
Thus, the larger MIT response still fits safely, while existing inverter families retain the acceptance threshold used before the buffer expansion. The fix is committed on The complete |
|
@smoe thank you for the hint regarding the rx buffer. Havn't testet yet but it also affects the the nrf Part and could make things better! Would you mind to genervte a seperate PR just to fix this issue in cmt and nrf code? (I can also create a fix and mention your Name. As you prefere) |
@tbnobody, great to hear that this was putatively helpful beyond the MIT work. Please proceed directly. My literally only own contribution was to ask Codex "Is there anything that raises you attention?" and to then review what Codex came up with. Not sure if I should be credited for that. But I have a Codex-driven OpenClaw instance with a seperate GitHub account @smoe-bot, who I guess would be much appreciating to be mentioned. |
|
This one in my reading may be the major blocker for merging this MIT work. MIT recovery settings unintentionally affect every inverterThe MIT workaround changes two recovery settings for all inverter models:
This can delay every other inverter and command in the queue by roughly ten times as long. The patch introduces separate, centrally defined retransmission limits: #define MAX_DEFAULT_RETRANSMIT_COUNT 5
#define MAX_MIT_RETRANSMIT_COUNT 20It selects the MIT limit only for serial prefix The separate diff --git a/lib/Hoymiles/src/commands/CommandAbstract.h b/lib/Hoymiles/src/commands/CommandAbstract.h
@@
#define RF_LEN 32
#define MAX_RESEND_COUNT 4 // Used if all packages are missing
-#define MAX_RETRANSMIT_COUNT 20 // MIT-5000-8T: 2 frags/burst, ~3-4 retransmits per remaining frag, need margin
+#define MAX_DEFAULT_RETRANSMIT_COUNT 5 // Used to send the retransmit package
+#define MAX_MIT_RETRANSMIT_COUNT 20 // Used to send the retransmit package to MIT inverters
diff --git a/lib/Hoymiles/src/commands/CommandAbstract.cpp b/lib/Hoymiles/src/commands/CommandAbstract.cpp
@@
uint8_t CommandAbstract::getMaxRetransmitCount() const
{
- return MAX_RETRANSMIT_COUNT;
+ // Keep the experimental MIT recovery allowance local to that model.
+ constexpr uint16_t mitSerialPrefix = 0x1520;
+ const uint16_t serialPrefix = (_inv->serial() >> 32) & 0xffff;
+ if (serialPrefix == mitSerialPrefix) {
+ return MAX_MIT_RETRANSMIT_COUNT;
+ }
+
+ return MAX_DEFAULT_RETRANSMIT_COUNT;
}
diff --git a/lib/Hoymiles/src/commands/SingleDataCommand.cpp b/lib/Hoymiles/src/commands/SingleDataCommand.cpp
@@
- // MIT-5000-8T responds to retransmit requests at ~835ms intervals
+ // Keep the experimental MIT recovery timeout local to that model.
const uint16_t prefix = (inv->serial() >> 32) & 0xffff;
if (prefix == 0x1520) {
setTimeout(2000);
} else {
- setTimeout(250);
+ setTimeout(100);
}
}This isolation fix does not establish that 20 requests or 2000 ms are the correct MIT values. Those values are preserved to avoid changing MIT behaviour in the same patch, but they should be measured again after correcting the receive-queue bug. The generic ESP32 firmware build succeeds with this patch. Commit: |
|
Here is now a final "Priority 1" observation by Codex. This is not perfectly fixable until we truly know how the channel-changes work. Because of the queue-bug above I also did not want to induce the scheme from what was reported before. Codex asked me for an SDR. Not today. :-) @upstream, I prepared these patches as #3161 for your convenience. Capture mode can make OpenDTU send a command on the wrong frequencyThe CMT2300A is the radio chip that OpenDTU uses to communicate with HMS, HMT and MIT inverters. A radio frequency is the frequency on which the radio sends or receives a message. The source code also calls each available frequency a “channel”. This branch adds an optional function called capture mode. When capture mode is switched on, OpenDTU regularly changes the CMT2300A frequency and listens for messages from nearby inverters. It stays on each frequency for 50 milliseconds before trying the next one. For normal communication, OpenDTU uses the value shown as CMT2300A Frequency in the DTU settings. In the source code, this value is stored in The following sequence was possible:
The complete command was sent. It was not stopped while it was being sent. The problem was that it could be sent on the wrong frequency. A later attempt could succeed because the radio had returned to the communication frequency by then. This could make the problem appear as an occasional lost command or a slow answer. This problem is not limited to MIT inverters. HMS and HMT inverters use the same CMT2300A send code. Capture mode could therefore also disturb communication with these existing inverter models. The problem can only occur while capture mode is switched on. Capture mode can make OpenDTU send a command on the wrong frequencyThe small correction is to select the correct frequency immediately before sending a command: if (cmd.getDataPayload()[0] == 0x56) {
cmtSwitchDtuFreq(getInvBootFrequency());
+} else {
+ cmtSwitchDtuFreq(_inverterTargetFrequency);
}OpenDTU already treats one command differently. All other commands are now sent on the CMT2300A Frequency selected in the DTU settings. Capture mode can still change frequencies while OpenDTU is only listening. It can also still miss messages because it listens on only one frequency at a time. This patch does not try to solve those limitations. It only prevents the frequency selected by capture mode from being used accidentally for a normal command. |
Hi smoe lib/Hoymiles/src/commands/CommandAbstract.cpp:132:12: error: 'MAX_RESEND_COUNT' was not declared in this scope Alf |
Please doublecheck if MAX_RESEND_COUNT is defined in CommandAbstract.h line 10: #define MAX_RESEND_COUNT 4 // Used if all packages are missing also, apply PR 3161: #3161, not this one |
|
Ok, yesterday the DTU Pro S arrived. I installed it quickly in the afternoon. At first, I noticed it needed an update because the MIT5000 wouldn't come online. After the update, it started receiving inverter data. I sent an email to [service@hoymiles.com] asking them to update my inverter to firmware version V01.04.00-01.04.00. They updated it this morning, and the whole process took about half an hour. Now I'm really surprised: since 11:30 this morning until now (13:43), I've had 100% successful reception. I'm still using Geoffn-Hub's
|
|
First thanks to @ms49434. Here my old results from last month:
I build the new the firmware this morning and have this results: Unfortunately, my levels have worsened. Alf |
Just for the sake of curiosity and since I cannot try myself - can you influence your success rate by getting the OpenDTU closer to the inverter (or move thad giant chunk of lead out of the way or unsheath the OpenDTU from its aluminium hat)? |
|
Did you both read my comment? I would say the initial firmware is worse or the OpenDTU part won't fit for the firmware. I upgraded from V01.01.12-01.01.08 to V01.04.00-01.04.00. And now 100℅. Before i get also 33℅/33℅/33℅. |
|
Hi everyone 😄 The solution to our problem is quite simple: Firmware V01.04.00-01.04.00 ❗ ❗ Today in the morning i get the new firmware on my MIT-5000. DTU = ESP32S3 with the last modified code 5c1df91 Alf |
|
I just wish we would not need the vendor's DTU at all. |
|
@Zurrmaxe Empfang Erfolgreich | 13 | 65 % So #define MAX_RETRANSMIT_COUNT 40 is ok. Empfang Erfolgreich | 10 | 100 % |
Looks much like it, indeed. I do not have an exact idea what the basis of that counting of packages is. But I could imagine some scenario with those channel changes and queue problems in which a package was not counted as missed or partly missed. Ideally, the MAX_RETRANSMIT_COUNT should be .. what .. 0 ? So having the need for such a high number of again-transmitted-requests is an indication of something being wrong. Please kindly once try that 5c1df91 branch with those 20. |
We only need the vendors's DTU for the Firmware update. 😒 I used this settings: Alf |
There is admittedly an immediate reflex to reverse-engineer a firmware for the Hoymiles devices. Just like we are doing it for various Wifi routers - I am just not ultimately confident that I want any of those inverters to run on OpenWRT .-) . Preferably though we watch out for Open Source projects like |
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository. edit: Gesendete Anfragen | 16 | So i will go higher again to 40 edit2: |
No, it is a fork of the repository, like the one you tried before that was from gueffn-hub, this one took gueff's work, rebased it on the current main and added some extras.
Thank you!
So, for 16 packages the OpenDTU needs to ask 211+16=227 times and is successful only for 6 out of 10 packages is wanted to receive.
Yes, please do. My temporary explanation is that something is wrong with the channel transition. I presume the OpenDTU asks just so often until that channel iteration by some random chance is working. Well, not having any immediate other ideas, so be it. It would possibly be interesting to learn what channel was successful in receiving the message and the how-many-th request it was to which was successfully replied. |
|
Even with 26sec i get: Empfang Erfolgreich | 19 | 58 % So again, i get back to #define MAX_MIT_RETRANSMIT_COUNT 40 |







Summary
This PR adds native support for the Hoymiles MIT-5000-8T 3-phase microinverter and includes an RF capture mode tool used to reverse-engineer the protocol.
Closes #2742
MIT-5000-8T Inverter Support
Key Findings
0x1520) reports 4 independent MPPT channels over RF, despite the "8T" name — each MPPT tracker has 2 panel inputs sharing a single trackerProtocol Behaviour Differences from HMS/HMT
Testing with a live MIT-5000-8T revealed several protocol differences that required OpenDTU changes:
SystemConfigPara response is 38 bytes (vs 48 for HMS/HMT). The hardcoded
SYSTEM_CONFIG_PARA_SIZEcheck was rejecting valid responses. Fix: made expected byte count configurable viasetExpectedByteCount(), with MIT_8CH setting 38 in its constructor.The MIT sends only 2 fragments per response burst, then waits for individual retransmit requests for each remaining fragment. The HMS/HMT sends all fragments in a single burst. Each retransmit cycle requires ~3 requests before the MIT responds. With 6 total fragments and 4 needing individual recovery at ~3 retransmits each, the default
MAX_RETRANSMIT_COUNTof 5 was insufficient. Increased to 20 for reliable reception.Retransmit RX window: The
SingleDataCommand(RequestFrame) timeout was increased from 100ms to 250ms to accommodate the MIT's slower response timing.Result: MIT RealTimeRunData now achieves ~100% success rate, with each poll cycle taking ~4-5 seconds due to the retransmit dance.
Validated Byte Assignments
All byte assignments were validated by cross-referencing passive RF captures from the MIT-5000-8T against DTU-Pro Modbus TCP data read via Home Assistant sensors:
Complete Decoded Capture (230W production, 2026-02-01)
Raw frames:
Unresolved Fields
Files Changed (MIT Support)
lib/Hoymiles/src/inverters/MIT_8CH.cpp— Inverter implementation with validated byte assignmentslib/Hoymiles/src/inverters/MIT_8CH.h— Headerlib/Hoymiles/src/Hoymiles.cpp— Register MIT_8CH for serial prefix0x1520lib/Hoymiles/src/parser/SystemConfigParaParser.cpp— Configurable expected byte countlib/Hoymiles/src/parser/SystemConfigParaParser.h—setExpectedByteCount()methodlib/Hoymiles/src/commands/CommandAbstract.h—MAX_RETRANSMIT_COUNTincreased to 20lib/Hoymiles/src/commands/SingleDataCommand.cpp— RequestFrame timeout 100ms → 250msRF Capture Mode
A diagnostic tool for reverse-engineering new inverter protocols by passively sniffing RF traffic.
How It Works
API
Files Changed (Capture Mode)
lib/Hoymiles/src/HoymilesRadio_CMT.cpp— Capture mode: channel hopping, frame logging, CRC error reportinglib/Hoymiles/src/HoymilesRadio_CMT.h— Capture mode state and configurationsrc/WebApi_dtu.cpp— REST API endpoints (/api/dtu/captureGET/POST)include/WebApi_dtu.h— Handler declarationsdocs/CaptureMode.md— Full documentationTesting
Tested on: