Pierwszy commit - inicjalizacja projektu

This commit is contained in:
ms
2026-02-08 17:31:44 +01:00
commit 3fd3951aa8
25 changed files with 8468 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
{
"permissions": {
"allow": [
"Bash(bash:*)",
"Bash(./modborno3:*)",
"Bash(timeout 10 python3:*)",
"Bash(timeout 8 python3:*)",
"Bash(for i in 1 2 3)",
"Bash(do echo \"=== Test $i ===\")",
"Bash(done)",
"Bash(1 echo \"Test wykonany. Analiza wyników:\" echo \"\" echo \"✓ POMYŚLNIE ODCZYTANE REJESTRY:\" grep \"OK.*elapsed\" /tmp/sun2k_test.log)",
"Read(//tmp/**)",
"Bash(for i in 1 2)",
"Bash(ls:*)"
]
}
}

611
CONFIG.html Normal file
View File

@@ -0,0 +1,611 @@
<!DOCTYPE html>
<html>
<head>
<title>CONFIG.md</title>
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
<style>
/* https://github.com/microsoft/vscode/blob/master/extensions/markdown-language-features/media/markdown.css */
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
body {
font-family: var(--vscode-markdown-font-family, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif);
font-size: var(--vscode-markdown-font-size, 14px);
padding: 0 26px;
line-height: var(--vscode-markdown-line-height, 22px);
word-wrap: break-word;
}
#code-csp-warning {
position: fixed;
top: 0;
right: 0;
color: white;
margin: 16px;
text-align: center;
font-size: 12px;
font-family: sans-serif;
background-color:#444444;
cursor: pointer;
padding: 6px;
box-shadow: 1px 1px 1px rgba(0,0,0,.25);
}
#code-csp-warning:hover {
text-decoration: none;
background-color:#007acc;
box-shadow: 2px 2px 2px rgba(0,0,0,.25);
}
body.scrollBeyondLastLine {
margin-bottom: calc(100vh - 22px);
}
body.showEditorSelection .code-line {
position: relative;
}
body.showEditorSelection .code-active-line:before,
body.showEditorSelection .code-line:hover:before {
content: "";
display: block;
position: absolute;
top: 0;
left: -12px;
height: 100%;
}
body.showEditorSelection li.code-active-line:before,
body.showEditorSelection li.code-line:hover:before {
left: -30px;
}
.vscode-light.showEditorSelection .code-active-line:before {
border-left: 3px solid rgba(0, 0, 0, 0.15);
}
.vscode-light.showEditorSelection .code-line:hover:before {
border-left: 3px solid rgba(0, 0, 0, 0.40);
}
.vscode-light.showEditorSelection .code-line .code-line:hover:before {
border-left: none;
}
.vscode-dark.showEditorSelection .code-active-line:before {
border-left: 3px solid rgba(255, 255, 255, 0.4);
}
.vscode-dark.showEditorSelection .code-line:hover:before {
border-left: 3px solid rgba(255, 255, 255, 0.60);
}
.vscode-dark.showEditorSelection .code-line .code-line:hover:before {
border-left: none;
}
.vscode-high-contrast.showEditorSelection .code-active-line:before {
border-left: 3px solid rgba(255, 160, 0, 0.7);
}
.vscode-high-contrast.showEditorSelection .code-line:hover:before {
border-left: 3px solid rgba(255, 160, 0, 1);
}
.vscode-high-contrast.showEditorSelection .code-line .code-line:hover:before {
border-left: none;
}
img {
max-width: 100%;
max-height: 100%;
}
a {
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
a:focus,
input:focus,
select:focus,
textarea:focus {
outline: 1px solid -webkit-focus-ring-color;
outline-offset: -1px;
}
hr {
border: 0;
height: 2px;
border-bottom: 2px solid;
}
h1 {
padding-bottom: 0.3em;
line-height: 1.2;
border-bottom-width: 1px;
border-bottom-style: solid;
}
h1, h2, h3 {
font-weight: normal;
}
table {
border-collapse: collapse;
}
table > thead > tr > th {
text-align: left;
border-bottom: 1px solid;
}
table > thead > tr > th,
table > thead > tr > td,
table > tbody > tr > th,
table > tbody > tr > td {
padding: 5px 10px;
}
table > tbody > tr + tr > td {
border-top: 1px solid;
}
blockquote {
margin: 0 7px 0 5px;
padding: 0 16px 0 10px;
border-left-width: 5px;
border-left-style: solid;
}
code {
font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback";
font-size: 1em;
line-height: 1.357em;
}
body.wordWrap pre {
white-space: pre-wrap;
}
pre:not(.hljs),
pre.hljs code > div {
padding: 16px;
border-radius: 3px;
overflow: auto;
}
pre code {
color: var(--vscode-editor-foreground);
tab-size: 4;
}
/** Theming */
.vscode-light pre {
background-color: rgba(220, 220, 220, 0.4);
}
.vscode-dark pre {
background-color: rgba(10, 10, 10, 0.4);
}
.vscode-high-contrast pre {
background-color: rgb(0, 0, 0);
}
.vscode-high-contrast h1 {
border-color: rgb(0, 0, 0);
}
.vscode-light table > thead > tr > th {
border-color: rgba(0, 0, 0, 0.69);
}
.vscode-dark table > thead > tr > th {
border-color: rgba(255, 255, 255, 0.69);
}
.vscode-light h1,
.vscode-light hr,
.vscode-light table > tbody > tr + tr > td {
border-color: rgba(0, 0, 0, 0.18);
}
.vscode-dark h1,
.vscode-dark hr,
.vscode-dark table > tbody > tr + tr > td {
border-color: rgba(255, 255, 255, 0.18);
}
</style>
<style>
/* Tomorrow Theme */
/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */
/* Original theme - https://github.com/chriskempson/tomorrow-theme */
/* Tomorrow Comment */
.hljs-comment,
.hljs-quote {
color: #8e908c;
}
/* Tomorrow Red */
.hljs-variable,
.hljs-template-variable,
.hljs-tag,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class,
.hljs-regexp,
.hljs-deletion {
color: #c82829;
}
/* Tomorrow Orange */
.hljs-number,
.hljs-built_in,
.hljs-builtin-name,
.hljs-literal,
.hljs-type,
.hljs-params,
.hljs-meta,
.hljs-link {
color: #f5871f;
}
/* Tomorrow Yellow */
.hljs-attribute {
color: #eab700;
}
/* Tomorrow Green */
.hljs-string,
.hljs-symbol,
.hljs-bullet,
.hljs-addition {
color: #718c00;
}
/* Tomorrow Blue */
.hljs-title,
.hljs-section {
color: #4271ae;
}
/* Tomorrow Purple */
.hljs-keyword,
.hljs-selector-tag {
color: #8959a8;
}
.hljs {
display: block;
overflow-x: auto;
color: #4d4d4c;
padding: 0.5em;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
</style>
<style>
/*
* Markdown PDF CSS
*/
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif, "Meiryo";
padding: 0 12px;
}
pre {
background-color: #f8f8f8;
border: 1px solid #cccccc;
border-radius: 3px;
overflow-x: auto;
white-space: pre-wrap;
overflow-wrap: break-word;
}
pre:not(.hljs) {
padding: 23px;
line-height: 19px;
}
blockquote {
background: rgba(127, 127, 127, 0.1);
border-color: rgba(0, 122, 204, 0.5);
}
.emoji {
height: 1.4em;
}
code {
font-size: 14px;
line-height: 19px;
}
/* for inline code */
:not(pre):not(.hljs) > code {
color: #C9AE75; /* Change the old color so it seems less like an error */
font-size: inherit;
}
/* Page Break : use <div class="page"/> to insert page break
-------------------------------------------------------- */
.page {
page-break-after: always;
}
</style>
<script src="https://unpkg.com/mermaid/dist/mermaid.min.js"></script>
</head>
<body>
<script>
mermaid.initialize({
startOnLoad: true,
theme: document.body.classList.contains('vscode-dark') || document.body.classList.contains('vscode-high-contrast')
? 'dark'
: 'default'
});
</script>
<h1 id="konfiguracja-programu-modborno3">Konfiguracja programu modb_orno3</h1>
<h2 id="parametry-konfiguracyjne">Parametry konfiguracyjne</h2>
<p>Wszystkie parametry są zdefiniowane na początku pliku <code>modb_orno3.c</code> (linie 11-40):</p>
<pre class="hljs"><code><div><span class="hljs-comment">/* KONFIGURACJA PROGRAMU - EDYTUJ TU */</span>
<span class="hljs-comment">/* Port szeregowy */</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> USB_DEV_DEFAULT <span class="hljs-meta-string">"/dev/ttyUSB0"</span></span>
<span class="hljs-comment">/* Adresy urządzeń Modbus RTU */</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> ORNO_SLAVE_ADR 2</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> SUN2000_SLAVE_ADR 3</span>
<span class="hljs-comment">/* Parametry timing dla ORNO (mikrosekund) */</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> ORNO_RTS_DELAY 5000 <span class="hljs-comment">/* RTS delay: 5ms */</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> ORNO_BYTE_TIMEOUT 2500 <span class="hljs-comment">/* Byte timeout: 2.5ms */</span></span>
<span class="hljs-comment">/* Parametry timing dla SUN2K (mikrosekund) */</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> SUN2K_RTS_DELAY 5000 <span class="hljs-comment">/* RTS delay: 5ms */</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> SUN2K_BYTE_TIMEOUT 2500 <span class="hljs-comment">/* Byte timeout: 2.5ms */</span></span>
<span class="hljs-comment">/* Przerwa między ORNO a SUN2K (mikrosekund) */</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> DELAY_BETWEEN_DEVICES 100000 <span class="hljs-comment">/* 100ms */</span></span>
<span class="hljs-comment">/* MQTT - ustaw 1 aby wyłączyć MQTT */</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> DISABLE_MQTT 0 <span class="hljs-comment">/* 0 = MQTT włączony, 1 = wyłączony */</span></span>
<span class="hljs-comment">/* Broker MQTT */</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> MQTT_BROKER <span class="hljs-meta-string">"10.1.1.1"</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> MQTT_PORT 1883</span>
<span class="hljs-comment">/* Tryb pracy */</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> READ_LOOP false <span class="hljs-comment">/* false = jeden odczyt, true = pętla co 5s */</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> READ_ORNO true <span class="hljs-comment">/* Czy odczytywać ORNO */</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> READ_SUN2K true <span class="hljs-comment">/* Czy odczytywać SUN2K */</span></span>
</div></code></pre>
<h2 id="uruchomienie">Uruchomienie</h2>
<h3 id="kompilacja">Kompilacja:</h3>
<pre class="hljs"><code><div>bash cc-modb_orno3.sh
</div></code></pre>
<h3 id="uruchomienie">Uruchomienie:</h3>
<pre class="hljs"><code><div>./modborno3
</div></code></pre>
<p>Program uruchamia się <strong>bez parametrów</strong> i używa konfiguracji z #define.</p>
<h2 id="sekwencja-dzia%C5%82ania">Sekwencja działania</h2>
<ol>
<li>
<p><strong>Odczyt ORNO</strong> (adres Modbus: 2)</p>
<ul>
<li>Napięcia L1, L2, L3</li>
<li>Prądy L1, L2, L3</li>
<li>Moc Total, L1, L2, L3</li>
<li>Energia Total, L1, L2, L3</li>
<li>Częstotliwość</li>
</ul>
</li>
<li>
<p><strong>Przerwa 100ms</strong></p>
</li>
<li>
<p><strong>Odczyt SUN2K</strong> (adres Modbus: 3)</p>
<ul>
<li>Wszystkie rejestry falownika</li>
<li>Czasy start/shutdown</li>
<li>Statusy</li>
<li>Moce</li>
<li>Energia dzienna i całkowita</li>
</ul>
</li>
</ol>
<h2 id="tematy-mqtt">Tematy MQTT</h2>
<h3 id="orno">ORNO:</h3>
<ul>
<li><code>/energy/orno/U</code> - napięcia (JSON: U1, U2, U3)</li>
<li><code>/energy/orno/I</code> - prądy (JSON: I1, I2, I3)</li>
<li><code>/energy/orno/P</code> - moc (JSON: P_Tot, P1, P2, P3)</li>
<li><code>/energy/orno/W</code> - energia (JSON: W_Tot, W1, W2, W3)</li>
<li><code>/energy/orno/Hz</code> - częstotliwość (wartość)</li>
</ul>
<h3 id="sun2k">SUN2K:</h3>
<ul>
<li><code>/energy/SUN2K</code> - wszystkie parametry (JSON dla każdego rejestru)</li>
</ul>
<h2 id="modyfikacja-konfiguracji">Modyfikacja konfiguracji</h2>
<ol>
<li>Edytuj plik <code>modb_orno3.c</code></li>
<li>Zmień wartości #define</li>
<li>Przekompiluj: <code>bash cc-modb_orno3.sh</code></li>
<li>Uruchom: <code>./modborno3</code></li>
</ol>
<h2 id="przyk%C5%82ady-modyfikacji">Przykłady modyfikacji</h2>
<h3 id="wy%C5%82%C4%85czenie-mqtt">Wyłączenie MQTT:</h3>
<pre class="hljs"><code><div><span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> DISABLE_MQTT 1</span>
</div></code></pre>
<h3 id="zmiana-adresu-brokera">Zmiana adresu brokera:</h3>
<pre class="hljs"><code><div><span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> MQTT_BROKER <span class="hljs-meta-string">"192.168.1.100"</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> MQTT_PORT 1883</span>
</div></code></pre>
<h3 id="tryb-ci%C4%85g%C5%82y-p%C4%99tla-co-5s">Tryb ciągły (pętla co 5s):</h3>
<pre class="hljs"><code><div><span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> READ_LOOP true</span>
</div></code></pre>
<h3 id="tylko-orno-bez-sun2k">Tylko ORNO (bez SUN2K):</h3>
<pre class="hljs"><code><div><span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> READ_SUN2K false</span>
</div></code></pre>
<h3 id="zmiana-przerwy-mi%C4%99dzy-odczytami-200ms">Zmiana przerwy między odczytami (200ms):</h3>
<pre class="hljs"><code><div><span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> DELAY_BETWEEN_DEVICES 200000</span>
</div></code></pre>
<h2 id="walidacja-danych-i-zabezpieczenia-mqtt">Walidacja danych i zabezpieczenia MQTT</h2>
<h3 id="automatyczna-walidacja-warto%C5%9Bci">Automatyczna walidacja wartości</h3>
<p>Program <strong>automatycznie waliduje</strong> wszystkie odczytane wartości przed wysłaniem do MQTT.
Dane są wysyłane do MQTT <strong>tylko</strong> gdy:</p>
<ol>
<li>✅ Odczyt Modbus zakończył się sukcesem (prawidłowa liczba rejestrów)</li>
<li>✅ Wartość nie jest <strong>NaN</strong> (Not a Number)</li>
<li>✅ Wartość nie jest <strong>Inf</strong> (Infinity)</li>
<li>✅ Wartość mieści się w <strong>rozsądnym zakresie</strong></li>
</ol>
<h3 id="zakresy-walidacji-dla-orno">Zakresy walidacji dla ORNO</h3>
<table>
<thead>
<tr>
<th>Parametr</th>
<th>Zakres</th>
<th>Jednostka</th>
<th>Powód</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Napięcia L1/L2/L3</strong></td>
<td>150 - 280</td>
<td>V</td>
<td>Typowy zakres sieci UE (230V ±20%)</td>
</tr>
<tr>
<td><strong>Prądy L1/L2/L3</strong></td>
<td>0 - 100</td>
<td>A</td>
<td>Zabezpieczenie instalacji domowej</td>
</tr>
<tr>
<td><strong>Moc Total/L1/L2/L3</strong></td>
<td>-25000 - +25000</td>
<td>W</td>
<td>Pozwala na generację (wartości ujemne)</td>
</tr>
<tr>
<td><strong>Energia Total/L1/L2/L3</strong></td>
<td>0 - 1000000</td>
<td>kWh</td>
<td>Licznik nie może mieć ujemnej energii</td>
</tr>
<tr>
<td><strong>Częstotliwość</strong></td>
<td>47 - 53</td>
<td>Hz</td>
<td>Typowy zakres sieci UE (50Hz) / US (60Hz)</td>
</tr>
</tbody>
</table>
<h3 id="logi-walidacji">Logi walidacji</h3>
<p>Program wyświetla szczegółowe informacje o walidacji:</p>
<p><strong>Gdy dane są prawidłowe:</strong></p>
<pre class="hljs"><code><div>Voltages: L1=225.8 V, L2=221.3 V, L3=229.8 V
MQTT: Published voltages
</div></code></pre>
<p><strong>Gdy odczyt się nie powiódł:</strong></p>
<pre class="hljs"><code><div>Failed to read currents: Resource temporarily unavailable
(brak komunikatu MQTT - dane nie są wysyłane)
</div></code></pre>
<p><strong>Gdy wartość jest poza zakresem:</strong></p>
<pre class="hljs"><code><div>WARNING: Value 350.5 out of range [150.0, 280.0]
MQTT: Skipping voltages - invalid values
</div></code></pre>
<p><strong>Gdy wartość jest NaN lub Inf:</strong></p>
<pre class="hljs"><code><div>WARNING: Value is NaN
MQTT: Skipping power - invalid values
</div></code></pre>
<h3 id="zabezpieczenia-dla-sun2k">Zabezpieczenia dla SUN2K</h3>
<p>Dla urządzenia SUN2K program:</p>
<ul>
<li><strong>NIE wysyła</strong> danych gdy odczyt się nie powiódł</li>
<li><strong>NIE używa</strong> starych wartości (<code>buf_last_val</code>) po błędzie</li>
<li><strong>Tylko</strong> aktualne, poprawnie odczytane wartości są publikowane</li>
</ul>
<p><strong>Przykład:</strong></p>
<pre class="hljs"><code><div>modbus_read_registers addr=0x7D55 nb=1 -&gt; ERR (Connection timed out)
Frequency: Failed to 11 read: Connection timed out
MQTT: Skipping Frequency - no valid data
</div></code></pre>
<h3 id="dlaczego-to-wa%C5%BCne">Dlaczego to ważne?</h3>
<p><strong>Bez walidacji:</strong></p>
<ul>
<li>Błędne odczyty (CRC errors) mogły wysyłać śmieciowe dane do MQTT</li>
<li>Wartości NaN/Inf mogły powodować błędy w systemach monitoringu</li>
<li>Niemożliwe wartości (np. 10000V) mogły wywoływać fałszywe alarmy</li>
</ul>
<p><strong>Z walidacją:</strong></p>
<ul>
<li>✅ Tylko <strong>poprawne</strong> wartości trafiają do MQTT</li>
<li>✅ System monitoringu dostaje <strong>wiarygodne</strong> dane</li>
<li>✅ Brak <strong>fałszywych alarmów</strong> z powodu błędnych odczytów</li>
<li><strong>Logi</strong> pokazują dokładnie co poszło nie tak</li>
</ul>
<h3 id="kod-walidacji">Kod walidacji</h3>
<p>Funkcja walidacji znajduje się w <a href="modb_orno3.c#L237-L250">modb_orno3.c:237-250</a>:</p>
<pre class="hljs"><code><div><span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">is_valid_float</span><span class="hljs-params">(<span class="hljs-keyword">float</span> value, <span class="hljs-keyword">float</span> min_val, <span class="hljs-keyword">float</span> max_val)</span>
</span>{
<span class="hljs-keyword">if</span> (isnan(value)) {
<span class="hljs-built_in">printf</span>(<span class="hljs-string">"WARNING: Value is NaN\n"</span>);
<span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
}
<span class="hljs-keyword">if</span> (isinf(value)) {
<span class="hljs-built_in">printf</span>(<span class="hljs-string">"WARNING: Value is Inf\n"</span>);
<span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
}
<span class="hljs-keyword">if</span> (value &lt; min_val || value &gt; max_val) {
<span class="hljs-built_in">printf</span>(<span class="hljs-string">"WARNING: Value %.2f out of range [%.2f, %.2f]\n"</span>,
value, min_val, max_val);
<span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
}
<span class="hljs-keyword">return</span> <span class="hljs-number">1</span>;
}
</div></code></pre>
<p>Walidacja jest wywoływana dla każdego pomiaru przed wysłaniem do MQTT.</p>
<h3 id="modyfikacja-zakres%C3%B3w-walidacji">Modyfikacja zakresów walidacji</h3>
<p>Jeśli chcesz zmienić zakresy walidacji (np. dla instalacji przemysłowej):</p>
<ol>
<li>Otwórz plik <code>modb_orno3.c</code></li>
<li>Znajdź sekcję z walidacją (np. dla napięć około linii 370)</li>
<li>Zmień wartości w <code>is_valid_float()</code>:</li>
</ol>
<p><strong>Przykład - zwiększenie zakresu prądów do 200A:</strong></p>
<pre class="hljs"><code><div><span class="hljs-comment">/* Przed: */</span>
<span class="hljs-keyword">if</span> (is_valid_float(I1, <span class="hljs-number">0.0</span>, <span class="hljs-number">100.0</span>) &amp;&amp; ...
<span class="hljs-comment">/* Po: */</span>
<span class="hljs-keyword">if</span> (is_valid_float(I1, <span class="hljs-number">0.0</span>, <span class="hljs-number">200.0</span>) &amp;&amp; ...
</div></code></pre>
<ol start="4">
<li>Przekompiluj: <code>bash cc-modb_orno3.sh</code></li>
</ol>
</body>
</html>

241
CONFIG.md Normal file
View File

@@ -0,0 +1,241 @@
# Konfiguracja programu modb_orno3
## Parametry konfiguracyjne
Wszystkie parametry są zdefiniowane na początku pliku `modb_orno3.c` (linie 11-40):
```c
/* KONFIGURACJA PROGRAMU - EDYTUJ TU */
/* Port szeregowy */
#define USB_DEV_DEFAULT "/dev/ttyUSB0"
/* Adresy urządzeń Modbus RTU */
#define ORNO_SLAVE_ADR 2
#define SUN2000_SLAVE_ADR 3
/* Parametry timing dla ORNO (mikrosekund) */
#define ORNO_RTS_DELAY 5000 /* RTS delay: 5ms */
#define ORNO_BYTE_TIMEOUT 2500 /* Byte timeout: 2.5ms */
/* Parametry timing dla SUN2K (mikrosekund) */
#define SUN2K_RTS_DELAY 5000 /* RTS delay: 5ms */
#define SUN2K_BYTE_TIMEOUT 2500 /* Byte timeout: 2.5ms */
/* Przerwa między ORNO a SUN2K (mikrosekund) */
#define DELAY_BETWEEN_DEVICES 100000 /* 100ms */
/* MQTT - ustaw 1 aby wyłączyć MQTT */
#define DISABLE_MQTT 0 /* 0 = MQTT włączony, 1 = wyłączony */
/* Broker MQTT */
#define MQTT_BROKER "10.1.1.1"
#define MQTT_PORT 1883
/* Tryb pracy */
#define READ_LOOP false /* false = jeden odczyt, true = pętla co 5s */
#define READ_ORNO true /* Czy odczytywać ORNO */
#define READ_SUN2K true /* Czy odczytywać SUN2K */
```
## Uruchomienie
### Kompilacja:
```bash
bash cc-modb_orno3.sh
```
### Uruchomienie:
```bash
./modborno3
```
Program uruchamia się **bez parametrów** i używa konfiguracji z #define.
## Sekwencja działania
1. **Odczyt ORNO** (adres Modbus: 2)
- Napięcia L1, L2, L3
- Prądy L1, L2, L3
- Moc Total, L1, L2, L3
- Energia Total, L1, L2, L3
- Częstotliwość
2. **Przerwa 100ms**
3. **Odczyt SUN2K** (adres Modbus: 3)
- Wszystkie rejestry falownika
- Czasy start/shutdown
- Statusy
- Moce
- Energia dzienna i całkowita
## Tematy MQTT
### ORNO:
- `/energy/orno/U` - napięcia (JSON: U1, U2, U3)
- `/energy/orno/I` - prądy (JSON: I1, I2, I3)
- `/energy/orno/P` - moc (JSON: P_Tot, P1, P2, P3)
- `/energy/orno/W` - energia (JSON: W_Tot, W1, W2, W3)
- `/energy/orno/Hz` - częstotliwość (wartość)
### SUN2K:
- `/energy/SUN2K` - wszystkie parametry (JSON dla każdego rejestru)
## Modyfikacja konfiguracji
1. Edytuj plik `modb_orno3.c`
2. Zmień wartości #define
3. Przekompiluj: `bash cc-modb_orno3.sh`
4. Uruchom: `./modborno3`
## Przykłady modyfikacji
### Wyłączenie MQTT:
```c
#define DISABLE_MQTT 1
```
### Zmiana adresu brokera:
```c
#define MQTT_BROKER "192.168.1.100"
#define MQTT_PORT 1883
```
### Tryb ciągły (pętla co 5s):
```c
#define READ_LOOP true
```
### Tylko ORNO (bez SUN2K):
```c
#define READ_SUN2K false
```
### Zmiana przerwy między odczytami (200ms):
```c
#define DELAY_BETWEEN_DEVICES 200000
```
## Walidacja danych i zabezpieczenia MQTT
### Automatyczna walidacja wartości
Program **automatycznie waliduje** wszystkie odczytane wartości przed wysłaniem do MQTT.
Dane są wysyłane do MQTT **tylko** gdy:
1. ✅ Odczyt Modbus zakończył się sukcesem (prawidłowa liczba rejestrów)
2. ✅ Wartość nie jest **NaN** (Not a Number)
3. ✅ Wartość nie jest **Inf** (Infinity)
4. ✅ Wartość mieści się w **rozsądnym zakresie**
### Zakresy walidacji dla ORNO
| Parametr | Zakres | Jednostka | Powód |
|----------|--------|-----------|-------|
| **Napięcia L1/L2/L3** | 150 - 280 | V | Typowy zakres sieci UE (230V ±20%) |
| **Prądy L1/L2/L3** | 0 - 100 | A | Zabezpieczenie instalacji domowej |
| **Moc Total/L1/L2/L3** | -25000 - +25000 | W | Pozwala na generację (wartości ujemne) |
| **Energia Total/L1/L2/L3** | 0 - 1000000 | kWh | Licznik nie może mieć ujemnej energii |
| **Częstotliwość** | 47 - 53 | Hz | Typowy zakres sieci UE (50Hz) / US (60Hz) |
### Logi walidacji
Program wyświetla szczegółowe informacje o walidacji:
**Gdy dane są prawidłowe:**
```
Voltages: L1=225.8 V, L2=221.3 V, L3=229.8 V
MQTT: Published voltages
```
**Gdy odczyt się nie powiódł:**
```
Failed to read currents: Resource temporarily unavailable
(brak komunikatu MQTT - dane nie są wysyłane)
```
**Gdy wartość jest poza zakresem:**
```
WARNING: Value 350.5 out of range [150.0, 280.0]
MQTT: Skipping voltages - invalid values
```
**Gdy wartość jest NaN lub Inf:**
```
WARNING: Value is NaN
MQTT: Skipping power - invalid values
```
### Zabezpieczenia dla SUN2K
Dla urządzenia SUN2K program:
-**NIE wysyła** danych gdy odczyt się nie powiódł
-**NIE używa** starych wartości (`buf_last_val`) po błędzie
-**Tylko** aktualne, poprawnie odczytane wartości są publikowane
**Przykład:**
```
modbus_read_registers addr=0x7D55 nb=1 -> ERR (Connection timed out)
Frequency: Failed to 11 read: Connection timed out
MQTT: Skipping Frequency - no valid data
```
### Dlaczego to ważne?
**Bez walidacji:**
- Błędne odczyty (CRC errors) mogły wysyłać śmieciowe dane do MQTT
- Wartości NaN/Inf mogły powodować błędy w systemach monitoringu
- Niemożliwe wartości (np. 10000V) mogły wywoływać fałszywe alarmy
**Z walidacją:**
- ✅ Tylko **poprawne** wartości trafiają do MQTT
- ✅ System monitoringu dostaje **wiarygodne** dane
- ✅ Brak **fałszywych alarmów** z powodu błędnych odczytów
-**Logi** pokazują dokładnie co poszło nie tak
### Kod walidacji
Funkcja walidacji znajduje się w [modb_orno3.c:237-250](modb_orno3.c#L237-L250):
```c
int is_valid_float(float value, float min_val, float max_val)
{
if (isnan(value)) {
printf("WARNING: Value is NaN\n");
return 0;
}
if (isinf(value)) {
printf("WARNING: Value is Inf\n");
return 0;
}
if (value < min_val || value > max_val) {
printf("WARNING: Value %.2f out of range [%.2f, %.2f]\n",
value, min_val, max_val);
return 0;
}
return 1;
}
```
Walidacja jest wywoływana dla każdego pomiaru przed wysłaniem do MQTT.
### Modyfikacja zakresów walidacji
Jeśli chcesz zmienić zakresy walidacji (np. dla instalacji przemysłowej):
1. Otwórz plik `modb_orno3.c`
2. Znajdź sekcję z walidacją (np. dla napięć około linii 370)
3. Zmień wartości w `is_valid_float()`:
**Przykład - zwiększenie zakresu prądów do 200A:**
```c
/* Przed: */
if (is_valid_float(I1, 0.0, 100.0) && ...
/* Po: */
if (is_valid_float(I1, 0.0, 200.0) && ...
```
4. Przekompiluj: `bash cc-modb_orno3.sh`

611
CONFIG_tmp.html Normal file
View File

@@ -0,0 +1,611 @@
<!DOCTYPE html>
<html>
<head>
<title>CONFIG.md</title>
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
<style>
/* https://github.com/microsoft/vscode/blob/master/extensions/markdown-language-features/media/markdown.css */
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
body {
font-family: var(--vscode-markdown-font-family, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif);
font-size: var(--vscode-markdown-font-size, 14px);
padding: 0 26px;
line-height: var(--vscode-markdown-line-height, 22px);
word-wrap: break-word;
}
#code-csp-warning {
position: fixed;
top: 0;
right: 0;
color: white;
margin: 16px;
text-align: center;
font-size: 12px;
font-family: sans-serif;
background-color:#444444;
cursor: pointer;
padding: 6px;
box-shadow: 1px 1px 1px rgba(0,0,0,.25);
}
#code-csp-warning:hover {
text-decoration: none;
background-color:#007acc;
box-shadow: 2px 2px 2px rgba(0,0,0,.25);
}
body.scrollBeyondLastLine {
margin-bottom: calc(100vh - 22px);
}
body.showEditorSelection .code-line {
position: relative;
}
body.showEditorSelection .code-active-line:before,
body.showEditorSelection .code-line:hover:before {
content: "";
display: block;
position: absolute;
top: 0;
left: -12px;
height: 100%;
}
body.showEditorSelection li.code-active-line:before,
body.showEditorSelection li.code-line:hover:before {
left: -30px;
}
.vscode-light.showEditorSelection .code-active-line:before {
border-left: 3px solid rgba(0, 0, 0, 0.15);
}
.vscode-light.showEditorSelection .code-line:hover:before {
border-left: 3px solid rgba(0, 0, 0, 0.40);
}
.vscode-light.showEditorSelection .code-line .code-line:hover:before {
border-left: none;
}
.vscode-dark.showEditorSelection .code-active-line:before {
border-left: 3px solid rgba(255, 255, 255, 0.4);
}
.vscode-dark.showEditorSelection .code-line:hover:before {
border-left: 3px solid rgba(255, 255, 255, 0.60);
}
.vscode-dark.showEditorSelection .code-line .code-line:hover:before {
border-left: none;
}
.vscode-high-contrast.showEditorSelection .code-active-line:before {
border-left: 3px solid rgba(255, 160, 0, 0.7);
}
.vscode-high-contrast.showEditorSelection .code-line:hover:before {
border-left: 3px solid rgba(255, 160, 0, 1);
}
.vscode-high-contrast.showEditorSelection .code-line .code-line:hover:before {
border-left: none;
}
img {
max-width: 100%;
max-height: 100%;
}
a {
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
a:focus,
input:focus,
select:focus,
textarea:focus {
outline: 1px solid -webkit-focus-ring-color;
outline-offset: -1px;
}
hr {
border: 0;
height: 2px;
border-bottom: 2px solid;
}
h1 {
padding-bottom: 0.3em;
line-height: 1.2;
border-bottom-width: 1px;
border-bottom-style: solid;
}
h1, h2, h3 {
font-weight: normal;
}
table {
border-collapse: collapse;
}
table > thead > tr > th {
text-align: left;
border-bottom: 1px solid;
}
table > thead > tr > th,
table > thead > tr > td,
table > tbody > tr > th,
table > tbody > tr > td {
padding: 5px 10px;
}
table > tbody > tr + tr > td {
border-top: 1px solid;
}
blockquote {
margin: 0 7px 0 5px;
padding: 0 16px 0 10px;
border-left-width: 5px;
border-left-style: solid;
}
code {
font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback";
font-size: 1em;
line-height: 1.357em;
}
body.wordWrap pre {
white-space: pre-wrap;
}
pre:not(.hljs),
pre.hljs code > div {
padding: 16px;
border-radius: 3px;
overflow: auto;
}
pre code {
color: var(--vscode-editor-foreground);
tab-size: 4;
}
/** Theming */
.vscode-light pre {
background-color: rgba(220, 220, 220, 0.4);
}
.vscode-dark pre {
background-color: rgba(10, 10, 10, 0.4);
}
.vscode-high-contrast pre {
background-color: rgb(0, 0, 0);
}
.vscode-high-contrast h1 {
border-color: rgb(0, 0, 0);
}
.vscode-light table > thead > tr > th {
border-color: rgba(0, 0, 0, 0.69);
}
.vscode-dark table > thead > tr > th {
border-color: rgba(255, 255, 255, 0.69);
}
.vscode-light h1,
.vscode-light hr,
.vscode-light table > tbody > tr + tr > td {
border-color: rgba(0, 0, 0, 0.18);
}
.vscode-dark h1,
.vscode-dark hr,
.vscode-dark table > tbody > tr + tr > td {
border-color: rgba(255, 255, 255, 0.18);
}
</style>
<style>
/* Tomorrow Theme */
/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */
/* Original theme - https://github.com/chriskempson/tomorrow-theme */
/* Tomorrow Comment */
.hljs-comment,
.hljs-quote {
color: #8e908c;
}
/* Tomorrow Red */
.hljs-variable,
.hljs-template-variable,
.hljs-tag,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class,
.hljs-regexp,
.hljs-deletion {
color: #c82829;
}
/* Tomorrow Orange */
.hljs-number,
.hljs-built_in,
.hljs-builtin-name,
.hljs-literal,
.hljs-type,
.hljs-params,
.hljs-meta,
.hljs-link {
color: #f5871f;
}
/* Tomorrow Yellow */
.hljs-attribute {
color: #eab700;
}
/* Tomorrow Green */
.hljs-string,
.hljs-symbol,
.hljs-bullet,
.hljs-addition {
color: #718c00;
}
/* Tomorrow Blue */
.hljs-title,
.hljs-section {
color: #4271ae;
}
/* Tomorrow Purple */
.hljs-keyword,
.hljs-selector-tag {
color: #8959a8;
}
.hljs {
display: block;
overflow-x: auto;
color: #4d4d4c;
padding: 0.5em;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
</style>
<style>
/*
* Markdown PDF CSS
*/
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif, "Meiryo";
padding: 0 12px;
}
pre {
background-color: #f8f8f8;
border: 1px solid #cccccc;
border-radius: 3px;
overflow-x: auto;
white-space: pre-wrap;
overflow-wrap: break-word;
}
pre:not(.hljs) {
padding: 23px;
line-height: 19px;
}
blockquote {
background: rgba(127, 127, 127, 0.1);
border-color: rgba(0, 122, 204, 0.5);
}
.emoji {
height: 1.4em;
}
code {
font-size: 14px;
line-height: 19px;
}
/* for inline code */
:not(pre):not(.hljs) > code {
color: #C9AE75; /* Change the old color so it seems less like an error */
font-size: inherit;
}
/* Page Break : use <div class="page"/> to insert page break
-------------------------------------------------------- */
.page {
page-break-after: always;
}
</style>
<script src="https://unpkg.com/mermaid/dist/mermaid.min.js"></script>
</head>
<body>
<script>
mermaid.initialize({
startOnLoad: true,
theme: document.body.classList.contains('vscode-dark') || document.body.classList.contains('vscode-high-contrast')
? 'dark'
: 'default'
});
</script>
<h1 id="konfiguracja-programu-modborno3">Konfiguracja programu modb_orno3</h1>
<h2 id="parametry-konfiguracyjne">Parametry konfiguracyjne</h2>
<p>Wszystkie parametry są zdefiniowane na początku pliku <code>modb_orno3.c</code> (linie 11-40):</p>
<pre class="hljs"><code><div><span class="hljs-comment">/* KONFIGURACJA PROGRAMU - EDYTUJ TU */</span>
<span class="hljs-comment">/* Port szeregowy */</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> USB_DEV_DEFAULT <span class="hljs-meta-string">"/dev/ttyUSB0"</span></span>
<span class="hljs-comment">/* Adresy urządzeń Modbus RTU */</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> ORNO_SLAVE_ADR 2</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> SUN2000_SLAVE_ADR 3</span>
<span class="hljs-comment">/* Parametry timing dla ORNO (mikrosekund) */</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> ORNO_RTS_DELAY 5000 <span class="hljs-comment">/* RTS delay: 5ms */</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> ORNO_BYTE_TIMEOUT 2500 <span class="hljs-comment">/* Byte timeout: 2.5ms */</span></span>
<span class="hljs-comment">/* Parametry timing dla SUN2K (mikrosekund) */</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> SUN2K_RTS_DELAY 5000 <span class="hljs-comment">/* RTS delay: 5ms */</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> SUN2K_BYTE_TIMEOUT 2500 <span class="hljs-comment">/* Byte timeout: 2.5ms */</span></span>
<span class="hljs-comment">/* Przerwa między ORNO a SUN2K (mikrosekund) */</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> DELAY_BETWEEN_DEVICES 100000 <span class="hljs-comment">/* 100ms */</span></span>
<span class="hljs-comment">/* MQTT - ustaw 1 aby wyłączyć MQTT */</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> DISABLE_MQTT 0 <span class="hljs-comment">/* 0 = MQTT włączony, 1 = wyłączony */</span></span>
<span class="hljs-comment">/* Broker MQTT */</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> MQTT_BROKER <span class="hljs-meta-string">"10.1.1.1"</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> MQTT_PORT 1883</span>
<span class="hljs-comment">/* Tryb pracy */</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> READ_LOOP false <span class="hljs-comment">/* false = jeden odczyt, true = pętla co 5s */</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> READ_ORNO true <span class="hljs-comment">/* Czy odczytywać ORNO */</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> READ_SUN2K true <span class="hljs-comment">/* Czy odczytywać SUN2K */</span></span>
</div></code></pre>
<h2 id="uruchomienie">Uruchomienie</h2>
<h3 id="kompilacja">Kompilacja:</h3>
<pre class="hljs"><code><div>bash cc-modb_orno3.sh
</div></code></pre>
<h3 id="uruchomienie">Uruchomienie:</h3>
<pre class="hljs"><code><div>./modborno3
</div></code></pre>
<p>Program uruchamia się <strong>bez parametrów</strong> i używa konfiguracji z #define.</p>
<h2 id="sekwencja-dzia%C5%82ania">Sekwencja działania</h2>
<ol>
<li>
<p><strong>Odczyt ORNO</strong> (adres Modbus: 2)</p>
<ul>
<li>Napięcia L1, L2, L3</li>
<li>Prądy L1, L2, L3</li>
<li>Moc Total, L1, L2, L3</li>
<li>Energia Total, L1, L2, L3</li>
<li>Częstotliwość</li>
</ul>
</li>
<li>
<p><strong>Przerwa 100ms</strong></p>
</li>
<li>
<p><strong>Odczyt SUN2K</strong> (adres Modbus: 3)</p>
<ul>
<li>Wszystkie rejestry falownika</li>
<li>Czasy start/shutdown</li>
<li>Statusy</li>
<li>Moce</li>
<li>Energia dzienna i całkowita</li>
</ul>
</li>
</ol>
<h2 id="tematy-mqtt">Tematy MQTT</h2>
<h3 id="orno">ORNO:</h3>
<ul>
<li><code>/energy/orno/U</code> - napięcia (JSON: U1, U2, U3)</li>
<li><code>/energy/orno/I</code> - prądy (JSON: I1, I2, I3)</li>
<li><code>/energy/orno/P</code> - moc (JSON: P_Tot, P1, P2, P3)</li>
<li><code>/energy/orno/W</code> - energia (JSON: W_Tot, W1, W2, W3)</li>
<li><code>/energy/orno/Hz</code> - częstotliwość (wartość)</li>
</ul>
<h3 id="sun2k">SUN2K:</h3>
<ul>
<li><code>/energy/SUN2K</code> - wszystkie parametry (JSON dla każdego rejestru)</li>
</ul>
<h2 id="modyfikacja-konfiguracji">Modyfikacja konfiguracji</h2>
<ol>
<li>Edytuj plik <code>modb_orno3.c</code></li>
<li>Zmień wartości #define</li>
<li>Przekompiluj: <code>bash cc-modb_orno3.sh</code></li>
<li>Uruchom: <code>./modborno3</code></li>
</ol>
<h2 id="przyk%C5%82ady-modyfikacji">Przykłady modyfikacji</h2>
<h3 id="wy%C5%82%C4%85czenie-mqtt">Wyłączenie MQTT:</h3>
<pre class="hljs"><code><div><span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> DISABLE_MQTT 1</span>
</div></code></pre>
<h3 id="zmiana-adresu-brokera">Zmiana adresu brokera:</h3>
<pre class="hljs"><code><div><span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> MQTT_BROKER <span class="hljs-meta-string">"192.168.1.100"</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> MQTT_PORT 1883</span>
</div></code></pre>
<h3 id="tryb-ci%C4%85g%C5%82y-p%C4%99tla-co-5s">Tryb ciągły (pętla co 5s):</h3>
<pre class="hljs"><code><div><span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> READ_LOOP true</span>
</div></code></pre>
<h3 id="tylko-orno-bez-sun2k">Tylko ORNO (bez SUN2K):</h3>
<pre class="hljs"><code><div><span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> READ_SUN2K false</span>
</div></code></pre>
<h3 id="zmiana-przerwy-mi%C4%99dzy-odczytami-200ms">Zmiana przerwy między odczytami (200ms):</h3>
<pre class="hljs"><code><div><span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> DELAY_BETWEEN_DEVICES 200000</span>
</div></code></pre>
<h2 id="walidacja-danych-i-zabezpieczenia-mqtt">Walidacja danych i zabezpieczenia MQTT</h2>
<h3 id="automatyczna-walidacja-warto%C5%9Bci">Automatyczna walidacja wartości</h3>
<p>Program <strong>automatycznie waliduje</strong> wszystkie odczytane wartości przed wysłaniem do MQTT.
Dane są wysyłane do MQTT <strong>tylko</strong> gdy:</p>
<ol>
<li>✅ Odczyt Modbus zakończył się sukcesem (prawidłowa liczba rejestrów)</li>
<li>✅ Wartość nie jest <strong>NaN</strong> (Not a Number)</li>
<li>✅ Wartość nie jest <strong>Inf</strong> (Infinity)</li>
<li>✅ Wartość mieści się w <strong>rozsądnym zakresie</strong></li>
</ol>
<h3 id="zakresy-walidacji-dla-orno">Zakresy walidacji dla ORNO</h3>
<table>
<thead>
<tr>
<th>Parametr</th>
<th>Zakres</th>
<th>Jednostka</th>
<th>Powód</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Napięcia L1/L2/L3</strong></td>
<td>150 - 280</td>
<td>V</td>
<td>Typowy zakres sieci UE (230V ±20%)</td>
</tr>
<tr>
<td><strong>Prądy L1/L2/L3</strong></td>
<td>0 - 100</td>
<td>A</td>
<td>Zabezpieczenie instalacji domowej</td>
</tr>
<tr>
<td><strong>Moc Total/L1/L2/L3</strong></td>
<td>-25000 - +25000</td>
<td>W</td>
<td>Pozwala na generację (wartości ujemne)</td>
</tr>
<tr>
<td><strong>Energia Total/L1/L2/L3</strong></td>
<td>0 - 1000000</td>
<td>kWh</td>
<td>Licznik nie może mieć ujemnej energii</td>
</tr>
<tr>
<td><strong>Częstotliwość</strong></td>
<td>47 - 53</td>
<td>Hz</td>
<td>Typowy zakres sieci UE (50Hz) / US (60Hz)</td>
</tr>
</tbody>
</table>
<h3 id="logi-walidacji">Logi walidacji</h3>
<p>Program wyświetla szczegółowe informacje o walidacji:</p>
<p><strong>Gdy dane są prawidłowe:</strong></p>
<pre class="hljs"><code><div>Voltages: L1=225.8 V, L2=221.3 V, L3=229.8 V
MQTT: Published voltages
</div></code></pre>
<p><strong>Gdy odczyt się nie powiódł:</strong></p>
<pre class="hljs"><code><div>Failed to read currents: Resource temporarily unavailable
(brak komunikatu MQTT - dane nie są wysyłane)
</div></code></pre>
<p><strong>Gdy wartość jest poza zakresem:</strong></p>
<pre class="hljs"><code><div>WARNING: Value 350.5 out of range [150.0, 280.0]
MQTT: Skipping voltages - invalid values
</div></code></pre>
<p><strong>Gdy wartość jest NaN lub Inf:</strong></p>
<pre class="hljs"><code><div>WARNING: Value is NaN
MQTT: Skipping power - invalid values
</div></code></pre>
<h3 id="zabezpieczenia-dla-sun2k">Zabezpieczenia dla SUN2K</h3>
<p>Dla urządzenia SUN2K program:</p>
<ul>
<li><strong>NIE wysyła</strong> danych gdy odczyt się nie powiódł</li>
<li><strong>NIE używa</strong> starych wartości (<code>buf_last_val</code>) po błędzie</li>
<li><strong>Tylko</strong> aktualne, poprawnie odczytane wartości są publikowane</li>
</ul>
<p><strong>Przykład:</strong></p>
<pre class="hljs"><code><div>modbus_read_registers addr=0x7D55 nb=1 -&gt; ERR (Connection timed out)
Frequency: Failed to 11 read: Connection timed out
MQTT: Skipping Frequency - no valid data
</div></code></pre>
<h3 id="dlaczego-to-wa%C5%BCne">Dlaczego to ważne?</h3>
<p><strong>Bez walidacji:</strong></p>
<ul>
<li>Błędne odczyty (CRC errors) mogły wysyłać śmieciowe dane do MQTT</li>
<li>Wartości NaN/Inf mogły powodować błędy w systemach monitoringu</li>
<li>Niemożliwe wartości (np. 10000V) mogły wywoływać fałszywe alarmy</li>
</ul>
<p><strong>Z walidacją:</strong></p>
<ul>
<li>✅ Tylko <strong>poprawne</strong> wartości trafiają do MQTT</li>
<li>✅ System monitoringu dostaje <strong>wiarygodne</strong> dane</li>
<li>✅ Brak <strong>fałszywych alarmów</strong> z powodu błędnych odczytów</li>
<li><strong>Logi</strong> pokazują dokładnie co poszło nie tak</li>
</ul>
<h3 id="kod-walidacji">Kod walidacji</h3>
<p>Funkcja walidacji znajduje się w <a href="modb_orno3.c#L237-L250">modb_orno3.c:237-250</a>:</p>
<pre class="hljs"><code><div><span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">is_valid_float</span><span class="hljs-params">(<span class="hljs-keyword">float</span> value, <span class="hljs-keyword">float</span> min_val, <span class="hljs-keyword">float</span> max_val)</span>
</span>{
<span class="hljs-keyword">if</span> (isnan(value)) {
<span class="hljs-built_in">printf</span>(<span class="hljs-string">"WARNING: Value is NaN\n"</span>);
<span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
}
<span class="hljs-keyword">if</span> (isinf(value)) {
<span class="hljs-built_in">printf</span>(<span class="hljs-string">"WARNING: Value is Inf\n"</span>);
<span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
}
<span class="hljs-keyword">if</span> (value &lt; min_val || value &gt; max_val) {
<span class="hljs-built_in">printf</span>(<span class="hljs-string">"WARNING: Value %.2f out of range [%.2f, %.2f]\n"</span>,
value, min_val, max_val);
<span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
}
<span class="hljs-keyword">return</span> <span class="hljs-number">1</span>;
}
</div></code></pre>
<p>Walidacja jest wywoływana dla każdego pomiaru przed wysłaniem do MQTT.</p>
<h3 id="modyfikacja-zakres%C3%B3w-walidacji">Modyfikacja zakresów walidacji</h3>
<p>Jeśli chcesz zmienić zakresy walidacji (np. dla instalacji przemysłowej):</p>
<ol>
<li>Otwórz plik <code>modb_orno3.c</code></li>
<li>Znajdź sekcję z walidacją (np. dla napięć około linii 370)</li>
<li>Zmień wartości w <code>is_valid_float()</code>:</li>
</ol>
<p><strong>Przykład - zwiększenie zakresu prądów do 200A:</strong></p>
<pre class="hljs"><code><div><span class="hljs-comment">/* Przed: */</span>
<span class="hljs-keyword">if</span> (is_valid_float(I1, <span class="hljs-number">0.0</span>, <span class="hljs-number">100.0</span>) &amp;&amp; ...
<span class="hljs-comment">/* Po: */</span>
<span class="hljs-keyword">if</span> (is_valid_float(I1, <span class="hljs-number">0.0</span>, <span class="hljs-number">200.0</span>) &amp;&amp; ...
</div></code></pre>
<ol start="4">
<li>Przekompiluj: <code>bash cc-modb_orno3.sh</code></li>
</ol>
</body>
</html>

2708
Claude_conversation.md Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

753
Claude_conversation_full.md Normal file
View File

@@ -0,0 +1,753 @@
---
title: "Claude Code Conversation - modb_orno3"
author: "Claude & User"
date: "25 stycznia 2026"
geometry: margin=2cm
---
## 🤖 Claude Response
Przeanalizuję program modb_orno3.c i porównam go z ornowe51x_copilo.py. Zacznę od odczytania obu plików.
## 🤖 Claude Response
Nie znalazłem pliku ornowe51x_copilo.py. Pozwól, że go poszukam w katalogu.
## 🤖 Claude Response
## Analiza programu modb_orno3.c
Przeanalizowałem program C oraz odpowiadający mu program Python. Oto szczegółowe zestawienie:
### **Konfiguracja parametrów przy uruchomieniu `/dev/ttyUSB0 2 3 orno`:**
Z linii [modb_orno3.c:245-260](modb_orno3.c#L245-L260):
- USB_DEV = `/dev/ttyUSB0`
- ORNO_SLAVE = `2`
- SUN2000_SLAVE = `3`
- mode = `"orno"` → ustawia `do_orno=1, do_sun2k=0`
### **Konfiguracja portu szeregowego:**
**Program C** ([modb_orno3.c:347](modb_orno3.c#L347)):
```c
modbus_new_rtu(USB_DEV, 9600, 'E', 8, 1)
```
**Program Python** ([mbusclient.py:258](mbusclient.py#L258)):
```python
serial.Serial(device, baudrate=4800, parity=PARITY_EVEN, stopbits=STOPBITS_ONE)
```
ale potem ustawia `baudrate=brate` (parametr z createClient → 9600)
**Zgodne: 9600 baud, Even parity, 8 data bits, 1 stop bit**
### **Timeout:**
**Program C** ([modb_orno3.c:366-368](modb_orno3.c#L366-L368)):
- RTS delay: 1000 µs (1ms)
- Response timeout: 900000 µs (0.9s)
- Byte timeout: 2500 µs (2.5ms)
**Program Python** ([mbusclient.py:255](mbusclient.py#L255)):
- mainTout = 0.9s
- charTout = 12 * 1/9600 = 0.00125s = 1.25ms
**Prawidłowe**
### **Odczytywane rejestry:**
| Adres | Ilość | Python | C (program) |
|-------|-------|--------|-------------|
| 0x0E | 6 | ✅ Napięcia L1,L2,L3 | ✅ [linia 413](modb_orno3.c#L413) |
| 0x16 | 6 | ✅ Prądy L1,L2,L3 | ❌ **BRAK!** |
| 0x1C | 8 | ✅ Moc Tot,L1,L2,L3 | ✅ [linia 437](modb_orno3.c#L437) |
| 0x100 | 8 | ✅ Energia Tot,L1,L2,L3 | ✅ [linia 463](modb_orno3.c#L463) |
| 0x14 | 2 | ✅ Częstotliwość | ❌ **BRAK!** |
### **🔴 Główne problemy:**
1. **Program C NIE odczytuje prądów (0x16) i częstotliwości (0x14)** - które Python odczytuje
2. **Błąd w definicji mbReg_Orno[]** ([linia 84](modb_orno3.c#L84)):
```c
{"L3 Volt", 0x0012, 2, "L3 Volt", 10, rgU32, "V", "info", "hold", ""}
```
L3 ma `scale=10`, a powinno być `scale=1` (jak L1 i L2)
3. **Debug jest włączony** ([linia 364](modb_orno3.c#L364)):
```c
modbus_set_debug(ctx, TRUE);
```
To powoduje wypisywanie surowych ramek Modbus na stdout
4. **Niepotrzebne opóźnienia** ([linia 434-435](modb_orno3.c#L434-L435)):
```c
usleep(10000); // 10ms
usleep(100000); // 100ms - to jest zbędne podwójne opóźnienie
```
### **Konwersja danych:**
**Python** używa:
```python
def mem2float(reg1, reg2):
raw = struct.pack('>HH', reg1, reg2)
return struct.unpack('>f', raw)[0]
```
**C** używa:
```c
modbus_get_float_dcba(&reg[0])
```
✅ **Obie metody są zgodne** (big-endian float, DCBA order)
### **Podsumowanie:**
Program powinien działać dla podstawowych odczytów (napięcia, moc, energia), ale:
- Nie odczytuje prądów i częstotliwości
- Ma błąd w skali dla L3 Volt
- Debug mode może powodować zaśmiecenie outputu
Czy chcesz, żebym naprawił te problemy?
## 🤖 Claude Response
Zbuduję i uruchomię program. Najpierw znajdę skrypt budowania.
## 🤖 Claude Response
Widzę problem! Program otrzymuje błędne CRC, co wskazuje na problemy z komunikacją. Odpowiedzi są zbyt krótkie i nieprawidłowe. Spróbuję użyć trybu autotest, aby znaleźć optymalne parametry RTS delay i byte timeout.
## 🤖 Claude Response
Świetnie! Autotest pokazuje, że domyślne parametry **nie działają**. Znalazłem optymalne ustawienia:
**Działające kombinacje:**
- ✅ RTS=5000us, byte_timeout=2500us (najszybsze: 62-70ms)
- ✅ RTS=10000us, byte_timeout=2500us (stabilne: 64-69ms)
Teraz uruchomię program z optymalnymi parametrami:
## 🤖 Claude Response
Widzę problem! Program ignoruje parametry RTS i byte_timeout w normalnym trybie. Parametry są odczytywane ([linia 272-277](modb_orno3.c#L272-L277)), ale używane tylko w trybie autotest. W normalnym trybie wartości są sztywno ustawione w [liniach 366-368](modb_orno3.c#L366-L368).
Sprawdzę kod dokładniej:
## 🤖 Claude Response
Problem jest jasny! Kod **ignoruje** parametry przekazane z linii komend. Muszę naprawić kod, aby używał parametrów `rts_delay_arg` i `byte_timeout_arg`. Ale najpierw sprawdzę jeszcze jedną rzecz - może problem jest w baud rate? W kodzie Python widziałem `BIT_RATE = 4800`. Sprawdzę:
## 🤖 Claude Response
Muszę naprawić kod. Zobaczę jak to działa w trybie autotest i dodam wsparcie dla parametrów w normalnym trybie:
## 🤖 Claude Response
Widzę problem! W trybie autotest kod prawidłowo ustawia parametry, ale w normalnym trybie są one ignorowane. Naprawię kod:
## 🤖 Claude Response
Teraz usunę zbędne podwójne ustawienia:
## 🤖 Claude Response
Teraz przekompiluję i uruchomię z poprawnymi parametrami:
## 🤖 Claude Response
Widzę że parametry są teraz używane (RTS Delay 5000 us), ale nadal są błędy CRC. Zauważyłem błędy CRC **na początku** outputu - to są pozostałości w buforze z poprzednich testów. Wyłączę debug mode i dodam flush bufora przed odczytem:
## 🤖 Claude Response
Teraz dodam flush bufora i poczekam przed pierwszym odczytem:
## 🤖 Claude Response
Świetnie! Komunikacja działa (wszystkie odczyty OK), ale wartości są błędne. Problem jest w formacie float! Obecnie używa `modbus_get_float_dcba`, ale to daje niepoprawne wartości.
Przetestuję wszystkie dostępne formaty float aby znaleźć właściwy:
## 🤖 Claude Response
Doskonale! **Format ABCD** jest prawidłowy! Wartości napięć wyglądają poprawnie:
- L1: 222.4 V
- L2: 215.0 V
- L3: 225.0 V
Teraz zmienię wszystkie formaty float na ABCD:
## 🤖 Claude Response
Usunę debug output:
## 🤖 Claude Response
Poprawię również output dla pozostałych odczytów:
## 🤖 Claude Response
Teraz przekompiluję i uruchomię końcową wersję:
## 🤖 Claude Response
## ✅ DOSKONALE! Program działa poprawnie!
### **Wyniki transmisji:**
**Napięcia (0x0E):**
- L1: 223.1 V
- L2: 217.6 V
- L3: 226.1 V
- Czas: 62ms ✓
**Moc (0x1C):**
- Total: 0.595 W
- L1: 0.220 W
- L2: 0.256 W
- L3: 0.117 W
- Czas: 131ms ✓
**Energia (0x100):**
- Total: 27197.779 kWh
- L1: 10579.180 kWh
- L2: 8609.990 kWh
- L3: 8008.610 kWh
- Czas: 68ms ✓
### **Podsumowanie poprawek:**
1. ✅ **Naprawiono parametry RTS/byte timeout** - teraz program używa parametrów z linii komend (domyślnie: RTS=5000µs, byte=2500µs)
2. ✅ **Naprawiono format float** - zmieniono z `DCBA` na `ABCD` (big-endian)
3. ✅ **Wyłączono debug mode** - eliminuje problemy z timingiem
4. ✅ **Dodano flush bufora** - czyści port przed pierwszym odczytem
5. ✅ **Poprawiono output** - czytelny format wyników
Porównajmy teraz z programem Python:
## 🤖 Claude Response
Python program nie generuje outputu (prawdopodobnie problem z MQTT). Ale program C działa prawidłowo! Teraz sprawdzę czy wartości są stabilne:
## 🤖 Claude Response
## ✅ WERYFIKACJA ZAKOŃCZONA SUKCESEM!
### **Test stabilności (3 próby):**
| Parametr | Test 1 | Test 2 | Test 3 | Stabilność |
|----------|--------|--------|--------|------------|
| **Napięcie L1** | 221.0 V | 219.4 V | 217.4 V | ✓ Wahania ±2V (normalne) |
| **Napięcie L2** | 221.7 V | 217.5 V | 218.3 V | ✓ Wahania ±2V (normalne) |
| **Napięcie L3** | 228.6 V | 228.4 V | 226.9 V | ✓ Stabilne |
| **Moc Total** | 0.614 W | 0.609 W | 0.609 W | ✓ Bardzo stabilna |
| **Energia Total** | 27197.801 | 27197.801 | 27197.811 | ✓ Powolny wzrost (normalne) |
### **Kluczowe naprawy w programie C:**
1. **Format float**: `DCBA` → `ABCD` (big-endian)
2. **Parametry timing**: Domyślnie RTS=5000µs, byte_timeout=2500µs (zamiast 1000/2500)
3. **Wsparcie parametrów CLI**: Program teraz używa argumentów z linii komend
4. **Flush bufora**: Dodano czyszczenie portu przed pierwszym odczytem
5. **Wyłączono debug**: Eliminuje problemy z timingiem komunikacji
### **Stan programu:**
- ✅ Komunikacja Modbus RTU działa poprawnie (9600 baud, Even parity)
- ✅ Odczyty napięć (0x0E) - OK
- ✅ Odczyty mocy (0x1C) - OK
- ✅ Odczyty energii (0x100) - OK
- ❌ **Brak odczytów prądów (0x16) i częstotliwości (0x14)** - te funkcje nie są zaimplementowane w programie C (są w Python)
Program działa zgodnie z oczekiwaniami dla parametrów `/dev/ttyUSB0 2 3 orno 5000 2500`!
## 🤖 Claude Response
Dodam odczyt prądów (0x16) i częstotliwości (0x14) do programu C, tak jak są w programie Python.
## 🤖 Claude Response
Najpierw dodam funkcje MQTT dla prądów i częstotliwości:
## 🤖 Claude Response
Teraz dodam zmienne dla prądów i częstotliwości:
## 🤖 Claude Response
Teraz dodam odczyt prądów (0x16) po odczycie napięć:
## 🤖 Claude Response
Teraz dodam odczyt częstotliwości (0x14) na końcu, po odczycie energii:
## 🤖 Claude Response
Teraz przekompiluję i przetestuję program z nowymi odczytami:
## 🤖 Claude Response
Doskonale! Wszystkie odczyty działają. Teraz wykonam test stabilności:
## 🤖 Claude Response
Świetnie! Wykonam jeszcze jeden pełny test z dokładnym timerem:
## 🤖 Claude Response
Doskonale! Porównajmy teraz z programem Python aby zweryfikować zgodność:
## 🤖 Claude Response
Teraz stworzę podsumowanie zmian:
## 🤖 Claude Response
Wykonam ostateczny test pokazujący wszystkie pomiary:
## 🤖 Claude Response
Wystąpił błąd CRC - prawdopodobnie port był zajęty. Poczekam chwilę i wykonam test ponownie:
## 🤖 Claude Response
## ✅ SUKCES! Dodano odczyt prądów i częstotliwości
### **Kompletny odczyt z miernika ORNO:**
```
Napięcia (0x0E): L1=215.4 V, L2=212.1 V, L3=226.0 V [64ms]
Prądy (0x16): L1=1.47 A, L2=1.83 A, L3=0.91 A [63ms] ← NOWE
Moc (0x1C): Total=0.607 W, L1=0.220 W, L2=0.275 W, L3=0.112 W [266ms]
Energia (0x100): Total=27197.840 kWh, L1=10579.200 kWh, L2=8610.020 kWh, L3=8008.620 kWh [69ms]
Częstotliwość (0x14): 49.99 Hz [54ms] ← NOWE
```
### **Dodane do programu C:**
**1. Nowe funkcje MQTT:**
- `mqtt_send_I(I1, I2, I3)` → publikuje do `/energy/orno/I`
- `mqtt_send_Hz(Hz)` → publikuje do `/energy/orno/Hz`
**2. Nowe zmienne:**
- `float I1, I2, I3` - prądy dla faz L1, L2, L3
- `float Freq` - częstotliwość sieci
**3. Nowe odczyty Modbus:**
- [modb_orno3.c:419-428](modb_orno3.c#L419-L428) - odczyt prądów z rejestru 0x16
- [modb_orno3.c:469-478](modb_orno3.c#L469-L478) - odczyt częstotliwości z rejestru 0x14
### **Zgodność z programem Python:**
| Rejestr | Python | C | Status |
|---------|--------|---|--------|
| 0x0E (napięcia) | ✓ | ✓ | Zgodne |
| 0x16 (prądy) | ✓ | ✓ | **Dodane!** |
| 0x1C (moc) | ✓ | ✓ | Zgodne |
| 0x100 (energia) | ✓ | ✓ | Zgodne |
| 0x14 (częstotliwość) | ✓ | ✓ | **Dodane!** |
Program C w [modb_orno3.c](modb_orno3.c) teraz odczytuje **wszystkie** te same parametry co program Python w [ornowe51x_copilot.py](ornowe51x_copilot.py)!
## 🤖 Claude Response
Zweryfikuję działanie programu dla urządzenia SUN2K. Najpierw sprawdzę konfigurację SUN2K w kodzie:
## 🤖 Claude Response
Teraz uruchomię program dla SUN2K:
## 🤖 Claude Response
Świetnie! Program działa dla SUN2K. Widzę, że:
**✅ Odczyty działają poprawnie:**
- Startup time: Sun Jan 25 08:36:23 2026
- Shutdown: Sun Jan 25 17:09:27 2026
- Current time: Sun Jan 25 20:16:18 2026
- Peak Power: 2.274 kW
- Accumulated energy: 32583.41 kWh
- Daily energy: 6.74 kWh
**⚠️ Problem:** Początkowe błędy CRC - pierwsze 9-10 prób niepowodzeń, potem program zaczął działać. To przez brak konfiguracji timeoutów i RTS delay dla SUN2K.
Dodam konfigurację timeoutów dla SUN2K podobnie jak dla ORNO:
## 🤖 Claude Response
Widzę poprawę, ale nadal są problemy z niektórymi rejestrami (32089, 32090). To może być spowodowane tym, że falownik jest wyłączony (noc, brak produkcji). Spróbuję z większym opóźnieniem między odczytami:
## 🤖 Claude Response
Opóźnienie już jest (100ms między odczytami). Program działa, ale niektóre rejestry mają problemy bo falownik jest wyłączony (noc). Zobaczmy czystszy output:
## 🤖 Claude Response
Doskonale! Teraz wszystkie rejestry zostały poprawnie odczytane. Stworzę czytelniejsze podsumowanie wyników:
## 🤖 Claude Response
Widzę niestabilność komunikacji. Sprawdzę autotest dla SUN2K, aby znaleźć optymalne parametry:
## 🤖 Claude Response
Stworzę podsumowanie wyników dla SUN2K:
## 🤖 Claude Response
## ✅ WERYFIKACJA PROGRAMU DLA SUN2K ZAKOŃCZONA
### **Konfiguracja SUN2K:**
```
Port: /dev/ttyUSB0
Slave Address: 3
Baudrate: 9600
Parity: None (N)
Data bits: 8
Stop bits: 1
RTS Delay: 5000 µs
Response Timeout: 0.9 s
Byte Timeout: 2500 µs
```
### **Wyniki testów:**
| Parametr | Rejestr | Test 1 | Test 2 | Status |
|----------|---------|--------|--------|--------|
| Startup Time | 32091 | ✓ | ✓ | **Stabilny** |
| Status | 32089 | ✓ 40960 | ✓ 40960 | **Stabilny** |
| Peak Power | 32078 | ✓ 2.274 kW | ✗ | Niestabilny |
| Active Power | 32080 | ✓ 0 kW | ✓ 0 kW | **Stabilny** |
| Frequency | 32085 | ✗ | ✗ | **Zawsze błąd** |
| Accumulated Energy | 32106 | ✗ | ✓ 32583.41 kWh | Niestabilny |
| Daily Energy | 32114 | ✓ 6.74 kWh | ✓ 6.74 kWh | **Stabilny** |
### **Podsumowanie:**
**✅ Program działa dla SUN2K** - odczytuje dane z falownika
**⚠️ Niestabilność komunikacji:**
- Niektóre rejestry mają problemy z CRC
- Frequency (32085) konsekwentnie zawodzi
- Prawdopodobna przyczyna: **falownik jest wyłączony** (noc, brak produkcji słonecznej)
**✓ Kluczowe dane odczytywane poprawnie:**
- Daily Energy: 6.74 kWh (produkcja dzienna)
- Accumulated Energy: 32583.41 kWh (całkowita produkcja)
- Status: 40960 (urządzenie w trybie standby/nocnym)
- Startup: 08:36:23, Shutdown: 17:09:27
### **Dodane ulepszenia:**
1. ✅ Konfiguracja timeoutów dla SUN2K (wcześniej brak)
2. ✅ Flush bufora przed pierwszym odczytem
3. ✅ Wyświetlanie konfiguracji połączenia
4. ✅ Retry mechanizm (do 10 prób) dla każdego rejestru
**Zalecenie:** Test w ciągu dnia (gdy falownik produkuje energię) powinien dać stabilniejsze wyniki dla wszystkich rejestrów.
## 🤖 Claude Response
Zmodyfikuję program aby uruchamiał się bez parametrów, z predefiniowanymi ustawieniami i automatycznym odczytem ORNO → SUN2K z wysyłką do MQTT.
## 🤖 Claude Response
Naprawię błędy kompilacji - muszę zamienić wszystkie wystąpienia NO_MQTT na DISABLE_MQTT:
## 🤖 Claude Response
Naprawię błąd w linii 280 - nie można przypisać wartości do DISABLE_MQTT, bo to teraz #define:
## 🤖 Claude Response
Teraz muszę usunąć cały kod parsowania parametrów linii komend, ponieważ program ma działać bez parametrów:
## 🤖 Claude Response
Naprawię błędy - zamienię zmienne na wartości z #define:
## 🤖 Claude Response
Teraz dodam opóźnienie między ORNO a SUN2K:
## 🤖 Claude Response
Teraz skompilujmy i przetestujmy program:
## 🤖 Claude Response
Świetnie! Program działa bez parametrów. Przetestuję jeszcze raz:
## 🤖 Claude Response
Doskonale! Teraz sprawdzę czy MQTT działa (muszę zatrzymać screen przed testem z prawdziwym MQTT):
## 🤖 Claude Response
Teraz wykonam końcowy test pokazujący pełną sekwencję:
## 🤖 Claude Response
Stworzę dokumentację konfiguracji w osobnym pliku:
## 🤖 Claude Response
## ✅ PROGRAM ZMODYFIKOWANY I GOTOWY DO UŻYCIA
### **Wykonane zmiany:**
**1. Usunięto parsowanie parametrów linii komend**
- Program uruchamia się **bez żadnych argumentów**: `./modborno3`
- Wszystkie parametry zdefiniowane jako `#define` w pliku źródłowym
**2. Dodano sekcję konfiguracyjną** ([modb_orno3.c:11-40](modb_orno3.c#L11-L40))
```c
#define USB_DEV_DEFAULT "/dev/ttyUSB0"
#define ORNO_SLAVE_ADR 2
#define SUN2000_SLAVE_ADR 3
#define ORNO_RTS_DELAY 5000
#define ORNO_BYTE_TIMEOUT 2500
#define SUN2K_RTS_DELAY 5000
#define SUN2K_BYTE_TIMEOUT 2500
#define DELAY_BETWEEN_DEVICES 100000 /* 100ms */
#define MQTT_BROKER "10.1.1.1"
#define MQTT_PORT 1883
#define DISABLE_MQTT 0 /* 0=włączony, 1=wyłączony */
```
**3. Automatyczna sekwencja odczytów:**
- **ORNO** (pełny odczyt: napięcia, prądy, moc, energia, częstotliwość)
- **100ms przerwa** (konfigurowalna)
- **SUN2K** (pełny odczyt falownika)
**4. MQTT automatycznie włączony:**
- Dane z ORNO → `/energy/orno/` (U, I, P, W, Hz)
- Dane z SUN2K → `/energy/SUN2K`
### **Test działania:**
```
==============================================
Energy Meter Reader - ORNO & SUN2K
Start: Sun Jan 25 19:31:54 2026
==============================================
Configuration:
Device: /dev/ttyUSB0
ORNO slave address: 2
SUN2K slave address: 3
MQTT: ENABLED
MQTT Broker: 10.1.1.1:1883
==============================================
Voltages: L1=216.2 V, L2=214.0 V, L3=227.2 V
Currents: L1=1.29 A, L2=8.95 A, L3=0.92 A
Power: Total=2.191 W, L1=0.180 W, L2=1.896 W, L3=0.115 W
Energy: Total=27198.211 kWh, L1=10579.260 kWh, L2=8610.290 kWh, L3=8008.660 kWh
Frequency: 49.9800 Hz
Waiting 100 ms before SUN2K...
=== SUN2K Reading Started ===
[... dane z SUN2K ...]
```
### **Jak używać:**
**Kompilacja:**
```bash
bash cc-modb_orno3.sh
```
**Uruchomienie:**
```bash
./modborno3
```
**Modyfikacja konfiguracji:**
1. Edytuj [modb_orno3.c:11-40](modb_orno3.c#L11-L40)
2. Przekompiluj: `bash cc-modb_orno3.sh`
3. Uruchom: `./modborno3`
Dokumentacja konfiguracji została zapisana w pliku [CONFIG.md](CONFIG.md).
## 🤖 Claude Response
Utworzę plik systemd service wraz z timerem do cyklicznego odczytu co 10 sekund.
## 🤖 Claude Response
## ✅ Pliki systemd service gotowe!
### **Utworzone pliki:**
1. **[read-energy.service](read-energy.service)** - jednostka serwisu systemd
- Uruchamia program `modborno3`
- Użytkownik: `ms`, grupa: `dialout`
- Logi do journald
2. **[read-energy.timer](read-energy.timer)** - timer systemd
- Uruchamia co **10 sekund**
- Start po 30s od bootu
- Dokładność: 1s
3. **[install-systemd.sh](install-systemd.sh)** - automatyczny skrypt instalacyjny
- Sprawdza wymagania
- Instaluje pliki
- Włącza i uruchamia timer
4. **[SYSTEMD_INSTALL.md](SYSTEMD_INSTALL.md)** - pełna dokumentacja
---
### **INSTALACJA (2 sposoby):**
#### **Metoda 1: Automatyczna (ZALECANA)**
```bash
sudo bash install-systemd.sh
```
#### **Metoda 2: Ręczna**
```bash
# 1. Skopiuj pliki
sudo cp read-energy.service /etc/systemd/system/
sudo cp read-energy.timer /etc/systemd/system/
# 2. Przeładuj systemd
sudo systemctl daemon-reload
# 3. Włącz i uruchom
sudo systemctl enable read-energy.timer
sudo systemctl start read-energy.timer
```
---
### **SPRAWDZENIE:**
```bash
# Status timera
sudo systemctl status read-energy.timer
# Kiedy następny odczyt
systemctl list-timers read-energy.timer
# Logi w czasie rzeczywistym
sudo journalctl -u read-energy.service -f
```
---
### **KONFIGURACJA TIMERA:**
**Plik: read-energy.timer**
```ini
OnBootSec=30s # Start po 30s od boot
OnUnitActiveSec=10s # Powtarzaj co 10 sekund ← TUTAJ ZMIEŃ
AccuracySec=1s # Dokładność 1s
```
**Zmiana interwału:**
1. Edytuj: `/etc/systemd/system/read-energy.timer`
2. Zmień `OnUnitActiveSec=10s` na np. `OnUnitActiveSec=30s`
3. Przeładuj:
```bash
sudo systemctl daemon-reload
sudo systemctl restart read-energy.timer
```
---
### **PRZYDATNE KOMENDY:**
| Akcja | Komenda |
|-------|---------|
| **Status timera** | `sudo systemctl status read-energy.timer` |
| **Status serwisu** | `sudo systemctl status read-energy.service` |
| **Logi (realtime)** | `sudo journalctl -u read-energy.service -f` |
| **Logi (ostatnie 100)** | `sudo journalctl -u read-energy.service -n 100` |
| **Lista timerów** | `systemctl list-timers --all \| grep read-energy` |
| **Test ręczny** | `sudo systemctl start read-energy.service` |
| **Restart** | `sudo systemctl restart read-energy.timer` |
| **Stop** | `sudo systemctl stop read-energy.timer` |
| **Wyłącz autostart** | `sudo systemctl disable read-energy.timer` |
---
### **JAK DZIAŁA:**
1. Timer (`read-energy.timer`) uruchamia się co 10 sekund
2. Każde uruchomienie wywołuje service (`read-energy.service`)
3. Service uruchamia program `modborno3`
4. Program odczytuje ORNO → czeka 100ms → odczytuje SUN2K
5. Wysyła dane do MQTT (10.1.1.1:1883)
6. Kończy działanie
7. Timer czeka 10 sekund i powtarza
---
✅ **Wszystko gotowe do instalacji!** Uruchom: `sudo bash install-systemd.sh`

View File

@@ -0,0 +1,59 @@
# 1. Stwórz Markdown z JSONL
cat > ~/claude_to_markdown.sh << 'EOF'
#!/bin/bash
INPUT="$1"
OUTPUT="$2"
cat > "$OUTPUT" << 'MD_START'
# Claude Code Conversation - Program modb_orno3.c
**Data:** 25 stycznia 2026
**Temat:** Analiza i modyfikacja programu do odczytu mierników energii ORNO i SUN2K
---
MD_START
jq -r '
select(.type == "user" or .type == "assistant") |
if .type == "user" then
"\n## 👤 User Query\n\n```\n" + (.message.content // "") + "\n```\n"
elif .type == "assistant" then
"\n## 🤖 Claude Response\n\n" + (
if (.message.content | type) == "array" then
(.message.content[] | select(.type == "text") | .text)
else
(.message.content // "")
end
) + "\n"
else
empty
end
' "$INPUT" >> "$OUTPUT"
echo "✓ Markdown saved to: $OUTPUT"
EOF
chmod +x ~/claude_to_markdown.sh
# 2. Generuj Markdown
~/claude_to_markdown.sh \
~/.claude/projects/-home-ms-projekty-python3-modebus-ornoserial3/3f3d08dc-f007-453b-8bd3-4aa0b1158050.jsonl \
~/projekty/python3/modebus/ornoserial3/Claude_conversation_full.md
# 3. Otwórz w VSCode
code ~/projekty/python3/modebus/ornoserial3/Claude_conversation_full.md
echo ""
echo "=========================================="
echo "TERAZ W VSCODE:"
echo "=========================================="
echo "1. Zainstaluj rozszerzenie 'Markdown PDF' (yzane.markdown-pdf)"
echo " - Ctrl+Shift+X → wyszukaj 'Markdown PDF' → Install"
echo ""
echo "2. Eksportuj do PDF:"
echo " - Ctrl+Shift+P → 'Markdown PDF: Export (pdf)'"
echo ""
echo "3. PDF pojawi się w:"
echo " ~/projekty/python3/modebus/ornoserial3/Claude_conversation_full.pdf"
echo "=========================================="

189
SYSTEMD_INSTALL.md Normal file
View File

@@ -0,0 +1,189 @@
# Instalacja i konfiguracja systemd service
## Pliki
- `read-energy.service` - jednostka serwisu, która uruchamia program modborno3
- `read-energy.timer` - timer uruchamiający service co 10 sekund
## Instalacja
### 1. Skopiuj pliki do systemd
```bash
sudo cp read-energy.service /etc/systemd/system/
sudo cp read-energy.timer /etc/systemd/system/
```
### 2. Przeładuj konfigurację systemd
```bash
sudo systemctl daemon-reload
```
### 3. Włącz timer (autostart)
```bash
sudo systemctl enable read-energy.timer
```
### 4. Uruchom timer
```bash
sudo systemctl start read-energy.timer
```
## Zarządzanie
### Sprawdzenie statusu
```bash
# Status timera
sudo systemctl status read-energy.timer
# Status serwisu
sudo systemctl status read-energy.service
# Lista aktywnych timerów
systemctl list-timers --all | grep read-energy
```
### Zatrzymanie
```bash
# Zatrzymaj timer (przestanie uruchamiać odczyty)
sudo systemctl stop read-energy.timer
# Wyłącz autostart
sudo systemctl disable read-energy.timer
```
### Ponowne uruchomienie
```bash
sudo systemctl restart read-energy.timer
```
### Logi
```bash
# Pokaż logi z ostatniego odczytu
sudo journalctl -u read-energy.service -n 50
# Logi w czasie rzeczywistym
sudo journalctl -u read-energy.service -f
# Logi z ostatniej godziny
sudo journalctl -u read-energy.service --since "1 hour ago"
# Logi z dzisiaj
sudo journalctl -u read-energy.service --since today
```
### Testowanie
```bash
# Ręczne uruchomienie (test)
sudo systemctl start read-energy.service
# Sprawdź czy działa
sudo systemctl status read-energy.service
# Zobacz logi
sudo journalctl -u read-energy.service -n 100 --no-pager
```
## Modyfikacja interwału
Aby zmienić interwał z 10s na inny:
1. Edytuj plik `/etc/systemd/system/read-energy.timer`
2. Zmień wartość `OnUnitActiveSec=10s` (np. `OnUnitActiveSec=30s` dla 30 sekund)
3. Przeładuj i zrestartuj:
```bash
sudo systemctl daemon-reload
sudo systemctl restart read-energy.timer
```
## Debugowanie
### Sprawdź czy timer jest aktywny
```bash
systemctl is-active read-energy.timer
systemctl is-enabled read-energy.timer
```
### Sprawdź kiedy następny odczyt
```bash
systemctl list-timers read-energy.timer
```
### Sprawdź błędy
```bash
sudo journalctl -u read-energy.service -p err -n 50
```
### Test połączenia Modbus
```bash
# Ręcznie uruchom program
cd /home/ms/projekty/python3/modebus/ornoserial3
./modborno3
```
## Wymagania
- Program `modborno3` musi być skompilowany i dostępny w:
`/home/ms/projekty/python3/modebus/ornoserial3/modborno3`
- Użytkownik `ms` musi być w grupie `dialout`
- Serwer MQTT musi być dostępny (10.1.1.1:1883)
- Port `/dev/ttyUSB0` musi być dostępny
### Sprawdź uprawnienia
```bash
# Sprawdź czy użytkownik jest w grupie dialout
groups ms
# Jeśli nie, dodaj:
sudo usermod -a -G dialout ms
# Wyloguj i zaloguj się ponownie
```
## Konfiguracja
Aby zmienić parametry (port, adresy Modbus, MQTT), edytuj plik:
`/home/ms/projekty/python3/modebus/ornoserial3/modb_orno3.c`
Sekcja konfiguracji znajduje się w liniach 11-42 (#define).
Po zmianie:
```bash
cd /home/ms/projekty/python3/modebus/ornoserial3
bash cc-modb_orno3.sh
sudo systemctl restart read-energy.timer
```
## Monitorowanie w czasie rzeczywistym
```bash
# Terminal 1: logi
sudo journalctl -u read-energy.service -f
# Terminal 2: status
watch -n 2 'systemctl status read-energy.timer && echo && systemctl list-timers read-energy.timer'
```
## Usunięcie
```bash
sudo systemctl stop read-energy.timer
sudo systemctl disable read-energy.timer
sudo rm /etc/systemd/system/read-energy.service
sudo rm /etc/systemd/system/read-energy.timer
sudo systemctl daemon-reload
```

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

1
cc-modb_orno3.sh Executable file
View File

@@ -0,0 +1 @@
gcc -g -I /usr/include/modbus modb_orno3.c -lmosquitto -lmodbus -o modborno3

32
claude_markdown.sh Executable file
View File

@@ -0,0 +1,32 @@
#!/bin/bash
INPUT="$1"
OUTPUT="$2"
cat > "$OUTPUT" << 'MD_START'
---
title: "Claude Code Conversation - modb_orno3"
author: "Claude & User"
date: "25 stycznia 2026"
geometry: margin=2cm
---
MD_START
jq -r '
select(.type == "user" or .type == "assistant") |
if .type == "user" then
"\n## 👤 User Query\n\n```\n" + (.message.content // "") + "\n```\n"
elif .type == "assistant" then
"\n## 🤖 Claude Response\n\n" + (
if (.message.content | type) == "array" then
(.message.content[] | select(.type == "text") | .text)
else
(.message.content // "")
end
) + "\n"
else
empty
end
' "$INPUT" >> "$OUTPUT"
echo "✓ Markdown saved to: $OUTPUT"

49
crc16.py Normal file
View File

@@ -0,0 +1,49 @@
INITIAL_MODBUS = 0xFFFF
INITIAL_DF1 = 0x0000
table = (
0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440,
0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40,
0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841,
0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40,
0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41,
0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641,
0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040,
0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240,
0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441,
0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41,
0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840,
0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41,
0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40,
0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640,
0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041,
0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240,
0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441,
0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41,
0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840,
0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41,
0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40,
0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640,
0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041,
0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241,
0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440,
0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40,
0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841,
0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40,
0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41,
0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641,
0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040 )
def calcByte( charek, crc):
if type(charek) == type("c"):
bajt = ord( charek)
else:
bajt = charek
crc = (crc >> 8) ^ table[(crc ^ bajt) & 0xFF]
return (crc & 0xFFFF)
def calcString( st, crc):
for charek in st:
crc = (crc >> 8) ^ table[(crc ^ ord(charek)) & 0xFF]
return crc

102
energymeter.py Normal file
View File

@@ -0,0 +1,102 @@
#!/usr/bin/env python3
# encoding: utf-8
# ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# Copyright (C) 2017 Piotr Murawski
# Niniejszy program jest wolnym oprogramowaniem; mozesz go
# rozprowadzac dalej i/lub modyfikowac na warunkach Powszechnej
# Licencji Publicznej GNU, wydanej przez Fundacje Wolnego
# Oprogramowania - wedlug wersji 3-ciej tej Licencji lub ktorejs
# z pozniejszych wersji.
# Niniejszy program rozpowszechniany jest z nadzieja iz bedzie on
# uzyteczny - jednak BEZ JAKIEJKOLWIEK GWARANCJI, nawet domyslnej
# gwarancji PRZYDATNOSCI HANDLOWEJ albo PRZYDATNOSCI DO OKRESLONYCH
# ZASTOSOWAN. W celu uzyskania blizszych informacji - Powszechna
# Licencja Publiczna GNU.
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# Z pewnoscia wraz z niniejszym programem otrzymales tez egzemplarz
# Powszechnej Licencji Publicznej GNU (GNU General Public License);
# jesli nie - napisz do Free Software Foundation, Inc., 675 Mass Ave,
# Cambridge, MA 02139, USA.
# Powszechna Licencja GNU dostepna jest rowniez na stronie:
# http://www.gnu.org/licenses/licenses.html
# nieoficjalne polskie tlumaczenie na
# http://www.gnu.org.pl
# ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import time
import mbusbase
import mbusclient
from mbusbase import bytestr
energyclient = None
def createClient(portDevice, adres):
tecl = mbusclient.mbclientserial()
tecl.adres = adres
tecl.transportOpen(portDevice, brate=9600)
return tecl
try:
pass
except:
print("ERROR: can't get modbus client to work!")
pass
return None
def readRegs(client, start, cnt):
r03 = mbusbase.request03(client.adres, start, cnt)
bytes=r03.rtu();
client.transportSend(bytes)
reply=client.getReply()
if reply:
if reply.fail:
return None
else:
return reply.vals
return None
def writeRegs(client, addr, regs):
r16 = mbusbase.request16(client.adres,addr,regs)
bytes=r16.rtu();
client.transportSend(bytes)
reply=client.getReply()
if reply:
if reply.fail:
return None
else:
return reply.vals
return None
def testPort(portDevice, adres):
mbcli = createClient(portDevice, adres)
if mbcli!=None:
regs = readRegs(mbcli, 0x40, 1)
mbcli.transportClose()
if regs!=None:
if len(regs)>=1:
return True
return False
def startClient(portDevice, adres):
print("Energy MODBUS: " + portDevice)
mbcli = createClient(portDevice, adres)
if mbcli!=None:
global energyclient
energyclient = mbcli
return mbcli

78
install-systemd.sh Executable file
View File

@@ -0,0 +1,78 @@
#!/bin/bash
# Skrypt instalacji systemd service dla read-energy
set -e
echo "========================================"
echo "Instalacja read-energy systemd service"
echo "========================================"
echo ""
# Sprawdź czy jesteś użytkownikiem root lub masz sudo
if [ "$EUID" -ne 0 ]; then
echo "Uruchom jako root lub z sudo:"
echo " sudo bash install-systemd.sh"
exit 1
fi
# Sprawdź czy program istnieje
PROG_PATH="/home/ms/projekty/python3/modebus/ornoserial3/modborno3"
if [ ! -f "$PROG_PATH" ]; then
echo "ERROR: Program nie istnieje: $PROG_PATH"
echo "Najpierw skompiluj program:"
echo " cd /home/ms/projekty/python3/modebus/ornoserial3"
echo " bash cc-modb_orno3.sh"
exit 1
fi
echo "✓ Program znaleziony: $PROG_PATH"
# Sprawdź czy użytkownik ms jest w grupie dialout
if ! groups ms | grep -q dialout; then
echo "⚠ Użytkownik 'ms' nie jest w grupie 'dialout'"
echo " Dodaję do grupy..."
usermod -a -G dialout ms
echo "✓ Dodano do grupy dialout (wymagane ponowne logowanie)"
else
echo "✓ Użytkownik 'ms' jest w grupie 'dialout'"
fi
# Kopiuj pliki
echo ""
echo "Kopiowanie plików do /etc/systemd/system/..."
cp -v read-energy.service /etc/systemd/system/
cp -v read-energy.timer /etc/systemd/system/
# Przeładuj systemd
echo ""
echo "Przeładowywanie systemd..."
systemctl daemon-reload
# Włącz timer
echo ""
echo "Włączanie timera (autostart)..."
systemctl enable read-energy.timer
# Uruchom timer
echo ""
echo "Uruchamianie timera..."
systemctl start read-energy.timer
# Status
echo ""
echo "========================================"
echo "Instalacja zakończona!"
echo "========================================"
echo ""
systemctl status read-energy.timer --no-pager
echo ""
echo "Timer będzie uruchamiał odczyt co 10 sekund."
echo ""
echo "Komendy:"
echo " sudo systemctl status read-energy.timer - status timera"
echo " sudo systemctl status read-energy.service - status serwisu"
echo " sudo journalctl -u read-energy.service -f - logi w czasie rzeczywistym"
echo " systemctl list-timers read-energy.timer - kiedy następny odczyt"
echo ""
echo "Więcej informacji: SYSTEMD_INSTALL.md"

496
mbusbase.py Normal file
View File

@@ -0,0 +1,496 @@
#!/usr//bin//env python
# encoding: utf-8
# ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# Copyright (C) 2017 Piotr Murawski
# Niniejszy program jest wolnym oprogramowaniem; mozesz go
# rozprowadzac dalej i//lub modyfikowac na warunkach Powszechnej
# Licencji Publicznej GNU, wydanej przez Fundacje Wolnego
# Oprogramowania - wedlug wersji 3-ciej tej Licencji lub ktorejs
# z pozniejszych wersji.
# Niniejszy program rozpowszechniany jest z nadzieja iz bedzie on
# uzyteczny - jednak BEZ JAKIEJKOLWIEK GWARANCJI, nawet domyslnej
# gwarancji PRZYDATNOSCI HANDLOWEJ albo PRZYDATNOSCI DO OKRESLONYCH
# ZASTOSOWAN. W celu uzyskania blizszych informacji - Powszechna
# Licencja Publiczna GNU.
# This program is free software: you can redistribute it and//or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# Z pewnoscia wraz z niniejszym programem otrzymales tez egzemplarz
# Powszechnej Licencji Publicznej GNU (GNU General Public License);
# jesli nie - napisz do Free Software Foundation, Inc., 675 Mass Ave,
# Cambridge, MA 02139, USA.
# Powszechna Licencja GNU dostepna jest rowniez na stronie:
# http://www.gnu.org//licenses//licenses.html
# nieoficjalne polskie tlumaczenie na
# http://www.gnu.org.pl
# ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
from crc16 import *
import ctypes
FAILCODE_CRC = 1
FAILCODE_REPLY = 3
class MbusbaseFail(Exception):
def __init__(self, message, code, fail=0, reply=None):
super(MbusbaseFail, self).__init__(message)
self.code = code
self.fail = reply
self.reply = reply
def bytestr(bytes):
ot=''
for by in bytes:
ot=ot+('%02X ' % by)
return ot
class errcode(object):
NOERROR =0x00
ILLEGAL_FUNCTION =0x01
ILLEGAL_DATA_ADDRESS =0x02
ILLEGAL_DATA_VALUE =0x03
SLAVE_DEVICE_FAILURE =0x04
ACKNOWLEDGE =0x05
SLAVE_DEVICE_BUSY =0x06
MEMORY_PARITY_ERROR =0x08
GATEWAY_PATH_UNAVAILABLE =0x0A
GATEWAY_TARGET_FAILED_TO_RESPOND =0x0B
names={
NOERROR :'no error',
ILLEGAL_FUNCTION :'illegal function',
ILLEGAL_DATA_ADDRESS :'bad data address',
ILLEGAL_DATA_VALUE :'bad data value',
SLAVE_DEVICE_FAILURE :'slave failure',
ACKNOWLEDGE :'acknowledge',
SLAVE_DEVICE_BUSY :'slave device busy',
MEMORY_PARITY_ERROR :'memory parity',
GATEWAY_PATH_UNAVAILABLE :'gateway path unavailable',
GATEWAY_TARGET_FAILED_TO_RESPOND :'gateway target failed to respond',
}
pass
def makeFrame8(addr, code, data):
lent=1+1+len(data);
bytes=bytearray(lent);
bytes[0] = addr;
bytes[1] = code;
idx=0;
for dat in data:
bytes[2] = dat & 0xFF;
idx+=1
return bytes
def makeFrame16(addr, code, data):
lent=1+1+len(data)*2;
bytes=bytearray(lent);
bytes[0] = addr;
bytes[1] = code;
idx=0;
for dat in data:
bytes[2+idx] = dat >> 8;
bytes[3+idx] = dat & 0xFF;
idx+=2
return bytes
def makeFrameAppendStart(addr, code):
bytes=bytearray();
if addr:
bytes.append(addr)
bytes.append(code)
return bytes
def makeFrameAppend8(data, value):
data.append(value)
return data
def makeFrameAppend16(data, value):
if value<0: value=ctypes.c_ushort(value).value
data.append(value >> 8)
data.append(value & 0xFF)
return data
def makeRtuFrame(frmabytes):
lent=len(frmabytes)
frmabytes.append(0)
frmabytes.append(0)
crc = INITIAL_MODBUS
for ix in range(0,lent):
crc = calcByte( frmabytes[ix], crc)
frmabytes[lent] = crc & 0xFF
frmabytes[lent+1] = crc >> 8
return frmabytes
def makeTcpFrame(frmabytes, sequence):
lent=len(frmabytes)
frmabytes.insert(0,lent)
frmabytes.insert(0,0)
frmabytes.insert(0,0)
frmabytes.insert(0,0)
frmabytes.insert(0,sequence & 0xFF)
frmabytes.insert(0,sequence >> 8)
return frmabytes
def get16(arrayin):
try:
return (arrayin[2:], (arrayin[0]<<8) | arrayin[1])
except:
return (arrayin, None)
def get8(arrayin):
try:
return (arrayin[1:], arrayin[0])
except:
return (arrayin, None)
class framebase(object):
def __init__(self,adres,code):
self.bytes = makeFrameAppendStart(adres,code)
self.tcpseq=0
pass
def rtu(self):
return makeRtuFrame(self.bytes)
def tcp(self, sequence):
self.tcpseq=sequence
return makeTcpFrame(self.bytes, sequence)
class response(framebase):
reqCode = 0x01
#-----------------------------------------------------
# 80 - error response
class responseError(response):
def __init__(self,adres,reqCodeRep,errCodek):
self.reqCode = reqCodeRep | 0x80
super(responseError,self).__init__(adres,self.reqCode)
self.bytes = makeFrameAppend8(self.bytes, errCodek)
#-----------------------------------------------------
# 01 - read coils
class request01(framebase):
reqCode = 0x01
def __init__(self,adres,val16_1,val16_2):
super(request01,self).__init__(adres,self.reqCode)
self.bytes = makeFrameAppend16(self.bytes, val16_1)
self.bytes = makeFrameAppend16(self.bytes, val16_2)
#print(bytestr(self.bytes))
class response01(response):
reqCode = 0x01
def __init__(self,adres,coilsarr):
super(response01,self).__init__(adres,self.reqCode)
coilcnt = len(coilsarr)
bytescnt = 1+((coilcnt -1 ) >> 3)
self.bytes = makeFrameAppend8(self.bytes, bytescnt)
byteidx=0
coilbyte=0
for coil in coilsarr:
if coil:
coilbyte |= 1 << byteidx
byteidx+=1
if byteidx==8:
self.bytes = makeFrameAppend8(self.bytes, coilbyte)
byteidx=0
coilbyte = 0
if byteidx:
self.bytes = makeFrameAppend8(self.bytes, coilbyte)
#-----------------------------------------------------
# 02 - read discrete inputs
class request02(request01):
reqCode = 0x02
class response02(response01):
reqCode = 0x02
#-----------------------------------------------------
# 03 - read holding registers
class request03(request01):
reqCode = 0x03
class response03(response):
reqCode = 0x03
def __init__(self,adres,regarr):
super(response03,self).__init__(adres,self.reqCode)
regcnt = len(regarr)
self.bytes = makeFrameAppend8(self.bytes, 2*regcnt)
for reg in regarr:
self.bytes = makeFrameAppend16(self.bytes, reg)
#-----------------------------------------------------
# 04 - read input register
class request04(request01):
reqCode = 0x04
class response04(response03):
reqCode = 0x04
#-----------------------------------------------------
# 05 - write single coil
class request05(request01):
reqCode = 0x05
def __init__(self,adres,addr,state):
super(request01,self).__init__(adres,self.reqCode)
statenumber=0x0000
if state:
statenumber=0xFF00 # as specified in the standard
self.bytes = makeFrameAppend16(self.bytes, addr)
self.bytes = makeFrameAppend16(self.bytes, statenumber)
pass
class response05(response):
reqCode = 0x05
def __init__(self,adres,addr,state):
super(response05,self).__init__(adres,self.reqCode)
statenumber=0x0000
if state:
statenumber=0xFF00 # as specified in the standard
self.bytes = makeFrameAppend16(self.bytes, addr)
self.bytes = makeFrameAppend16(self.bytes, statenumber)
#-----------------------------------------------------
# 06 - write single register
class request06(request01):
reqCode = 0x06
class response06(response):
reqCode = 0x06
def __init__(self,adres,addr,value):
super(response06,self).__init__(adres,self.reqCode)
self.bytes = makeFrameAppend16(self.bytes, addr)
self.bytes = makeFrameAppend16(self.bytes, value)
#-----------------------------------------------------
# 15 - write multiple coils
class request15(framebase):
reqCode = 0x0F
def __init__(self,adres,startaddr,coilsarr):
super(request15,self).__init__(adres,self.reqCode)
self.bytes = makeFrameAppend16(self.bytes, startaddr)
coilcnt = len(coilsarr)
self.bytes = makeFrameAppend16(self.bytes, coilcnt)
bytescnt = 1+((coilcnt -1 ) >> 3)
self.bytes = makeFrameAppend8(self.bytes, bytescnt)
byteidx=0
coilbyte=0
for coil in coilsarr:
if coil:
coilbyte |= 1 << byteidx
byteidx+=1
if byteidx==8:
self.bytes = makeFrameAppend8(self.bytes, coilbyte)
byteidx=0
coilbyte = 0
if byteidx:
self.bytes = makeFrameAppend8(self.bytes, coilbyte)
class response15(response):
reqCode = 0x0F
def __init__(self,adres,addr,quantity):
super(response15,self).__init__(adres,self.reqCode)
self.bytes = makeFrameAppend16(self.bytes, addr)
self.bytes = makeFrameAppend16(self.bytes, quantity)
#-----------------------------------------------------
# 16 - write multiple registers
class request16(framebase):
reqCode = 0x10
def __init__(self,adres,startaddr,regarr):
super(request16,self).__init__(adres,self.reqCode)
self.bytes = makeFrameAppend16(self.bytes, startaddr)
regcnt = len(regarr)
self.bytes = makeFrameAppend16(self.bytes, regcnt)
self.bytes = makeFrameAppend8(self.bytes, 2*regcnt)
for reg in regarr:
self.bytes = makeFrameAppend16(self.bytes, reg)
class response16(response15):
reqCode = 0x10
class received(object):
FAIL_OK = 0
FAIL_CRC = 1
FAIL_BAD = 2
FAIL_REPLY = 3
fcode = 0
address = 0
fail = 0
failCode= 0
data = None
def __init__(self):
pass
#-----------------------------------------------------
# Parse RTU
class receivedRtu(received):
def __init__(self,rxbytes, nocrc=False):
super(receivedRtu,self).__init__()
#print("RX:::" + bytestr(rxbytes))
rxcnt = len(rxbytes)
limit=4
if nocrc: limit=3
if rxcnt < limit:
self.fail = self.FAIL_BAD
self.failCode = 1000
self.failText = "receive RTU"
else:
if not nocrc:
crc = INITIAL_MODBUS
for bajt in rxbytes:
crc = calcByte( bajt, crc)
else:
crc=0
if crc:
self.fail = self.FAIL_CRC
self.failCode = 1000
self.failText = "CRC"
raise MbusbaseFail('Bad CRC',FAILCODE_CRC, reply=self)
else:
self.address = rxbytes[0]
self.fcode = rxbytes[1] & 0x7F
if rxbytes[1]>127:
self.fail = self.FAIL_REPLY
self.data = bytearray(rxbytes)
errortext = 'Host replied fail: ('+str(self.FAIL_REPLY)+') '+errcode.names[self.FAIL_REPLY]
raise MbusbaseFail(errortext,FAILCODE_REPLY, self.FAIL_REPLY, reply=self)
else:
if not nocrc:
self.data = bytearray(rxbytes[2:-2])
else:
self.data = bytearray(rxbytes[2:])
def clientParse(self):
self.vals=None
if self.fail == self.FAIL_REPLY:
self.vals=[self.data[1] & 0x7F, self.data[2]]
self.failCode=self.data[2]
self.failText=errcode.names[self.failCode]
elif self.fcode==3 or self.fcode==4:
self.vals=[]
for byi in range(0,self.data[0] // 2):
val16=self.data[1+byi*2]*256 + self.data[2+byi*2]
self.vals.append(val16)
elif self.fcode==1 or self.fcode==2:
self.vals=[]
for byi in range(0,self.data[0]):
for biti in range(0,8):
bit = (self.data[1+byi] >> biti) & 1
self.vals.append(bit)
elif self.fcode==5 or self.fcode==6 or self.fcode==15 or self.fcode==16:
self.vals=[]
#print(bytestr(self.data))
val16=self.data[0]*256 + self.data[1]
self.vals.append(val16)
val16=self.data[2]*256 + self.data[3]
self.vals.append(val16)
def parseRtu(frrtu):
#IF read 16 bit regs
valstxt=''
if frrtu[1]==0x03 or frrtu[1]==0x04:
vals=[]
valstxt=''
for byi in range(0,frrtu[2] // 2):
val16=frrtu[3+byi*2]*256 + frrtu[4+byi*2]
vals.append(val16)
valstxt+='%5u ' % val16
if frrtu[1]==0x01 or frrtu[1]==0x02:
vals=[]
for byi in range(0,frrtu[2]):
for biti in range(0,8):
bit = (frrtu[3+byi] >> biti) & 1;
valstxt+='%u ' % bit
#====================================================================
#
# TESTS
#
#====================================================================
if __name__ == "__main__":
req = request05(0x12,0xA,1).rtu()
print(bytestr(req) )
req = request15(0xCC,0x13,[1,0,1,1,0,0,1,1,1]).rtu()
print(bytestr(req) )
req = request16(0xCC,0x01,[0x000A,0x0102]).rtu()
print(bytestr(req) )
print("response")
print(bytestr(response04(0x11,[0x000A,0x0102]).rtu()) )
print(bytestr(response06(0x11,0xC00A,0x4008).rtu()) )
print(bytestr(response15(0x11,0xC00A,0x4008).rtu()) )
print(bytestr(response16(0x11,0xC00A,0x4008).rtu()) )
rxrtu = receivedRtu( req )
print(bytestr(rxrtu.data))
pass

347
mbusclient.py Normal file
View File

@@ -0,0 +1,347 @@
#!/usr/bin/python
# ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# Copyright (C) 2017 Piotr Murawski
# Niniejszy program jest wolnym oprogramowaniem; mozesz go
# rozprowadzac dalej i/lub modyfikowac na warunkach Powszechnej
# Licencji Publicznej GNU, wydanej przez Fundacje Wolnego
# Oprogramowania - wedlug wersji 3-ciej tej Licencji lub ktorejs
# z pozniejszych wersji.
# Niniejszy program rozpowszechniany jest z nadzieja iz bedzie on
# uzyteczny - jednak BEZ JAKIEJKOLWIEK GWARANCJI, nawet domyslnej
# gwarancji PRZYDATNOSCI HANDLOWEJ albo PRZYDATNOSCI DO OKRESLONYCH
# ZASTOSOWAN. W celu uzyskania blizszych informacji - Powszechna
# Licencja Publiczna GNU.
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# Z pewnoscia wraz z niniejszym programem otrzymales tez egzemplarz
# Powszechnej Licencji Publicznej GNU (GNU General Public License);
# jesli nie - napisz do Free Software Foundation, Inc., 675 Mass Ave,
# Cambridge, MA 02139, USA.
# Powszechna Licencja GNU dostepna jest rowniez na stronie:
# http://www.gnu.org/licenses/licenses.html
# nieoficjalne polskie tlumaczenie na
# http://www.gnu.org.pl
# ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import time
import serial
import mbusbase
from mbusbase import get16, get8, bytestr, errcode
import socket
import sys
FAILCODE_SOCKET = 1
FAILCODE_CONNECT = 2
FAILCODE_NODATA = 3
FAILCODE_TIMEOUT = 4
FAILCODE_SOCKETSEND = 5
FAILCODE_SOCKETREAD = 6
FAILCODE_LENGTH = 7
FAILCODE_SERIAL = 8
FAILCODE_SERIALWRITE = 9
FAILCODE_SERIALREAD = 10
FAILCODE_TCPSEQUENCE = 11
BIT_RATE = 4800
class MbclientFail(Exception):
def __init__(self, message, code):
super(MbclientFail, self).__init__(message)
self.code = code
class mbclient:
transportOpen=False
TOUTMSG=1.0
TOUTCHAR=6
def __init__(self):
pass
def getReply(self):
bstr=self.transportRead(3,self.TOUTMSG)
if bstr != '' and bstr!=None:
if len(bstr)<3:
raise MbclientFail("Not enough bytes received, <3",FAILCODE_NODATA)
bajty=bytearray(bstr)
#print(bytestr)(bajty)
# find expected length
bytescnt=0
if bajty[1]==1 or bajty[1]==2:
bytescnt = int(bajty[2])
elif bajty[1]==3 or bajty[1]==4:
bytescnt = int(bajty[2])
elif bajty[1]==5:
bytescnt = 4
elif bajty[1]==5 or bajty[1]==6:
bytescnt = 3
elif bajty[1]==5 or bajty[1]==15 or bajty[1]==16:
bytescnt = 4
elif bajty[1]>127:
bytescnt = 0
bytescnt+=2
#print('Expected bytes : ' + str)(bytescnt)
bstr=self.transportRead(bytescnt,self.charTout * 256)
#print('bytes : ',len)(bstr)
if bstr != '':
if len(bstr)<bytescnt:
return None
bajtydata=bytearray(bstr)
#print(bytestr)(bajtydata)
apdu=bajty+bajtydata
rtu = mbusbase.receivedRtu(apdu)
rtu.clientParse()
return rtu
else:
raise MbclientFail("Receive timeout",FAILCODE_TIMEOUT)
def transportOpen(self):
pass
def transportClose(self):
return None
def sendRequest(self, request):
return None
def transportSend(self, rawbytes):
return None
def transportRead(self, count,timeout=None):
return None
def readCoils(self, adres, start, cnt):
self.sendRequest(mbusbase.request01(adres, start, cnt) )
return self.awaitReply()
def readInputs(self, adres, start, cnt):
self.sendRequest(mbusbase.request02(adres, start, cnt) )
return self.awaitReply()
def readInputRegs(self, adres, start, cnt):
self.sendRequest(mbusbase.request04(adres, start, cnt) )
return self.awaitReply()
def readHoldingRegs(self, adres, start, cnt):
self.sendRequest(mbusbase.request03(adres, start, cnt) )
return self.awaitReply()
def writeCoil(self, adres, addr, state):
self.sendRequest(mbusbase.request05(adres, addr, state) )
return self.awaitReply()
def writeCoils(self, adres, startaddr,coilsarr):
self.sendRequest(mbusbase.request15(adres, startaddr,coilsarr) )
return self.awaitReply()
def writeRegister(self, adres, addr, value):
self.sendRequest(mbusbase.request06(adres, addr, value) )
return self.awaitReply()
def writeRegisters(self, adres, startaddr,regarr):
self.sendRequest(mbusbase.request16(adres, startaddr,regarr) )
return self.awaitReply()
class mbclienttcp(mbclient):
portdev=None
def transportOpen(self, clientAddress, timeout=0.9, port=502):
self.clientAddress = clientAddress
self.mainTout=timeout
self.socke=None
self.sequence=1
self.port=port
self.charTout = timeout / 256
self.lastsequence = 0
serverAddress = (self.clientAddress, self.port)
try:
self.socke = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
except Exception as inst:
raise MbclientFail("Can't open socket ["+inst.args[1]+']',FAILCODE_SOCKET)
try:
print('Connecting to %s:%s' % serverAddress, file=sys.stderr)
self.socke.connect(serverAddress)
except Exception as inst:
raise MbclientFail("Can't connect socket @"+ str(self.clientAddress)+":"+str(self.port)+" ["+inst.args[1]+']',FAILCODE_CONNECT)
self.transportOpen=True
def transportClose(self):
try:
self.socke.close()
except:
pass
return None
def transportSend(self, rawbytes):
if not self.transportOpen:
return None
try:
self.socke.sendall(rawbytes)
return
except Exception as inst:
raise MbclientFail("Socket send error ["+inst.args[1]+']',FAILCODE_SOCKETSEND)
return None
def transportRead(self, count, timeout):
self.socke.settimeout(timeout)
if not self.transportOpen:
return None
try:
return self.socke.recv(count, socket.MSG_WAITALL)
except Exception as inst:
raise MbclientFail("Socket read error ["+inst.args[1]+']',FAILCODE_SOCKETREAD)
return None
def sendRequest(self, request):
bytes=request.tcp(self.sequence)
self.lastsequence=self.sequence
self.sequence+=1
return self.transportSend(bytes)
def awaitReply(self):
bstr=self.transportRead(6,self.TOUTMSG)
if len(bstr)!=6:
raise MbclientFail("Not enough bytes received, <6 ",FAILCODE_NODATA)
else:
bajty=bytearray(bstr)
#print("|aaa|" + bytestr)(bajty)
sequence=bajty[0]*256 + bajty[1]
length=bajty[4]*256 + bajty[5]
if self.lastsequence != sequence:
raise MbclientFail("Sequence mismatch: expected "+str(self.lastsequence)+", got "+str(sequence),FAILCODE_TCPSEQUENCE)
if length:
bstr=self.transportRead(length,self.charTout * 256)
#print('bytes : ',len)(bstr)
if bstr != '':
if len(bstr)<length:
raise MbclientFail("Not enough bytes received ",FAILCODE_NODATA)
bajtydata=bytearray(bstr)
#print("|bbb|"+bytestr)(bajtydata)
apdu=bajtydata
rtu = mbusbase.receivedRtu(apdu, True)
rtu.clientParse()
return rtu
else:
raise MbclientFail("TCP header length = 0 ",FAILCODE_LENGTH)
return reply
def setTimeout(self, tout):
self.timeout = tout
class mbclientserial(mbclient):
portdev=None
def transportOpen(self, device, brate=BIT_RATE, stopbit=1, timeout=0.9, parity=serial.PARITY_EVEN, stopbits=serial.STOPBITS_ONE):
self.mainTout=timeout
self.charTout=12*1/brate
self.portdev=None
try:
self.portdev = serial.Serial(device, timeout = timeout, baudrate=BIT_RATE, parity=serial.PARITY_EVEN, stopbits=serial.STOPBITS_ONE)
if not hasattr(self.portdev, 'setTimeout'):
serial.Serial.setTimeout = setTimeout
except Exception as inst:
self.portdev = None
raise MbclientFail("Can't open serial "+str(device)+" ["+inst.args[1]+']',FAILCODE_SERIAL)
# flush some garbage
self.portdev.setTimeout(0.01)
self.portdev.read(16)
# configure for good
self.portdev.setTimeout(timeout)
self.portdev.baudrate=brate
self.portdev.parity=parity #serial.PARITY_EVEN
self.portdev.stopbits=stopbits
self.transportOpen=True
def transportClose(self):
try:
self.portdev.close()
except:
pass
return None
def transportSend(self, rawbytes):
if not self.transportOpen:
return None
try:
bb=bytearray(1)
for b in rawbytes:
bb[0]=b
self.portdev.write(bb)
#return self.portdev.write(rawbytes)
return
except Exception as inst:
raise MbclientFail("Can't write serial ["+inst.args[1]+']',FAILCODE_SERIALWRITE)
return None
def transportRead(self, count,timeout):
if not self.transportOpen:
return None
try:
if timeout:
if timeout==self.TOUTMSG: self.portdev.setTimeout(self.mainTout)
elif timeout==self.TOUTCHAR: self.portdev.setTimeout(self.charTout * count)
else: self.portdev.setTimeout(timeout)
return self.portdev.read(count)
except Exception as inst:
raise MbclientFail("Can't read serial ["+inst.args[1]+']',FAILCODE_SERIALREAD)
return None
def sendRequest(self, request):
bytes=request.rtu();
return self.transportSend(bytes)
def awaitReply(self):
reply=self.getReply()
return reply
class mbclientdevice:
def __init__(self, mbaddr, mbclientiface):
self.mbaddr = mbaddr
self.mbclientiface = mbclientiface
if __name__ == "__main__":
tecl = mbclientserial()
tecl.transportOpen('/dev/ttyUSB0', brate=BIT_RATE)
#r03 = mbusbase.request01(1,0x3,20)
r03 = mbusbase.request03(1,1,10)
print(bytestr(r03.rtu()))
tecl.transportSend(r03.rtu())
reply=tecl.getReply()
if reply:
if reply.fail:
print('error', reply.fail)
else:
print(reply.vals)
#time.sleep(0.5)
pass

871
modb_orno3.c Normal file
View File

@@ -0,0 +1,871 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include </usr/include/modbus/modbus-rtu.h>
#include <errno.h>
#include <time.h>
#include <unistd.h>
#include <mosquitto.h>
#include <sys/time.h>
#include <math.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
/* ============================================ */
/* KONFIGURACJA PROGRAMU - EDYTUJ TU */
/* ============================================ */
#define READ_LOOP true /* false = jeden odczyt, true = pętla co 5s */
#define VOLTAGE_BUFFER_SIZE 12
#define VOLTAGE_FLUCTUATION_THRESHOLD 0.05 // 5%
#define HIGH_FREQ_MODE_DURATION 300 // 300 seconds
#define READ_ORNO true /* Czy odczytywać ORNO */
#define READ_SUN2K true /* Czy odczytywać SUN2K */
/* Adresy urządzeń Modbus RTU */
#define SUN2000_SLAVE_ADR 3
#define ORNO_SLAVE_ADR 2
/* Port szeregowy */
#define USB_DEV_DEFAULT "/dev/ttyUSB0"
/* Parametry timing dla ORNO (mikrosekund) */
#define ORNO_RTS_DELAY 5000 /* RTS delay: 5ms */
#define ORNO_BYTE_TIMEOUT 2500 /* Byte timeout: 2.5ms */
/* Parametry timing dla SUN2K (mikrosekund) */
#define SUN2K_RTS_DELAY 5000 /* RTS delay: 5ms */
#define SUN2K_BYTE_TIMEOUT 2500 /* Byte timeout: 2.5ms */
/* Przerwa między ORNO a SUN2K (mikrosekund) */
#define DELAY_BETWEEN_DEVICES 10000 /* 10ms */
/* MQTT - ustaw 1 aby wyłączyć MQTT */
#define DISABLE_MQTT 0 /* 0 = MQTT włączony, 1 = wyłączony */
/* Broker MQTT */
#define MQTT_BROKER "10.1.1.1"
#define MQTT_PORT 1883
/* InfluxDB Configuration */
#define DISABLE_INFLUX 0 /* 0 = InfluxDB włączony, 1 = wyłączony */
#define INFLUX_HOST "10.1.1.1"
#define INFLUX_PORT 5086
#define INFLUX_ORG "sic" /* Zmień na swoją organizację */
#define INFLUX_BUCKET "energydb" /* Zmień na swój bucket */
#define INFLUX_TOKEN "BCIZ6kcCAVgpcwSfU0PBS7m0Zb6an93kuOtizbEtlXub-uaoYp4dmIQBQYaJCv8_KE4QYYZ08jxtpaZ3TUWP-Q==" /* Zmień na swój token */
/* ============================================ */
/*
'Model' : 'ID'
'addr' : '30000',
'registers' : 15,
'name' : 'Model',
'scale' : 1,
'type' : 'str',
'units' : '' ,
'use' : 'info',
'method' : 'hold'
*/
enum regtype
{
rgStr,
rgU16,
rgU32,
rgI16,
rgI32,
rgB16,
rgB32,
rgData,
rgFloat
};
typedef struct s_mb_reg
{
const char *reg_id;
int ireg;
int num_reg;
char *desc;
int scale;
enum regtype type;
char *units;
char *use;
char *method;
char buf_last_val[30];
} t_mb_reg;
t_mb_reg mbReg[] = {
{"Start", 32091, 2, "Startup time", 1, rgData, "s", "info", "hold", ""},
{"Shutdown", 32093, 2, "Shutdown time", 1, rgData, "s", "info", "hold", ""},
{"Time", 40000, 2, "Current time", 1, rgData, "s", "info", "hold", ""},
{"State1", 32000, 1, "Status 1", 1, rgB16, "", "stat", "hold", ""},
{"Alarm1", 32008, 1, "Alarm 1", 1, rgB16, "", "stat", "hold", ""},
{"Status", 32089, 1, "Device status", 1, rgU16, "", "stat", "hold", ""},
{"Fault", 32090, 1, "Fault code", 1, rgU16, "", "stat", "hold", ""},
{"PV_P", 32064, 2, "Input power", 1000, rgI32, "kW", "data", "hold", ""},
{"U_A", 32069, 1, "Phase Voltage A", 10, rgU16, "V", "data", "hold", ""},
{"U_B", 32070, 1, "Phase Voltage B", 10, rgU16, "V", "data", "hold", ""},
{"U_C", 32071, 1, "Phase Voltage C", 10, rgU16, "V", "ext", "hold", ""},
{"P_peak", 32078, 2, "Peak Power", 1000, rgI32, "kW", "data", "hold", ""},
{"P_active", 32080, 2, "Active power", 1000, rgI32, "kW", "data", "hold", ""},
{"P_reactive", 32082, 2, "Reactive power", 1000, rgI32, "kVar", "data", "hold", ""},
{"Frequency", 32085, 1, "Grid frequency", 100, rgU16, "Hz", "data", "hold", ""},
{"Temp", 32087, 1, "Internal temperature", 10, rgI16, "°C", "data", "hold", ""},
{"P_accum", 32106, 2, "Accumulated energy yield", 100, rgU32, "kWh", "data", "hold", ""},
{"P_daily", 32114, 2, "Daily energy yield", 100, rgU32, "kWh", "data", "hold", ""}
};
t_mb_reg mbReg_Orno[] = {
{"L1 Volt", 0x000e, 2, "L1 Volt", 1, rgU32, "V", "info", "hold", ""},
{"L2 Volt", 0x0010, 2, "L2 Volt", 1, rgU32, "V", "info", "hold", ""},
{"L3 Volt", 0x0012, 2, "L3 Volt", 10, rgU32, "V", "info", "hold", ""},
{"Tot Power", 0x001c, 2, "Tot Power", 1, rgU32, "kW", "info", "hold", ""},
{"L1 Tot Power", 0x001e, 2, "L1 Tot Power", 1, rgU32, "kW", "info", "hold", ""},
{"L2 Tot Power", 0x0020, 2, "L2 Tot Power", 1, rgU32, "kW", "info", "hold", ""},
{"L3 Tot Power", 0x0022, 2, "L3 Tot Power", 1, rgU32, "kW", "info", "hold", ""},
{"Tot Energ", 0x0100, 2, "Tot Energ", 1, rgU32, "kWh", "info", "hold", ""},
{"L1 Tot Energ", 0x0102, 2, "L1 Tot Energ", 1, rgU32, "kWh", "info", "hold", ""},
{"L2 Tot Energ", 0x0104, 2, "L2 Tot Energ", 1, rgU32, "kWh", "info", "hold", ""},
{"L3 Tot Energ", 0x0106, 2, "L3 Tot Energ", 1, rgU32, "kWh", "info", "hold", ""}};
void mosq_log_callback(struct mosquitto *mosq, void *userdata, int level, const char *str)
{
/* Pring all log messages regardless of level. */
switch (level)
{
//case MOSQ_LOG_DEBUG:
//case MOSQ_LOG_INFO:
//case MOSQ_LOG_NOTICE:
case MOSQ_LOG_WARNING:
case MOSQ_LOG_ERR:
{
printf("MQTT: %i:%s\n", level, str);
}
}
}
struct mosquitto *mosq = NULL;
void mqtt_setup()
{
if (DISABLE_MQTT) {
printf("MQTT: disabled by configuration\n");
return;
}
char *host = MQTT_BROKER;
int port = MQTT_PORT;
int keepalive = 60;
bool clean_session = true;
mosquitto_lib_init();
mosq = mosquitto_new(NULL, clean_session, NULL);
if (!mosq)
{
fprintf(stderr, "MQTT: Error: Out of memory.\n");
exit(1);
}
mosquitto_log_callback_set(mosq, mosq_log_callback);
if (mosquitto_connect(mosq, host, port, keepalive))
{
fprintf(stderr, "MQTT: Unable to connect.\n");
exit(1);
}
int loop = mosquitto_loop_start(mosq);
if (loop != MOSQ_ERR_SUCCESS)
{
fprintf(stderr, "MQTT: Unable to start loop: %i\n", loop);
exit(1);
}
}
int mqtt_send(char *topic, char *buf)
{
if (DISABLE_MQTT) return 0;
return mosquitto_publish(mosq, NULL, topic, strlen(buf), buf, 0, 0);
}
int mqtt_send_SUN2K(t_mb_reg *reg_to_send, char *_str_buf)
{
if (DISABLE_MQTT) return 0;
/* Don't send if str_buf is empty or NULL - means read failed */
if (!_str_buf || strlen(_str_buf) == 0) {
printf("MQTT: Skipping %s - no valid data\n", reg_to_send->reg_id);
return 0;
}
char topic[128];
char buf[128];
snprintf(topic, sizeof(topic), "/energy/SUN2K");
snprintf(buf, sizeof(buf), "{\"%s\":%s}", reg_to_send->reg_id, _str_buf);
/* Save last valid value */
strncpy(reg_to_send->buf_last_val, _str_buf, sizeof(reg_to_send->buf_last_val)-1);
reg_to_send->buf_last_val[sizeof(reg_to_send->buf_last_val)-1] = '\0';
printf("MQTT: Publishing %s\n", buf);
return mosquitto_publish(mosq, NULL, topic, strlen(buf), buf, 0, 0);
}
int mqtt_send_U(float U1, float U2, float U3)
{
if (DISABLE_MQTT) return 0;
char buf[128];
sprintf(buf, "{\"U1\":%f,\"U2\":%f,\"U3\":%f}", U1, U2, U3);
return mosquitto_publish(mosq, NULL, "/energy/orno/U", strlen(buf), buf, 0, 0);
}
int mqtt_send_P(float P_Tot, float P1, float P2, float P3)
{
if (DISABLE_MQTT) return 0;
char buf[128];
sprintf(buf, "{\"P_Tot\":%f,\"P1\":%f,\"P2\":%f,\"P3\":%f}", P_Tot, P1, P2, P3);
return mosquitto_publish(mosq, NULL, "/energy/orno/P", strlen(buf), buf, 0, 0);
}
int mqtt_send_W(float W_Tot, float W1, float W2, float W3)
{
if (DISABLE_MQTT) return 0;
char buf[128];
sprintf(buf, "{\"W_Tot\":%f,\"W1\":%f,\"W2\":%f,\"W3\":%f}", W_Tot, W1, W2, W3);
return mosquitto_publish(mosq, NULL, "/energy/orno/W", strlen(buf), buf, 0, 0);
}
int mqtt_send_I(float I1, float I2, float I3)
{
if (DISABLE_MQTT) return 0;
char buf[128];
sprintf(buf, "{\"I1\":%f,\"I2\":%f,\"I3\":%f}", I1, I2, I3);
return mosquitto_publish(mosq, NULL, "/energy/orno/I", strlen(buf), buf, 0, 0);
}
int mqtt_send_Hz(float Hz)
{
if (DISABLE_MQTT) return 0;
char buf[64];
sprintf(buf, "%f", Hz);
return mosquitto_publish(mosq, NULL, "/energy/orno/Hz", strlen(buf), buf, 0, 0);
}
/* InfluxDB Functions */
int influx_send_post(char *data)
{
if (DISABLE_INFLUX) return 0;
int sock;
struct sockaddr_in server;
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == -1) {
perror("INFLUX: Could not create socket for InfluxDB");
return -1;
}
server.sin_addr.s_addr = inet_addr(INFLUX_HOST);
server.sin_family = AF_INET;
server.sin_port = htons(INFLUX_PORT);
/* Set timeout for socket */
struct timeval tv;
tv.tv_sec = 2;
tv.tv_usec = 0;
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof tv);
setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tv, sizeof tv);
if (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0) {
perror("INFLUX: Connect to InfluxDB failed");
close(sock);
return -1;
}
char header[1024];
/* Construct HTTP POST request for InfluxDB v2 Write API */
snprintf(header, sizeof(header),
"POST /api/v2/write?org=%s&bucket=%s&precision=s HTTP/1.1\r\n"
"Host: %s:%d\r\n"
"Authorization: Token %s\r\n"
"Content-Length: %ld\r\n"
"Content-Type: text/plain; charset=utf-8\r\n"
"Connection: close\r\n\r\n",
INFLUX_ORG, INFLUX_BUCKET, INFLUX_HOST, INFLUX_PORT, INFLUX_TOKEN, strlen(data));
if (send(sock, header, strlen(header), 0) < 0) {
perror("INFLUX: Send header to InfluxDB failed");
close(sock);
return -1;
}
if (send(sock, data, strlen(data), 0) < 0) {
perror("INFLUX: Send body to InfluxDB failed");
close(sock);
return -1;
}
close(sock);
return 0;
}
int influx_send_U(float U1, float U2, float U3) {
char line[256];
snprintf(line, sizeof(line), "orno,device=orno,pomiar=voltage, L1=%.2f,L2=%.2f,L3=%.2f", U1, U2, U3);
return influx_send_post(line);
}
int influx_send_I(float I1, float I2, float I3) {
char line[256];
snprintf(line, sizeof(line), "orno,device=orno,pomiar=current L1=%.2f,L2=%.2f,L3=%.2f", I1, I2, I3);
return influx_send_post(line);
}
int influx_send_P(float P_Tot, float P1, float P2, float P3) {
char line[256];
snprintf(line, sizeof(line), "orno,device=orno,pomiar=power total=%.3f,L1=%.3f,L2=%.3f,L3=%.3f", P_Tot, P1, P2, P3);
return influx_send_post(line);
}
int influx_send_W(float W_Tot, float W1, float W2, float W3) {
char line[256];
snprintf(line, sizeof(line), "orno,device=orno,pomiar=energy total=%.3f,L1=%.3f,L2=%.3f,L3=%.3f", W_Tot, W1, W2, W3);
return influx_send_post(line);
}
int influx_send_Hz(float Hz) {
char line[256];
snprintf(line, sizeof(line), "orno,device=orno,pomiar=frequency value=%.4f", Hz);
return influx_send_post(line);
}
int influx_send_SUN2K(t_mb_reg *reg_to_send, char *_str_buf) {
if (!_str_buf || strlen(_str_buf) == 0) return 0;
char line[256];
/* reg_id used as tag, value as field */
snprintf(line, sizeof(line), "sun2k,device=sun2k,pomiar=%s value=%s", reg_to_send->reg_id, _str_buf);
return influx_send_post(line);
}
/* Validate float value - check if not NaN, Inf, or out of reasonable range */
int is_valid_float(float value, float min_val, float max_val)
{
if (isnan(value)) {
printf("VALIDATE: WARNING: Value is NaN\n");
return 0;
}
if (isinf(value)) {
printf("VALIDATE: WARNING: Value is Inf\n");
return 0;
}
if (value < min_val || value > max_val) {
printf("VALIDATE: WARNING: Value %.2f out of range [%.2f, %.2f]\n", value, min_val, max_val);
return 0;
}
return 1;
}
/* Timed wrapper for modbus_read_registers - logs start/stop and duration */
int modbus_read_timed(modbus_t *ctx, int addr, int nb, uint16_t *dest)
{
struct timeval t0, t1;
gettimeofday(&t0, NULL);
int res = modbus_read_registers(ctx, addr, nb, dest);
gettimeofday(&t1, NULL);
long ms = (t1.tv_sec - t0.tv_sec) * 1000 + (t1.tv_usec - t0.tv_usec) / 1000;
if (res < 0) {
printf("MBUS: modbus_read_registers addr=0x%X nb=%d -> ERR (%s) elapsed=%ldms\n", addr, nb, modbus_strerror(errno), ms);
} else {
printf("MBUS: modbus_read_registers addr=0x%X nb=%d -> OK (%d) elapsed=%ldms\n", addr, nb, res, ms);
}
return res;
}
int mosq_test()
{
mqtt_setup();
int i = -1000;
int k = 10;
char *buf = malloc(64);
while (k-- > 0)
{
sprintf(buf, "i=%i", i++);
int snd = mqtt_send("/testtopic", buf);
if (snd != 0)
printf("TEST: mqtt_send error=%i\n", snd);
usleep(5000000);
}
}
int main(int argc, char *argv[])
{
/* Użyj wartości z #define */
const char *USB_DEV = USB_DEV_DEFAULT;
int ORNO_SLAVE = ORNO_SLAVE_ADR;
int SUN2000_SLAVE = SUN2000_SLAVE_ADR;
int do_orno = READ_ORNO;
int do_sun2k = READ_SUN2K;
float voltage_buffer_L1[VOLTAGE_BUFFER_SIZE] = {0};
float voltage_buffer_L2[VOLTAGE_BUFFER_SIZE] = {0};
float voltage_buffer_L3[VOLTAGE_BUFFER_SIZE] = {0};
int voltage_buffer_index = 0;
int voltage_buffer_items = 0;
time_t high_frequency_mode_end_time = 0;
time_t rawtime;
struct tm *timeinfo;
time(&rawtime);
timeinfo = localtime(&rawtime);
printf("==============================================\n");
printf("Energy Meter Reader - ORNO & SUN2K\n");
printf("Start: %s", asctime(timeinfo));
printf("==============================================\n");
printf("CFG: Configuration:\n");
printf("CFG: Device: %s\n", USB_DEV);
printf("CFG: ORNO slave address: %d\n", ORNO_SLAVE);
printf("CFG: SUN2K slave address: %d\n", SUN2000_SLAVE);
printf("CFG: MQTT: %s\n", DISABLE_MQTT ? "DISABLED" : "ENABLED");
printf("CFG: MQTT Broker: %s:%d\n", MQTT_BROKER, MQTT_PORT);
printf("CFG: InfluxDB: %s (%s:%d)\n", DISABLE_INFLUX ? "DISABLED" : "ENABLED", INFLUX_HOST, INFLUX_PORT);
printf("==============================================\n\n");
mqtt_setup();
do
{
if (do_orno)
{
//Create a new RTU context with proper serial parameters (in this example,
//device name /dev/ttyS0, baud rate 9600, no parity bit, 8 data bits, 1 stop bit)
modbus_t *ctx = modbus_new_rtu(USB_DEV, 9600, 'E', 8, 1);
//Set the Modbus address of the remote slave (to 2)
modbus_set_slave(ctx, ORNO_SLAVE);
if (!ctx)
{
fprintf(stderr, "ORNO: Failed to create the context: %s\n", modbus_strerror(errno));
exit(1);
}
if (modbus_connect(ctx) == -1)
{
fprintf(stderr, "ORNO: Unable to connect: %s\n", modbus_strerror(errno));
modbus_free(ctx);
exit(1);
}
/* Enable libmodbus debug to print raw frames */
/* modbus_set_debug(ctx, TRUE); */ /* Disabled - causes timing issues */
/* Configure RTS delay and timeouts from #define */
modbus_rtu_set_rts_delay(ctx, ORNO_RTS_DELAY);
modbus_set_response_timeout(ctx, 0, 900000); /* 0.9s */
modbus_set_byte_timeout(ctx, 0, ORNO_BYTE_TIMEOUT);
/* Display current configuration */
int rts_delay = modbus_rtu_get_rts_delay(ctx);
uint32_t response_timeout_sec, response_timeout_usec;
uint32_t byte_timeout_sec, byte_timeout_usec;
modbus_get_response_timeout(ctx, &response_timeout_sec, &response_timeout_usec);
modbus_get_byte_timeout(ctx, &byte_timeout_sec, &byte_timeout_usec);
printf("ORNO: RTS Delay %u us\n", rts_delay);
printf("ORNO: Response Timeout %u,%06u s\n", response_timeout_sec, response_timeout_usec);
printf("ORNO: Byte Timeout %u,%06u s\n", byte_timeout_sec, byte_timeout_usec);
uint16_t reg[32]; // will store read registers values
uint16_t reg2[2]; // will store read registers values
float d = 0.0;
int num = 0;
int proba = 0;
float U1, U2, U3;
float I1, I2, I3;
float P_Tot, P1, P2, P3;
float W_Tot, W1, W2, W3;
float Freq;
long l = 0;
/* Flush serial buffer and wait for device to be ready */
modbus_flush(ctx);
usleep(100000); /* 100ms delay before first read */
memset((char *)reg, 0, sizeof(reg));
//NAPIECIA
num = modbus_read_timed(ctx, 0xe, 6, reg); // ORNO
// num = modbus_read_registers(ctx, 32072, 2, reg); // SUN2000
if (num != 6)
{ // number of read registers is not the one expected
printf("ORNO: %x \t\n", 0xe);
printf("ORNO: Failed to read: %s\n", modbus_strerror(errno));
}
else
{
U1 = modbus_get_float_abcd(&reg[0]);
U2 = modbus_get_float_abcd(&reg[2]);
U3 = modbus_get_float_abcd(&reg[4]);
printf("ORNO: Voltages: L1=%.1f V, L2=%.1f V, L3=%.1f V\n", U1, U2, U3);
// Add to circular buffer
voltage_buffer_L1[voltage_buffer_index] = U1;
voltage_buffer_L2[voltage_buffer_index] = U2;
voltage_buffer_L3[voltage_buffer_index] = U3;
voltage_buffer_index = (voltage_buffer_index + 1) % VOLTAGE_BUFFER_SIZE;
if (voltage_buffer_items < VOLTAGE_BUFFER_SIZE) {
voltage_buffer_items++;
}
float avg_L1 = 0, avg_L2 = 0, avg_L3 = 0;
for (int i = 0; i < voltage_buffer_items; i++) {
avg_L1 += voltage_buffer_L1[i];
avg_L2 += voltage_buffer_L2[i];
avg_L3 += voltage_buffer_L3[i];
}
avg_L1 /= voltage_buffer_items;
avg_L2 /= voltage_buffer_items;
avg_L3 /= voltage_buffer_items;
if (fabs(U1 - avg_L1) > avg_L1 * VOLTAGE_FLUCTUATION_THRESHOLD ||
fabs(U2 - avg_L2) > avg_L2 * VOLTAGE_FLUCTUATION_THRESHOLD ||
fabs(U3 - avg_L3) > avg_L3 * VOLTAGE_FLUCTUATION_THRESHOLD) {
printf("ORNO: Voltage fluctuation detected! Switching to high frequency polling for %d seconds.\n", HIGH_FREQ_MODE_DURATION);
high_frequency_mode_end_time = time(NULL) + HIGH_FREQ_MODE_DURATION;
}
/* Validate voltages (150V - 280V is reasonable range for EU grid) */
if (is_valid_float(U1, 150.0, 280.0) &&
is_valid_float(U2, 150.0, 280.0) &&
is_valid_float(U3, 150.0, 280.0)) {
mqtt_send_U(U1, U2, U3);
influx_send_U(U1, U2, U3);
printf("ORNO: MQTT: Published voltages\n");
} else {
printf("ORNO: MQTT: Skipping voltages - invalid values\n");
}
}
usleep(100000); /* 100ms delay between queries */
// PRADY (Currents)
memset((char *)reg, 0, sizeof(reg));
num = modbus_read_timed(ctx, 0x16, 6, reg); // ORNO
if (num != 6)
{
printf("ORNO: %x \t\n", 0x16);
printf("ORNO: Failed to read currents: %s\n", modbus_strerror(errno));
}
else
{
I1 = modbus_get_float_abcd(&reg[0]);
I2 = modbus_get_float_abcd(&reg[2]);
I3 = modbus_get_float_abcd(&reg[4]);
printf("ORNO: Currents: L1=%.2f A, L2=%.2f A, L3=%.2f A\n", I1, I2, I3);
/* Validate currents (0-100A is reasonable range for home use) */
if (is_valid_float(I1, 0.0, 100.0) &&
is_valid_float(I2, 0.0, 100.0) &&
is_valid_float(I3, 0.0, 100.0)) {
mqtt_send_I(I1, I2, I3);
influx_send_I(I1, I2, I3);
printf("ORNO: MQTT: Published currents\n");
} else {
printf("ORNO: MQTT: Skipping currents - invalid values\n");
}
}
usleep(100000); /* 100ms delay between queries */
memset((char *)reg, 0, sizeof(reg));
num = modbus_read_timed(ctx, 0x1c, 8, reg);
if (num != 8)
{ // number of read registers is not the one expected
printf("ORNO: %x \t\n", 0x1c);
printf("ORNO: Failed to read: %s\n", modbus_strerror(errno));
}
else
{
P_Tot = modbus_get_float_abcd(&reg[0]);
P1 = modbus_get_float_abcd(&reg[2]);
P2 = modbus_get_float_abcd(&reg[4]);
P3 = modbus_get_float_abcd(&reg[6]);
printf("ORNO: Power: Total=%.3f W, L1=%.3f W, L2=%.3f W, L3=%.3f W\n", P_Tot, P1, P2, P3);
/* Validate power (-25000W to +25000W, allows negative for generation) */
if (is_valid_float(P_Tot, -25000.0, 25000.0) &&
is_valid_float(P1, -10000.0, 10000.0) &&
is_valid_float(P2, -10000.0, 10000.0) &&
is_valid_float(P3, -10000.0, 10000.0)) {
mqtt_send_P(P_Tot, P1, P2, P3);
influx_send_P(P_Tot, P1, P2, P3);
printf("ORNO: MQTT: Published power\n");
} else {
printf("ORNO: MQTT: Skipping power - invalid values\n");
}
}
usleep(100000); /* 100ms delay between queries */
memset((char *)reg, 0, sizeof(reg));
num = modbus_read_timed(ctx, 0x100, 8, reg);
if (num != 8)
{ // number of read registers is not the one expected
printf("ORNO: %x \t\n", 0x100);
printf("ORNO: Failed to read: %s\n", modbus_strerror(errno));
}
else
{
W_Tot = modbus_get_float_abcd(&reg[0]);
W1 = modbus_get_float_abcd(&reg[2]);
W2 = modbus_get_float_abcd(&reg[4]);
W3 = modbus_get_float_abcd(&reg[6]);
printf("ORNO: Energy: Total=%.3f kWh, L1=%.3f kWh, L2=%.3f kWh, L3=%.3f kWh\n", W_Tot, W1, W2, W3);
/* Validate energy (0-1000000 kWh is reasonable range) */
if (is_valid_float(W_Tot, 0.0, 1000000.0) &&
is_valid_float(W1, 0.0, 1000000.0) &&
is_valid_float(W2, 0.0, 1000000.0) &&
is_valid_float(W3, 0.0, 1000000.0)) {
mqtt_send_W(W_Tot, W1, W2, W3);
influx_send_W(W_Tot, W1, W2, W3);
printf("ORNO: MQTT: Published energy\n");
} else {
printf("ORNO: MQTT: Skipping energy - invalid values\n");
}
}
usleep(10000); /* 10ms delay between queries */
// CZESTOTLIWOSC (Frequency)
memset((char *)reg, 0, sizeof(reg));
num = modbus_read_timed(ctx, 0x14, 2, reg); // ORNO
if (num != 2)
{
printf("ORNO: %x \t\n", 0x14);
printf("ORNO: Failed to read frequency: %s\n", modbus_strerror(errno));
}
else
{
Freq = modbus_get_float_abcd(&reg[0]);
printf("ORNO: Frequency: %.4f Hz\n", Freq);
/* Validate frequency (47-53 Hz is reasonable range for EU/US grid) */
if (is_valid_float(Freq, 47.0, 53.0)) {
mqtt_send_Hz(Freq);
influx_send_Hz(Freq);
printf("ORNO: MQTT: Published frequency\n");
} else {
printf("ORNO: MQTT: Skipping frequency - invalid value\n");
}
}
modbus_close(ctx);
modbus_free(ctx);
}
if (do_sun2k)
{
/* Delay between ORNO and SUN2K as configured */
if (do_orno) {
printf("\nSUN2K: Waiting %d ms before SUN2K...\n\n", DELAY_BETWEEN_DEVICES/1000);
usleep(DELAY_BETWEEN_DEVICES);
}
time_t rawtime;
struct tm *timeinfo;
time(&rawtime);
timeinfo = localtime(&rawtime);
printf("SUN2K: === SUN2K Reading Started: %s", asctime(timeinfo));
//Create a new RTU context with proper serial parameters (in this example,
//device name /dev/ttyS0, baud rate 9600, no parity bit, 8 data bits, 1 stop bit)
modbus_t *ctx = modbus_new_rtu(USB_DEV, 9600, 'N', 8, 1);
if (!ctx)
{
fprintf(stderr, "SUN2K: Failed to create the context: %s\n", modbus_strerror(errno));
exit(1);
}
//Set the Modbus address of the remote slave (to 3)
modbus_set_slave(ctx, SUN2000_SLAVE);
if (modbus_connect(ctx) == -1)
{
fprintf(stderr, "SUN2K: Unable to connect: %s\n", modbus_strerror(errno));
modbus_free(ctx);
exit(1);
}
/* Configure timeouts for SUN2K from #define */
modbus_rtu_set_rts_delay(ctx, SUN2K_RTS_DELAY);
modbus_set_response_timeout(ctx, 0, 900000); /* 0.9s */
modbus_set_byte_timeout(ctx, 0, SUN2K_BYTE_TIMEOUT);
/* Display current configuration */
int rts_delay = modbus_rtu_get_rts_delay(ctx);
uint32_t response_timeout_sec, response_timeout_usec;
uint32_t byte_timeout_sec, byte_timeout_usec;
modbus_get_response_timeout(ctx, &response_timeout_sec, &response_timeout_usec);
modbus_get_byte_timeout(ctx, &byte_timeout_sec, &byte_timeout_usec);
printf("SUN2K: RTS Delay %u us\n", rts_delay);
printf("SUN2K: Response Timeout %u,%06u s\n", response_timeout_sec, response_timeout_usec);
printf("SUN2K: Byte Timeout %u,%06u s\n", byte_timeout_sec, byte_timeout_usec);
/* Flush serial buffer and wait for device to be ready */
modbus_flush(ctx);
usleep(100000); /* 100ms delay before first read */
/* Enable libmodbus debug to print raw frames */
/* libmodbus debug disabled for normal runs */
/* modbus_set_debug(ctx, TRUE); */
uint16_t reg[32]; // will store read registers values
int num = 0;
int proba = 0;
char str_buf[32];
for (int i = 0; i < sizeof(mbReg) / sizeof(t_mb_reg); i++)
{
memset(str_buf, 0, sizeof(str_buf));
proba = 0;
do
{
num = modbus_read_timed(ctx, mbReg[i].ireg, mbReg[i].num_reg, reg);
} while ((num != mbReg[i].num_reg) && (proba++ < 10));
printf("SUN2K: %10i\t", mbReg[i].ireg);
printf("%u\t", reg[0]);
if (mbReg[i].num_reg == 2)
printf("%u\t", reg[1]);
else
printf("\t");
if (num != mbReg[i].num_reg)
{ // number of read registers is not the one expected
printf("%20.20s \t", mbReg[i].reg_id);
printf("SUN2K: Failed to %i read: %s\n", proba, modbus_strerror(errno));
continue;
}
else
{
printf("SUN2K: %20.20s \t", mbReg[i].reg_id);
switch (mbReg[i].type)
{
case rgStr:
{
for (int j = 0; j < mbReg[i].num_reg; j++)
{
printf("%c%c", (reg[j] / 256), (reg[j] % 256));
}
printf("\n");
break;
}
case rgData:
{
time_t td = reg[0] * 65536 + reg[1];
timeinfo = localtime(&td);
printf("%s", asctime(timeinfo));
sprintf(str_buf, "\"%s\"", asctime(timeinfo));
str_buf[strlen(str_buf) - 2] = '"';
str_buf[strlen(str_buf) - 1] = '\0';
break;
}
case rgI32:
{
int dh = reg[0];
int dl = reg[1];
int d = (dh << 16) + dl;
if (mbReg[i].scale == 1)
{
printf("%i", d);
sprintf(str_buf, "%i", d);
}
else
{
double f = d;
f /= mbReg[i].scale;
printf("%lf", f);
sprintf(str_buf, "%lf", f);
}
break;
}
case rgI16:
{
int d = reg[0];
if (mbReg[i].scale == 1)
{
printf("%i", d);
sprintf(str_buf, "%i", d);
}
else
{
double f = d;
f /= mbReg[i].scale;
printf("%lf", f);
sprintf(str_buf, "%lf", f);
}
break;
}
case rgU32:
{
unsigned int dh = reg[0];
unsigned int dl = reg[1];
unsigned int d = (dh << 16) + dl;
if (mbReg[i].scale == 1)
{
printf("%i", d);
sprintf(str_buf, "%i", d);
}
else
{
double f = d;
f /= mbReg[i].scale;
printf("%lf", f);
sprintf(str_buf, "%lf", f);
}
break;
}
case rgU16:
{
unsigned int d = reg[0];
if (mbReg[i].scale == 1)
{
printf("%i", d);
sprintf(str_buf, "%i", d);
}
else
{
double f = d;
f /= mbReg[i].scale;
printf("%lf", f);
sprintf(str_buf, "%lf", f);
}
break;
}
default:
{
for (int j = 0; j < mbReg[i].num_reg; j++)
{
printf("%i:", reg[j]);
sprintf(str_buf, "%i", reg[j]);
}
break;
}
}
if (mbReg[i].type != rgData)
printf(" %s\n", mbReg[i].units);
}
mqtt_send_SUN2K(&mbReg[i], str_buf);
influx_send_SUN2K(&mbReg[i], str_buf);
usleep(10000); /* 10ms delay between queries */
}
modbus_close(ctx);
modbus_free(ctx);
}
if (READ_LOOP) {
if (time(NULL) < high_frequency_mode_end_time) {
usleep(10000); // 10ms
} else {
sleep(5); // 5s
}
}
} while (READ_LOOP);
//mosq_test();
return 0;
}

BIN
modborno3 Executable file

Binary file not shown.

159
ornowe51x_copilot.py Executable file
View File

@@ -0,0 +1,159 @@
#!/usr/bin/env python3
# encoding: utf-8
# ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# Copyright (C) 2017 Piotr Murawski
# Niniejszy program jest wolnym oprogramowaniem; mozesz go
# rozprowadzac dalej i/lub modyfikowac na warunkach Powszechnej
# Licencji Publicznej GNU, wydanej przez Fundacje Wolnego
# Oprogramowania - wedlug wersji 3-ciej tej Licencji lub ktorejs
# z pozniejszych wersji.
# Niniejszy program rozpowszechniany jest z nadzieja iz bedzie on
# uzyteczny - jednak BEZ JAKIEJKOLWIEK GWARANCJI, nawet domyslnej
# gwarancji PRZYDATNOSCI HANDLOWEJ albo PRZYDATNOSCI DO OKRESLONYCH
# ZASTOSOWAN. W celu uzyskania blizszych informacji - Powszechna
# Licencja Publiczna GNU.
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# Z pewnoscia wraz z niniejszym programem otrzymales tez egzemplarz
# Powszechnej Licencji Publicznej GNU (GNU General Public License);
# jesli nie - napisz do Free Software Foundation, Inc., 675 Mass Ave,
# Cambridge, MA 02139, USA.
# Powszechna Licencja GNU dostepna jest rowniez na stronie:
# http://www.gnu.org/licenses/licenses.html
# nieoficjalne polskie tlumaczenie na
# http://www.gnu.org.pl
# ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import sys
import os
import energymeter
import math
import struct
import paho.mqtt.publish as publish
#publish.single("paho/test/single", "boo", hostname="mqtt.eclipse.org")
DEVICE_0="/dev/ttyUSB0"
ADDR=2
def mem2float(reg1, reg2):
# found on beloved "satckoverflow"
raw = struct.pack('>HH', reg1, reg2)
return struct.unpack('>f', raw)[0]
def float2mem(flo):
# found on beloved "satckoverflow"
raw = struct.pack('>f', flo)
regsy=struct.unpack('>HH', raw)
return [regsy[0], regsy[1]]
print("testing @ ", DEVICE_0)
for iter in range(50):
try:
print("\n--- Iteration %d ---" % (iter+1))
testEmeter=energymeter.testPort(DEVICE_0, ADDR)
#mbcli = createClient(portDevice, adres)
testEmeter = True
if testEmeter:
print("meter ok")
mbcli = energymeter.startClient(DEVICE_0, ADDR)
#print(regs)
regs=energymeter.readRegs(mbcli, 0xE,6) # ORNO
print()
print("voltage L1 ...", round(mem2float(regs[0], regs[1]), 1))
sU1 = round(mem2float(regs[0], regs[1]), 1)
publish.single("/energy/orno/U1", round(mem2float(regs[0], regs[1]),1), hostname="10.1.1.1")
print("voltage L2 ...", round(mem2float(regs[2], regs[3]), 1))
sU2 = round(mem2float(regs[2], regs[3]), 1)
publish.single("/energy/orno/U2", round(mem2float(regs[2], regs[3]),1), hostname="10.1.1.1")
print("voltage L3 ...", round(mem2float(regs[4], regs[5]), 1))
sU3 = round(mem2float(regs[4], regs[5]), 1)
publish.single("/energy/orno/U3", round(mem2float(regs[4], regs[5]),1), hostname="10.1.1.1")
sU = "{\"U1\":%f,\"U2\":%f,\"U3\":%f}" % (sU1, sU2, sU3)
print(sU)
publish.single("/energy/orno/U", sU, hostname="10.1.1.1")
regs=energymeter.readRegs(mbcli, 0x16,6)
print()
print("current L1 ...", round(mem2float(regs[0], regs[1]), 2))
sI1 = round(mem2float(regs[0], regs[1]), 2)
publish.single("/energy/orno/I1", round(mem2float(regs[0], regs[1]),2), hostname="10.1.1.1")
print("current L2 ...", round(mem2float(regs[2], regs[3]), 2))
sI2 = round(mem2float(regs[2], regs[3]), 2)
publish.single("/energy/orno/I2", round(mem2float(regs[2], regs[3]),2), hostname="10.1.1.1")
print("current L3 ...", round(mem2float(regs[4], regs[5]), 2))
sI3 = round(mem2float(regs[4], regs[5]), 2)
publish.single("/energy/orno/I3", round(mem2float(regs[4], regs[5]),2), hostname="10.1.1.1")
sI = "{\"I1\":%f,\"I2\":%f,\"I3\":%f}" % (sI1, sI2, sI3)
print(sI)
publish.single("/energy/orno/I", sI, hostname="10.1.1.1")
regs=energymeter.readRegs(mbcli, 0x1C,8)
print()
print("power total ..", round(mem2float(regs[0], regs[1]), 3))
sP_Tot = round(mem2float(regs[0], regs[1]), 3)
publish.single("/energy/orno/P_Tot", round(mem2float(regs[0], regs[1]),3), hostname="10.1.1.1")
print("power L1 ...", round(mem2float(regs[2], regs[3]), 3))
sP1 = round(mem2float(regs[2], regs[3]), 3)
publish.single("/energy/orno/P1", round(mem2float(regs[2], regs[3]),3), hostname="10.1.1.1")
print("power L2 ...", round(mem2float(regs[4], regs[5]), 3))
sP2 = round(mem2float(regs[4], regs[5]), 3)
publish.single("/energy/orno/P2", round(mem2float(regs[4], regs[5]),3), hostname="10.1.1.1")
print("power L3 ...", round(mem2float(regs[6], regs[7]), 3))
sP3 = round(mem2float(regs[6], regs[7]), 3)
publish.single("/energy/orno/P3", round(mem2float(regs[6], regs[7]),3), hostname="10.1.1.1")
sP = "{\"P_Tot\":%f,\"P1\":%f,\"P2\":%f,\"P3\":%f}" % (sP_Tot, sP1, sP2, sP3)
print(sP)
publish.single("/energy/orno/P", sP, hostname="10.1.1.1")
regs=energymeter.readRegs(mbcli, 0x100,8)
print()
print("energy total..", round(mem2float(regs[0], regs[1]), 3))
sW_Tot = round(mem2float(regs[0], regs[1]), 3)
publish.single("/energy/orno/W_Tot", round(mem2float(regs[0], regs[1]),3), hostname="10.1.1.1")
print("energy L1 ...", round(mem2float(regs[2], regs[3]), 3))
sW1 = round(mem2float(regs[2], regs[3]), 3)
publish.single("/energy/orno/W1", round(mem2float(regs[2], regs[3]),3), hostname="10.1.1.1")
print("energy L2 ...", round(mem2float(regs[4], regs[5]), 3))
sW2 = round(mem2float(regs[4], regs[5]), 3)
publish.single("/energy/orno/W2", round(mem2float(regs[4], regs[5]),3), hostname="10.1.1.1")
print("energy L3 ...", round(mem2float(regs[6], regs[7]), 3))
sW3 = round(mem2float(regs[6], regs[7]), 3)
publish.single("/energy/orno/W3", round(mem2float(regs[6], regs[7]), 3), hostname="10.1.1.1")
sW = "{\"W_Tot\":%f,\"W1\":%f,\"W2\":%f,\"W3\":%f}" % (sW_Tot, sW1, sW2, sW3)
print(sW)
publish.single("/energy/orno/W", sW, hostname="10.1.1.1")
regs=energymeter.readRegs(mbcli, 0x14,2)
print()
print("net frequency.", round(mem2float(regs[0], regs[1]), 4))
sFreq = round(mem2float(regs[0], regs[1]), 4)
publish.single("/energy/orno/Hz", round(mem2float(regs[0], regs[1]),4), hostname="10.1.1.1")
else:
print("ERROR: meter not found")
except Exception as e:
print('Iteration %d error:' % (iter+1), e)
finally:
import time
time.sleep(2)

25
read-energy.service Normal file
View File

@@ -0,0 +1,25 @@
[Unit]
Description=Energy Meter Reader (ORNO & SUN2K)
Documentation=file:///home/ms/projekty/python3/modebus/ornoserial3/CONFIG.md
After=network.target mosquitto.service
[Service]
Type=oneshot
User=ms
Group=dialout
WorkingDirectory=/home/ms/projekty/python3/modebus/ornoserial3
ExecStart=/home/ms/projekty/python3/modebus/ornoserial3/modborno3
StandardOutput=journal
StandardError=journal
SyslogIdentifier=read-energy
# Restart on failure
Restart=on-failure
RestartSec=5s
# Security settings
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target

16
read-energy.timer Normal file
View File

@@ -0,0 +1,16 @@
[Unit]
Description=Energy Meter Reader Timer (every 10s)
Documentation=file:///home/ms/projekty/python3/modebus/ornoserial3/CONFIG.md
Requires=read-energy.service
[Timer]
# Uruchom po starcie systemu po 30 sekundach
OnBootSec=30s
# Powtarzaj co 10 sekund
OnUnitActiveSec=10s
# Jeśli nie udało się uruchomić o czasie, uruchom jak najszybciej
Persistent=true
AccuracySec=1s
[Install]
WantedBy=timers.target