mk2vsc 0.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mk2vsc-0.1.2/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kyle Hart
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
mk2vsc-0.1.2/PKG-INFO ADDED
@@ -0,0 +1,238 @@
1
+ Metadata-Version: 2.4
2
+ Name: mk2vsc
3
+ Version: 0.1.2
4
+ Summary: Read, verify, edit and diff Victron VEConfigure .rvms configuration files without VEConfigure
5
+ Author: Kyle Hart
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/kylehart/mk2vsc
8
+ Keywords: victron,veconfigure,rvms,rvsc,ve.bus,multiplus,quattro,ess
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Intended Audience :: System Administrators
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: System :: Hardware
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Provides-Extra: test
19
+ Requires-Dist: pytest>=7; extra == "test"
20
+ Dynamic: license-file
21
+
22
+ # mk2vsc
23
+
24
+ Read, validate, decode, diff, edit and qualify Victron VEConfigure `.rvms` configuration files
25
+ without VEConfigure, on any operating system, from Python or the command line.
26
+
27
+ ## The problem
28
+
29
+ Victron MultiPlus and Quattro inverter/chargers are configured with VEConfigure and VE.Bus System
30
+ Configurator. Those tools run only on Windows. VictronConnect is replacing them but does not support
31
+ assistants (ESS, AC PV and the rest), so anyone maintaining a real installation still needs a Windows
32
+ machine or a virtual machine. The configuration itself travels as a small binary file: `.rvsc` for a
33
+ single unit, `.rvms` for parallel and multi-phase systems, downloaded and uploaded through VRM's Remote
34
+ VEConfigure. A Victron Community thread titled "RVSC File Format Specification" asked Victron to publish
35
+ the format so people could write their own editors; it received no answer. As of September 2026 we
36
+ found no open-source parser, specification or editor for these files anywhere.
37
+
38
+ We operate four two-inverter systems and needed to change charge voltages and Virtual Switch
39
+ thresholds on them remotely, repeatably, and with a record of what changed. This repository is what we
40
+ built to do that, together with everything we learned about the file along the way.
41
+
42
+ ## What this is
43
+
44
+ * A zero-dependency Python 3.9+ library and a CLI (`mk2vsc`) that:
45
+ * parses the file's section structure and verifies every integrity checksum,
46
+ * decodes the per-inverter settings array into labelled values with a confidence level per field,
47
+ * compares two files by inverter serial and tells you whether they differ only in bookkeeping,
48
+ * edits settings in place, self-verifies that nothing else changed, and never changes file length,
49
+ * qualifies a file against the values you intended before you upload and after you re-download,
50
+ * mines a library of archived downloads into a dated, per-inverter change log (`mk2vsc history`).
51
+ * A corpus of 84 real device files with a manifest, and a test suite that checks every documented
52
+ claim against that corpus (468 tests).
53
+ * A written account of the format as we understand it, and of what we do not understand.
54
+
55
+ ## What this is not
56
+
57
+ * It does not upload anything. You upload through VRM, exactly as before.
58
+ * It does not install assistants (ESS and others) as a supported operation. The experimental graft and
59
+ upload-form transform are included under `mk2vsc.experimental`, gated, and have never produced a running
60
+ system; docs/ESS_INJECTION.md is the complete record for anyone who wants to pick that up.
61
+ * It does not touch grid codes or the dealer password that protects them.
62
+ * It is not affiliated with or endorsed by Victron Energy.
63
+
64
+ ## Status and confidence
65
+
66
+ | Capability | Status | Evidence |
67
+ |---|---|---|
68
+ | Section grammar and integrity checksum | Proven | Validates on every section of all 84 fixture files (107 counting archive duplicates); edited files accepted by the device on 4 systems |
69
+ | Settings array = VE.Bus setting IDs at +0x59 + 2n | High | Reference IDs reproduce 120 V output, 50.0 A limit, 95 %/98 % SoC, grid-code flag on all 162 blocks of the 81 well-formed fixtures |
70
+ | Field table (190 entries) | Partial | 4 CONFIRMED, 10 HIGH, 9 MEDIUM, 19 LOW, 20 UNKNOWN named; the rest unnamed |
71
+ | Guarded writer (`mk2vsc set`) | Proven live | Absorption, float and Virtual Switch thresholds written and read back on 4 systems, July to August 2026 |
72
+ | By-serial diff (`mk2vsc diff`) | Proven | Consecutive downloads, including a pair whose blocks swapped position, classify as bookkeeping only |
73
+ | Qualifier (`mk2vsc qualify`) | Proven | Reproduces the incident that motivated it (a rollback that reverted a charge-voltage fix) |
74
+ | Assistant area | Read only | Record structure and stub signature recognised; record bodies not understood |
75
+ | Upload-form (GUI export) files | Read only | Detected and decoded; the writer refuses them |
76
+ | ESS injection (`mk2vsc experimental`) | Experimental, never ran | Graft and device-to-upload-form transform reproduce the August 2026 files byte-for-byte; the device stored them, the system never started |
77
+
78
+ The confidence vocabulary (CONFIRMED, HIGH, MEDIUM, LOW, UNKNOWN) is defined in `mk2vsc/fields.py` and
79
+ docs/FIELDS.md. The writer edits CONFIRMED and HIGH fields; anything lower needs an explicit override.
80
+
81
+ ## Install
82
+
83
+ ```
84
+ git clone <this repository>
85
+ cd mk2vsc
86
+ python3 -m venv .venv && .venv/bin/pip install -e ".[test]"
87
+ .venv/bin/pytest # 468 tests against the fixture corpus
88
+ ```
89
+
90
+ Or run without installing: `PYTHONPATH=. python3 -m mk2vsc.cli ...`.
91
+
92
+ ## Quickstart
93
+
94
+ ```
95
+ mk2vsc info fixtures/mango/mango_2026-07-24_download_bare_deviceform_1.rvms
96
+ ```
97
+ ```
98
+ format 1.33 length 5055 checksums OK
99
+ HQ24149MY9U fw 2729560 form=device flag=f5 saved 2026-07-24T22:49:20+00:00 assistant: no assistant
100
+ flags0 33268 bitmask [HIGH] (+0x059)
101
+ flags1 19966 bitmask [HIGH] (+0x05b)
102
+ absorption_V 56.8 V [CONFIRMED] (+0x05d)
103
+ float_V 54.0 V [CONFIRMED] (+0x05f)
104
+ charge_current_A 35 A [HIGH] (+0x061)
105
+ ...
106
+ ```
107
+
108
+ Two downloads of the same system a few minutes apart differ only in bookkeeping (pointer, save
109
+ timestamp, checksum), even though the two inverter blocks may have swapped position in the file:
110
+
111
+ ```
112
+ mk2vsc diff fixtures/mango/mango_2026-07-24_download_bare_deviceform_1.rvms \
113
+ fixtures/mango/mango_2026-07-24_download_bare_deviceform_2.rvms
114
+ ```
115
+ ```
116
+ lengths 5055 -> 5055; prologue same; verdict: ONLY BOOKKEEPING (settings verbatim)
117
+ HQ2240FKJDE: len 482->482 form device->device bookkeeping=6B header=0B assistant=0B
118
+ HQ24149MY9U: len 482->482 form device->device bookkeeping=6B header=0B assistant=0B
119
+ ```
120
+
121
+ Edit a setting on every inverter, then check the result against what you intended:
122
+
123
+ ```
124
+ mk2vsc set fixtures/guava/guava_2026-07-20_download_bare_deviceform_1.rvms /tmp/prepared.rvms \
125
+ absorption_V=56.8 float_V=54.0
126
+ mk2vsc qualify /tmp/prepared.rvms --intent examples/intent.example.json
127
+ ```
128
+ ```
129
+ HQ2414U6FVN absorption_V 56.0 -> 56.8 V (+0x05d / file 0x1056)
130
+ HQ2414AXENJ absorption_V 57.6 -> 56.8 V (+0x05d / file 0x123a)
131
+ ...
132
+ wrote /tmp/prepared.rvms; verified: only the listed bytes and their section checksums changed
133
+ /tmp/prepared.rvms: QUALIFIED
134
+ ok all section checksums valid
135
+ ok serials match the intended system
136
+ ok absorption_V = 56.8 on all inverters
137
+ ok float_V = 54.0 on all inverters
138
+ ```
139
+
140
+ Every command: `info`, `validate`, `decode`, `diff`, `set`, `qualify`, `fix`, `fields`, `census`,
141
+ `history`. `mk2vsc --help` and `mk2vsc <command> --help` describe the options.
142
+
143
+ From Python:
144
+
145
+ ```python
146
+ from mk2vsc import RvmsFile, units_by_serial, set_settings, diff_bytes
147
+
148
+ data = open("download.rvms", "rb").read()
149
+ print(units_by_serial(RvmsFile.parse(data))["HQ2414U6FVN"].setting(2) / 100) # absorption, volts
150
+ out, edits = set_settings(data, [(None, "absorption_V", 56.8)]) # None = every inverter
151
+ assert not diff_bytes(data, out).only_bookkeeping # the setting changed
152
+ open("prepared.rvms", "wb").write(out)
153
+ ```
154
+
155
+ See examples/edit_and_verify.py for the full loop.
156
+
157
+ ## The change-control loop
158
+
159
+ Uploading a file replaces the whole configuration of every inverter in the system. These five steps
160
+ are how we make that safe; docs/CHANGE_CONTROL.md explains each one and the incident behind it.
161
+
162
+ 1. Download a fresh file from VRM (Remote VEConfigure) into `00_baseline/`. Never start from an
163
+ archived copy: the device rejects stale save timestamps, and old files carry old values.
164
+ 2. `mk2vsc set` the baseline into `01_prepared/`, then `mk2vsc qualify` it against an intent file that
165
+ lives outside the file under test.
166
+ 3. Upload `01_prepared/` through VRM.
167
+ 4. Download again into `02_downloaded/`.
168
+ 5. `mk2vsc diff` prepared against downloaded (expect "ONLY BOOKKEEPING") and `mk2vsc qualify` the
169
+ download. "Success" in the upload dialog is not the same as "the settings are right".
170
+
171
+ ## Corpus and tests
172
+
173
+ The `fixtures/` directory holds 84 unique files from 4 split-phase MultiPlus systems (8 inverters,
174
+ firmware 2729560, format version 1.33) collected between June and September 2026, including device
175
+ downloads, GUI exports, files our tools produced, and three deliberately broken files kept as negative
176
+ controls. `fixtures/manifest.json` records each file's hash, origin, state and inverters. The tests in
177
+ `tests/` check structure, checksums, byte-exact round trips, every documented field claim, the writer,
178
+ the diff, the qualifier and the CLI against that corpus. docs/QA.md describes how to verify the same
179
+ things on your own system before trusting the tool with it.
180
+
181
+ ## Documentation
182
+
183
+ | File | Contents |
184
+ |---|---|
185
+ | docs/FORMAT.md | The file format as we understand it: sections, checksum, unit block layout, device vs upload form, assistant area |
186
+ | docs/FIELDS.md | The settings table: every field's offset, label, scale, confidence, presumed purpose and evidence |
187
+ | docs/CHANGE_CONTROL.md | The baseline / prepared / downloaded pattern, the rules, and the incidents that produced them |
188
+ | docs/WORKFLOW.md | Working with VRM Remote VEConfigure, and what still needs the Windows GUI |
189
+ | docs/SAFETY.md | Responsible use, the proven-safe surface, recovery, first-use protocol |
190
+ | docs/QA.md | How to decide whether to trust this: the test suite, the corpus, and a verify-it-yourself recipe |
191
+ | docs/ASSISTANTS.md | What we know and do not know about ESS and other assistants in the file |
192
+ | docs/ERRORS.md | What mk2vsc-36, mk2vsc-47, mk2vsc-49, Error 1303 and the VE.Bus errors mean |
193
+ | docs/HISTORY.md | How this came to be, in order, including the things we got wrong |
194
+ | docs/ESS_INJECTION.md | The ESS-by-file experiment in full: what a GUI install writes, every attempt, hypotheses, the next test |
195
+ | docs/FIXTURES.md | What every file in the corpus is |
196
+ | docs/PRACTICES.md | How the project is run: public record, evidence rules, safety rules, AI-assistance disclosure |
197
+
198
+ ## Limits and unknowns
199
+
200
+ * We hold files from one firmware (2729560), one format version (1.33), one product family, one
201
+ topology (two inverters, split phase). Other hardware may differ; the tests will tell you.
202
+ * We have no `.rvsc` single-unit files and no three-phase or three-plus-unit files.
203
+ * About two thirds of the settings array is unnamed or named with low confidence. docs/FIELDS.md
204
+ lists what each value looks like even where we cannot say what it does.
205
+ * The assistant record bodies, the 4001-byte BareSettingInfo section and parts of the block header
206
+ are not understood. docs/FORMAT.md keeps an explicit Observed / Inferred / Unknown list.
207
+ * Installing an assistant by file has never produced a running system for us. docs/ASSISTANTS.md
208
+ records each attempt and its outcome so nobody has to repeat them on live hardware.
209
+
210
+ ## How to help
211
+
212
+ The most useful contributions are files and controlled pairs, not code:
213
+
214
+ * A download, one setting changed in VEConfigure, and a second download, plus a screenshot of the
215
+ VEConfigure tab showing the value. One such pair names a field for everyone.
216
+ * Files from other hardware: Quattro, other firmware, three-phase, single-unit `.rvsc`.
217
+ * Running the verify-it-yourself recipe in docs/QA.md on your system and reporting what happened.
218
+
219
+ See CONTRIBUTING.md for how to add a fixture and what the privacy expectations are.
220
+
221
+ ## License and responsible use
222
+
223
+ MIT, see LICENSE. This tool is for people who are already responsible for, and authorized to
224
+ configure, the systems they apply it to. It produces files; the decision to upload one, and the
225
+ consequences on a live battery system, remain yours. Read docs/SAFETY.md first.
226
+
227
+ ## How this project is run
228
+
229
+ Every change goes through a public pull request, every open question is a labelled issue, and every
230
+ format claim is tied to a test on real files. The project is developed with AI assistance, disclosed
231
+ in commits and in docs/PRACTICES.md.
232
+
233
+ ## Acknowledgements
234
+
235
+ * github.com/xcellsior/ve-bus-programming documented the VE.Bus setting IDs and scales over the MK2/MK3
236
+ protocol; that table is what let us name most of the settings array.
237
+ * The Victron Community threads on `.rvsc`/`.rvms` files, Remote VEConfigure and the "switch as group"
238
+ error saved us time and confirmed the demand for this work.
mk2vsc-0.1.2/README.md ADDED
@@ -0,0 +1,217 @@
1
+ # mk2vsc
2
+
3
+ Read, validate, decode, diff, edit and qualify Victron VEConfigure `.rvms` configuration files
4
+ without VEConfigure, on any operating system, from Python or the command line.
5
+
6
+ ## The problem
7
+
8
+ Victron MultiPlus and Quattro inverter/chargers are configured with VEConfigure and VE.Bus System
9
+ Configurator. Those tools run only on Windows. VictronConnect is replacing them but does not support
10
+ assistants (ESS, AC PV and the rest), so anyone maintaining a real installation still needs a Windows
11
+ machine or a virtual machine. The configuration itself travels as a small binary file: `.rvsc` for a
12
+ single unit, `.rvms` for parallel and multi-phase systems, downloaded and uploaded through VRM's Remote
13
+ VEConfigure. A Victron Community thread titled "RVSC File Format Specification" asked Victron to publish
14
+ the format so people could write their own editors; it received no answer. As of September 2026 we
15
+ found no open-source parser, specification or editor for these files anywhere.
16
+
17
+ We operate four two-inverter systems and needed to change charge voltages and Virtual Switch
18
+ thresholds on them remotely, repeatably, and with a record of what changed. This repository is what we
19
+ built to do that, together with everything we learned about the file along the way.
20
+
21
+ ## What this is
22
+
23
+ * A zero-dependency Python 3.9+ library and a CLI (`mk2vsc`) that:
24
+ * parses the file's section structure and verifies every integrity checksum,
25
+ * decodes the per-inverter settings array into labelled values with a confidence level per field,
26
+ * compares two files by inverter serial and tells you whether they differ only in bookkeeping,
27
+ * edits settings in place, self-verifies that nothing else changed, and never changes file length,
28
+ * qualifies a file against the values you intended before you upload and after you re-download,
29
+ * mines a library of archived downloads into a dated, per-inverter change log (`mk2vsc history`).
30
+ * A corpus of 84 real device files with a manifest, and a test suite that checks every documented
31
+ claim against that corpus (468 tests).
32
+ * A written account of the format as we understand it, and of what we do not understand.
33
+
34
+ ## What this is not
35
+
36
+ * It does not upload anything. You upload through VRM, exactly as before.
37
+ * It does not install assistants (ESS and others) as a supported operation. The experimental graft and
38
+ upload-form transform are included under `mk2vsc.experimental`, gated, and have never produced a running
39
+ system; docs/ESS_INJECTION.md is the complete record for anyone who wants to pick that up.
40
+ * It does not touch grid codes or the dealer password that protects them.
41
+ * It is not affiliated with or endorsed by Victron Energy.
42
+
43
+ ## Status and confidence
44
+
45
+ | Capability | Status | Evidence |
46
+ |---|---|---|
47
+ | Section grammar and integrity checksum | Proven | Validates on every section of all 84 fixture files (107 counting archive duplicates); edited files accepted by the device on 4 systems |
48
+ | Settings array = VE.Bus setting IDs at +0x59 + 2n | High | Reference IDs reproduce 120 V output, 50.0 A limit, 95 %/98 % SoC, grid-code flag on all 162 blocks of the 81 well-formed fixtures |
49
+ | Field table (190 entries) | Partial | 4 CONFIRMED, 10 HIGH, 9 MEDIUM, 19 LOW, 20 UNKNOWN named; the rest unnamed |
50
+ | Guarded writer (`mk2vsc set`) | Proven live | Absorption, float and Virtual Switch thresholds written and read back on 4 systems, July to August 2026 |
51
+ | By-serial diff (`mk2vsc diff`) | Proven | Consecutive downloads, including a pair whose blocks swapped position, classify as bookkeeping only |
52
+ | Qualifier (`mk2vsc qualify`) | Proven | Reproduces the incident that motivated it (a rollback that reverted a charge-voltage fix) |
53
+ | Assistant area | Read only | Record structure and stub signature recognised; record bodies not understood |
54
+ | Upload-form (GUI export) files | Read only | Detected and decoded; the writer refuses them |
55
+ | ESS injection (`mk2vsc experimental`) | Experimental, never ran | Graft and device-to-upload-form transform reproduce the August 2026 files byte-for-byte; the device stored them, the system never started |
56
+
57
+ The confidence vocabulary (CONFIRMED, HIGH, MEDIUM, LOW, UNKNOWN) is defined in `mk2vsc/fields.py` and
58
+ docs/FIELDS.md. The writer edits CONFIRMED and HIGH fields; anything lower needs an explicit override.
59
+
60
+ ## Install
61
+
62
+ ```
63
+ git clone <this repository>
64
+ cd mk2vsc
65
+ python3 -m venv .venv && .venv/bin/pip install -e ".[test]"
66
+ .venv/bin/pytest # 468 tests against the fixture corpus
67
+ ```
68
+
69
+ Or run without installing: `PYTHONPATH=. python3 -m mk2vsc.cli ...`.
70
+
71
+ ## Quickstart
72
+
73
+ ```
74
+ mk2vsc info fixtures/mango/mango_2026-07-24_download_bare_deviceform_1.rvms
75
+ ```
76
+ ```
77
+ format 1.33 length 5055 checksums OK
78
+ HQ24149MY9U fw 2729560 form=device flag=f5 saved 2026-07-24T22:49:20+00:00 assistant: no assistant
79
+ flags0 33268 bitmask [HIGH] (+0x059)
80
+ flags1 19966 bitmask [HIGH] (+0x05b)
81
+ absorption_V 56.8 V [CONFIRMED] (+0x05d)
82
+ float_V 54.0 V [CONFIRMED] (+0x05f)
83
+ charge_current_A 35 A [HIGH] (+0x061)
84
+ ...
85
+ ```
86
+
87
+ Two downloads of the same system a few minutes apart differ only in bookkeeping (pointer, save
88
+ timestamp, checksum), even though the two inverter blocks may have swapped position in the file:
89
+
90
+ ```
91
+ mk2vsc diff fixtures/mango/mango_2026-07-24_download_bare_deviceform_1.rvms \
92
+ fixtures/mango/mango_2026-07-24_download_bare_deviceform_2.rvms
93
+ ```
94
+ ```
95
+ lengths 5055 -> 5055; prologue same; verdict: ONLY BOOKKEEPING (settings verbatim)
96
+ HQ2240FKJDE: len 482->482 form device->device bookkeeping=6B header=0B assistant=0B
97
+ HQ24149MY9U: len 482->482 form device->device bookkeeping=6B header=0B assistant=0B
98
+ ```
99
+
100
+ Edit a setting on every inverter, then check the result against what you intended:
101
+
102
+ ```
103
+ mk2vsc set fixtures/guava/guava_2026-07-20_download_bare_deviceform_1.rvms /tmp/prepared.rvms \
104
+ absorption_V=56.8 float_V=54.0
105
+ mk2vsc qualify /tmp/prepared.rvms --intent examples/intent.example.json
106
+ ```
107
+ ```
108
+ HQ2414U6FVN absorption_V 56.0 -> 56.8 V (+0x05d / file 0x1056)
109
+ HQ2414AXENJ absorption_V 57.6 -> 56.8 V (+0x05d / file 0x123a)
110
+ ...
111
+ wrote /tmp/prepared.rvms; verified: only the listed bytes and their section checksums changed
112
+ /tmp/prepared.rvms: QUALIFIED
113
+ ok all section checksums valid
114
+ ok serials match the intended system
115
+ ok absorption_V = 56.8 on all inverters
116
+ ok float_V = 54.0 on all inverters
117
+ ```
118
+
119
+ Every command: `info`, `validate`, `decode`, `diff`, `set`, `qualify`, `fix`, `fields`, `census`,
120
+ `history`. `mk2vsc --help` and `mk2vsc <command> --help` describe the options.
121
+
122
+ From Python:
123
+
124
+ ```python
125
+ from mk2vsc import RvmsFile, units_by_serial, set_settings, diff_bytes
126
+
127
+ data = open("download.rvms", "rb").read()
128
+ print(units_by_serial(RvmsFile.parse(data))["HQ2414U6FVN"].setting(2) / 100) # absorption, volts
129
+ out, edits = set_settings(data, [(None, "absorption_V", 56.8)]) # None = every inverter
130
+ assert not diff_bytes(data, out).only_bookkeeping # the setting changed
131
+ open("prepared.rvms", "wb").write(out)
132
+ ```
133
+
134
+ See examples/edit_and_verify.py for the full loop.
135
+
136
+ ## The change-control loop
137
+
138
+ Uploading a file replaces the whole configuration of every inverter in the system. These five steps
139
+ are how we make that safe; docs/CHANGE_CONTROL.md explains each one and the incident behind it.
140
+
141
+ 1. Download a fresh file from VRM (Remote VEConfigure) into `00_baseline/`. Never start from an
142
+ archived copy: the device rejects stale save timestamps, and old files carry old values.
143
+ 2. `mk2vsc set` the baseline into `01_prepared/`, then `mk2vsc qualify` it against an intent file that
144
+ lives outside the file under test.
145
+ 3. Upload `01_prepared/` through VRM.
146
+ 4. Download again into `02_downloaded/`.
147
+ 5. `mk2vsc diff` prepared against downloaded (expect "ONLY BOOKKEEPING") and `mk2vsc qualify` the
148
+ download. "Success" in the upload dialog is not the same as "the settings are right".
149
+
150
+ ## Corpus and tests
151
+
152
+ The `fixtures/` directory holds 84 unique files from 4 split-phase MultiPlus systems (8 inverters,
153
+ firmware 2729560, format version 1.33) collected between June and September 2026, including device
154
+ downloads, GUI exports, files our tools produced, and three deliberately broken files kept as negative
155
+ controls. `fixtures/manifest.json` records each file's hash, origin, state and inverters. The tests in
156
+ `tests/` check structure, checksums, byte-exact round trips, every documented field claim, the writer,
157
+ the diff, the qualifier and the CLI against that corpus. docs/QA.md describes how to verify the same
158
+ things on your own system before trusting the tool with it.
159
+
160
+ ## Documentation
161
+
162
+ | File | Contents |
163
+ |---|---|
164
+ | docs/FORMAT.md | The file format as we understand it: sections, checksum, unit block layout, device vs upload form, assistant area |
165
+ | docs/FIELDS.md | The settings table: every field's offset, label, scale, confidence, presumed purpose and evidence |
166
+ | docs/CHANGE_CONTROL.md | The baseline / prepared / downloaded pattern, the rules, and the incidents that produced them |
167
+ | docs/WORKFLOW.md | Working with VRM Remote VEConfigure, and what still needs the Windows GUI |
168
+ | docs/SAFETY.md | Responsible use, the proven-safe surface, recovery, first-use protocol |
169
+ | docs/QA.md | How to decide whether to trust this: the test suite, the corpus, and a verify-it-yourself recipe |
170
+ | docs/ASSISTANTS.md | What we know and do not know about ESS and other assistants in the file |
171
+ | docs/ERRORS.md | What mk2vsc-36, mk2vsc-47, mk2vsc-49, Error 1303 and the VE.Bus errors mean |
172
+ | docs/HISTORY.md | How this came to be, in order, including the things we got wrong |
173
+ | docs/ESS_INJECTION.md | The ESS-by-file experiment in full: what a GUI install writes, every attempt, hypotheses, the next test |
174
+ | docs/FIXTURES.md | What every file in the corpus is |
175
+ | docs/PRACTICES.md | How the project is run: public record, evidence rules, safety rules, AI-assistance disclosure |
176
+
177
+ ## Limits and unknowns
178
+
179
+ * We hold files from one firmware (2729560), one format version (1.33), one product family, one
180
+ topology (two inverters, split phase). Other hardware may differ; the tests will tell you.
181
+ * We have no `.rvsc` single-unit files and no three-phase or three-plus-unit files.
182
+ * About two thirds of the settings array is unnamed or named with low confidence. docs/FIELDS.md
183
+ lists what each value looks like even where we cannot say what it does.
184
+ * The assistant record bodies, the 4001-byte BareSettingInfo section and parts of the block header
185
+ are not understood. docs/FORMAT.md keeps an explicit Observed / Inferred / Unknown list.
186
+ * Installing an assistant by file has never produced a running system for us. docs/ASSISTANTS.md
187
+ records each attempt and its outcome so nobody has to repeat them on live hardware.
188
+
189
+ ## How to help
190
+
191
+ The most useful contributions are files and controlled pairs, not code:
192
+
193
+ * A download, one setting changed in VEConfigure, and a second download, plus a screenshot of the
194
+ VEConfigure tab showing the value. One such pair names a field for everyone.
195
+ * Files from other hardware: Quattro, other firmware, three-phase, single-unit `.rvsc`.
196
+ * Running the verify-it-yourself recipe in docs/QA.md on your system and reporting what happened.
197
+
198
+ See CONTRIBUTING.md for how to add a fixture and what the privacy expectations are.
199
+
200
+ ## License and responsible use
201
+
202
+ MIT, see LICENSE. This tool is for people who are already responsible for, and authorized to
203
+ configure, the systems they apply it to. It produces files; the decision to upload one, and the
204
+ consequences on a live battery system, remain yours. Read docs/SAFETY.md first.
205
+
206
+ ## How this project is run
207
+
208
+ Every change goes through a public pull request, every open question is a labelled issue, and every
209
+ format claim is tied to a test on real files. The project is developed with AI assistance, disclosed
210
+ in commits and in docs/PRACTICES.md.
211
+
212
+ ## Acknowledgements
213
+
214
+ * github.com/xcellsior/ve-bus-programming documented the VE.Bus setting IDs and scales over the MK2/MK3
215
+ protocol; that table is what let us name most of the settings array.
216
+ * The Victron Community threads on `.rvsc`/`.rvms` files, Remote VEConfigure and the "switch as group"
217
+ error saved us time and confirmed the demand for this work.
@@ -0,0 +1,35 @@
1
+ """
2
+ rvms -- read, verify, edit and diff Victron VEConfigure ``.rvms`` configuration files without VEConfigure.
3
+
4
+ Zero dependencies. Python 3.9+.
5
+
6
+ Quick tour::
7
+
8
+ from mk2vsc import RvmsFile, units_by_serial, decode_file, set_settings, diff_files
9
+
10
+ f = RvmsFile.load("system.rvms")
11
+ f.all_checksums_ok # every section's integrity trailer validates
12
+ units_by_serial(f)["HQ2414U6FVN"].setting(2) / 100 # absorption voltage
13
+
14
+ Safety model in one paragraph: this library produces *files*. It never talks to an inverter. A valid
15
+ file is necessary, not sufficient: editing the right offset is on you (see ``fields.py`` confidence
16
+ levels), and the only proven-safe edits are length-preserving value changes to the settings array.
17
+ Adding, removing or transplanting an assistant (ESS etc.) by file has never worked for us and has
18
+ disrupted live systems. Read docs/SAFETY.md before uploading anything.
19
+ """
20
+ from .sections import RvmsFile, Section, RvmsParseError, sum32_le, scan_unit_blocks
21
+ from .units import UnitBlock, unit_blocks, units_by_serial
22
+ from .fields import FIELDS, BY_ID, BY_NAME, lookup, Field
23
+ from .decode import decode_file, decode_bytes
24
+ from .writer import set_settings, WriteRefused
25
+ from .diff import diff_files, diff_bytes
26
+ from .qualify import qualify_file, Intent
27
+
28
+ __version__ = "0.1.2"
29
+ __all__ = [
30
+ "RvmsFile", "Section", "RvmsParseError", "sum32_le", "scan_unit_blocks",
31
+ "UnitBlock", "unit_blocks", "units_by_serial",
32
+ "FIELDS", "BY_ID", "BY_NAME", "lookup", "Field",
33
+ "decode_file", "decode_bytes", "set_settings", "WriteRefused", "diff_files", "diff_bytes",
34
+ "qualify_file", "Intent",
35
+ ]
@@ -0,0 +1,105 @@
1
+ """
2
+ READ-ONLY parsing of the assistant area that follows the settings array in a unit block.
3
+
4
+ What we can say with evidence (docs/ASSISTANTS.md has the full story):
5
+
6
+ * Bare block (no assistant): the area is the 9 bytes ``ff ff ff ff 00 00 ff 00 0b`` (device form) --
7
+ i.e. an empty record header (``ff ff`` marker, ``ff ff`` subtype, length ``00 00``) plus 3 trailer bytes.
8
+ * Block with the ESS assistant installed by the GUI: one or more records framed
9
+ ``f5 ff <subtype u16> <len u16> <body>``; body lengths 704 and 1152 in every working install we hold
10
+ (one per inverter of the pair; the two bodies differ from each other, and are byte-identical across
11
+ systems except for a single primary/secondary flag byte). The device pads records with ``0xff`` runs
12
+ and appends trailer bytes; the GUI's upload form writes the same records compact.
13
+ * Stub: after VEConfigure accepted one of our transplanted files it wrote a 64-byte empty container
14
+ ``40 00 a7 fe 00 00 57 01`` + ``0xff`` filler + ``c0 0a`` on both inverters and discarded our payload.
15
+ Its presence in a download is the signature of a failed by-file assistant install.
16
+
17
+ We do NOT understand the record body. It looks like a compiled program (entropy ~6.2 bits/byte,
18
+ recurring 2-3 byte opcodes, embedded parameter values such as 48.00 V and 10 %). This module reports
19
+ structure; it does not author it.
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import struct
24
+ from typing import Dict, List
25
+
26
+ from .units import UnitBlock
27
+
28
+ RECORD_MARK = b"\xf5\xff"
29
+ EMPTY_MARK = b"\xff\xff"
30
+ CONTAINER_SIG = b"\xa7\xfe\x00\x00\x57\x01"
31
+ STUB_MAGIC = b"\x40\x00" + CONTAINER_SIG # len 64 + signature, as it appears after the ff ff ff ff header
32
+
33
+
34
+ def parse_records(area: bytes):
35
+ """Walk ``marker(2) subtype(2) len(2) body`` records from the start of the area.
36
+
37
+ Returns (records, tail_offset). Walking stops at the first byte pair that is not a known marker.
38
+ """
39
+ records: List[Dict] = []
40
+ pos = 0
41
+ while pos + 6 <= len(area) and area[pos: pos + 2] in (EMPTY_MARK, RECORD_MARK):
42
+ marker = area[pos: pos + 2]
43
+ subtype, length = struct.unpack_from("<HH", area, pos + 2)
44
+ body = area[pos + 6: pos + 6 + length]
45
+ records.append({"offset": pos, "marker": marker.hex(), "subtype": f"{subtype:04x}", "length": length,
46
+ "body_sha8": _sha8(body) if length else "", "nonpad_bytes": sum(1 for b in body if b != 0xFF),
47
+ "container_signature": body.startswith(CONTAINER_SIG),
48
+ "truncated": len(body) < length})
49
+ pos += 6 + length
50
+ return records, pos
51
+
52
+
53
+ def parse_assistant_area(u: UnitBlock) -> Dict:
54
+ """Describe the assistant area of a unit block.
55
+
56
+ Uniform model (every block in the corpus fits it)::
57
+
58
+ area := record* tail
59
+ record := marker(2) subtype(2) len(2) body[len]
60
+ marker ff ff -> empty slot / container. Bare blocks: len 0. Two June-2026 files from an older
61
+ tool build: len 6, body a7 fe 00 00 57 01. Stub written by VEConfigure after it
62
+ discarded a transplanted assistant: len 64, same signature + 0xff filler.
63
+ f5 ff -> assistant record. GUI-installed ESS: one 704-byte and one 1152-byte record per
64
+ system (one on each inverter), subtype 0101 / 0001.
65
+ tail := padding(0xff)* | ff | u16 free
66
+ On bare, legacy and stub blocks free == 2816 - bytes used; see docs/FORMAT.md for ESS.
67
+
68
+ A ``f5 ff`` header with len 0 where ``ff ff`` is expected is residue seen on downloads taken after a
69
+ rejected or rolled-back assistant upload; functionally bare.
70
+ """
71
+ area = u.assistant_area
72
+ records, tail_off = parse_records(area)
73
+ tail = area[tail_off:]
74
+ out: Dict = {"bytes": len(area), "records": records, "tail_bytes": len(tail), "tail_hex": tail[-8:].hex(" "),
75
+ "stub": False, "kind": "unknown", "summary": ""}
76
+ if len(tail) >= 3 and tail[-3] == 0xFF:
77
+ out["free"] = struct.unpack_from("<H", tail, len(tail) - 2)[0]
78
+ out["used"] = tail_off
79
+ out["free_plus_used"] = out["free"] + tail_off
80
+ if any(r["truncated"] for r in records):
81
+ out["kind"] = "malformed"
82
+ out["summary"] = "record length exceeds the area (malformed)"
83
+ return out
84
+ real = [r for r in records if r["marker"] == "f5ff" and r["length"] > 0]
85
+ containers = [r for r in records if r["marker"] == "ffff" and r["length"] > 0]
86
+ if any(r["length"] >= 64 and r["container_signature"] for r in containers):
87
+ out["kind"], out["stub"] = "stub", True
88
+ out["summary"] = "EMPTY STUB container (signature of a failed by-file install)"
89
+ elif real:
90
+ out["kind"] = "records"
91
+ out["summary"] = "assistant records: " + ", ".join(f"{r['length']}B/{r['subtype']}" for r in real)
92
+ elif containers:
93
+ out["kind"] = "container"
94
+ out["summary"] = f"empty {containers[0]['length']}-byte container (no program)"
95
+ elif records:
96
+ out["kind"] = "none"
97
+ out["summary"] = "no assistant" + (" (empty record residue)" if records[0]["marker"] == "f5ff" else "")
98
+ else:
99
+ out["summary"] = f"{len(area)} unrecognised bytes"
100
+ return out
101
+
102
+
103
+ def _sha8(b: bytes) -> str:
104
+ import hashlib
105
+ return hashlib.sha256(b).hexdigest()[:8]