mirror of
https://github.com/lumapu/ahoy.git
synced 2025-06-03 19:21:38 +02:00
Added current version of format description. Added 'first contact' code example by of22.
This commit is contained in:
parent
59aa635ebe
commit
a7e73f6ae8
14 changed files with 1502 additions and 0 deletions
299
doc/hoymiles-format-description.txt
Normal file
299
doc/hoymiles-format-description.txt
Normal file
|
@ -0,0 +1,299 @@
|
|||
Ziel dieses Projekts
|
||||
====================
|
||||
|
||||
Anstelle der DTU wollen wir direkt von einem Arduino/RaspberryPi o.ä.
|
||||
die aktuellen Betriebsdaten der Wechselrichter auslesen.
|
||||
|
||||
Ohne Umweg über die "Cloud".
|
||||
|
||||
|
||||
|
||||
Systemaufbau
|
||||
============
|
||||
|
||||
- Eine "DTU" kommuniziert mit vielen Wechselrichtern.
|
||||
- Die Kommunikation geht immer von der DTU aus:
|
||||
DTU stellt Anfrage und erwartet eine Antwort vom WR.
|
||||
- Dafür muss die DTU die Adressen aller WR kennen.
|
||||
|
||||
|
||||
Nordic
|
||||
"Shockburst"
|
||||
2.4 GHz
|
||||
\|/ <-----------------> \|/
|
||||
| |
|
||||
+-------+ +-----------+
|
||||
| DTU | | MI-600 |
|
||||
+-------+ +-----------+-+
|
||||
| MI-600 |
|
||||
+-----------+-+
|
||||
| MI-1500 |
|
||||
+-----------+
|
||||
:
|
||||
:
|
||||
ABBILDUNG 1: Systemübersicht
|
||||
|
||||
|
||||
|
||||
Nordic
|
||||
WLAN "Shockburst"
|
||||
2.4 GHz
|
||||
\|/ \|/
|
||||
| |
|
||||
+---------+ +-----------+
|
||||
| ESP8266 | | NRF24LE1E |
|
||||
+---------+ +-----------+
|
||||
^ ^
|
||||
| |
|
||||
| +----------+ |
|
||||
+-----> | GD32F303 | <-----+
|
||||
(B) +----------+ (C)
|
||||
|
||||
ABBILDUNG 2: Innerer Aufbau "DTU"
|
||||
|
||||
|
||||
|
||||
Nordic
|
||||
"Shockburst"
|
||||
NRF24LE1E 2.4 GHz
|
||||
+------------------+ \|/
|
||||
+----------+ | | | |
|
||||
| GD32F303 | <----->| µC | NRF24L01+ |-------+
|
||||
+----------+ (C) | | |
|
||||
+------+-----------+
|
||||
|
||||
ABBILDUNG 3: Detailansicht GD32F303 - NRF24LE1E
|
||||
|
||||
|
||||
|
||||
Adressierung
|
||||
============
|
||||
|
||||
Die Seriennummern der DTU und der WR werden intern wie folgt
|
||||
als Adresse für die Kommunikation verwendet:
|
||||
|
||||
Beispiel: Seriennummer ....72818832
|
||||
|
||||
Innerhalb der Pakete auf (C) wird daraus die 4-Byte-Adresse
|
||||
0x72, 0x81, 0x88, 0x32 gebildet. Das ist die BCD-Darstellung
|
||||
der letzen 8 Dezimalziffern.
|
||||
|
||||
Die zugehörige Shockburst Zieladresse ist dieselbe, aber
|
||||
die Byte-Reihenfolge wird umgedreht, und eine 0x01 ergänzt
|
||||
(Shockburst ist auf 5-Byte-Adressen eingestellt).
|
||||
|
||||
Um eine Nachricht an das Gerät mit o.g. Seriennummer zu senden
|
||||
lautet die Zieladresse also (0x32, 0x88, 0x81, 0x72, 0x01).
|
||||
|
||||
|
||||
|
||||
Nachrichten
|
||||
===========
|
||||
|
||||
Nachricht: DTU an WR: "Init" (?)
|
||||
----------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
7E 07 00 00 00 00 00 00 00 00 00 07 7F
|
||||
^^ ^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^
|
||||
Bedeutung SOF MID WR ser# WR ser# ? CRC8 EOF
|
||||
?
|
||||
|
||||
|
||||
Nachricht: DTU an WR: "Init 2" (?)
|
||||
----------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
7E 07 72 81 88 32 72 81 88 32 00 07 7F
|
||||
^^ ^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^
|
||||
Bedeutung SOF MID DTU ser# DTU ser# ? CRC8 EOF
|
||||
Einheit BCD (letzte 8) BCD (letzte 8) ? ?
|
||||
Beispiel 72818832 72818832 ?
|
||||
|
||||
|
||||
|
||||
Nachricht 0x80: DTU an WR: "Zeit setzen" (?)
|
||||
----------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|<-------------CRC16 'modbus' für CRC_M----------------->|
|
||||
7E 15 72 22 02 00 72 22 02 00 80 0B 00 62 09 04 9b 00 00 00 00 00 00 00 00 F2 68 F0 7F
|
||||
^^ ^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^^^^^^^^^^ ^^^^^ ^^ ^^
|
||||
Bedeutung SOF MID WR ser# WR ser# CMD ? TIME (UTC) CRC_M CRC8 EOF
|
||||
Einheit BCD (letzte 8) BCD (letzte 8) ? [s] HI LO
|
||||
Beispiel 72220200 72220200 ? 2022-02-13
|
||||
13:16:11
|
||||
|
||||
|
||||
Nachricht 0x81: DTU an WR: "Anfrage DC-Daten" (?)
|
||||
----------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
GD->NRF 7E 15 70 51 43 68 70 51 43 68 81 xx 7F ...... (NOCH NICHT VERIFIZIERT / GESEHEN)
|
||||
^^^^^^^^^^^ ^^ ^^ ^^
|
||||
| (wird von CMD CRC8 EOF
|
||||
| NRF ersetzt) | (wird von NRF
|
||||
v v neu berechnet)
|
||||
|
||||
on-air 15 70 51 43 68 70 53 54 53 81 BA
|
||||
(payload) ^^^^^^^^^^^ ^^^^^^^^^^^
|
||||
WR ser # DTU ser #
|
||||
|
||||
|
||||
Nachricht 0x82: DTU an WR: "Anfrage AC-Daten" (?)
|
||||
----------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
GD->NRF 7E 15 70 51 43 68 70 51 43 68 82 xx 7F ...... (NOCH NICHT VERIFIZIERT / GESEHEN)
|
||||
^^^^^^^^^^^ ^^ ^^ ^^
|
||||
| (wird von CMD CRC8 EOF
|
||||
| NRF ersetzt) | (wird von NRF
|
||||
v v neu berechnet)
|
||||
|
||||
on-air 15 70 51 43 68 70 53 54 53 82 B9
|
||||
(payload) ^^^^^^^^^^^ ^^^^^^^^^^^
|
||||
WR ser # DTU ser #
|
||||
|
||||
|
||||
Nachricht 0x83: DTU an WR: "Anfrage DC-Daten" (?)
|
||||
----------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
GD->NRF 7E 15 70 51 43 68 70 51 43 68 83 xx 7F ...... (NOCH NICHT VERIFIZIERT / GESEHEN)
|
||||
^^^^^^^^^^^ ^^ ^^ ^^
|
||||
| (wird von CMD CRC8 EOF
|
||||
| NRF ersetzt) | (wird von NRF
|
||||
v v neu berechnet)
|
||||
|
||||
on-air 15 70 51 43 68 70 53 54 53 83 B8
|
||||
(payload) ^^^^^^^^^^^ ^^^^^^^^^^^
|
||||
WR ser # DTU ser #
|
||||
|
||||
|
||||
Nachricht 0x85: DTU an WR: "???" (?)
|
||||
----------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
GD->NRF 7E 15 70 51 43 68 70 51 43 68 85 xx 7F ...... (NOCH NICHT VERIFIZIERT / GESEHEN)
|
||||
^^^^^^^^^^^ ^^ ^^ ^^
|
||||
| (wird von CMD CRC8 EOF
|
||||
| NRF ersetzt) | (wird von NRF
|
||||
v v neu berechnet)
|
||||
|
||||
on-air 15 70 51 43 68 70 53 54 53 85 BE
|
||||
(payload) ^^^^^^^^^^^ ^^^^^^^^^^^
|
||||
WR ser # DTU ser #
|
||||
|
||||
|
||||
Nachricht 0xFF: DTU an WR: "???" (?)
|
||||
----------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
GD->NRF 7E 15 70 51 43 68 70 51 43 68 FF xx 7F ...... (NOCH NICHT VERIFIZIERT / GESEHEN)
|
||||
^^^^^^^^^^^ ^^ ^^ ^^
|
||||
| (wird von CMD CRC8 EOF
|
||||
| NRF ersetzt) | (wird von NRF
|
||||
v v neu berechnet)
|
||||
|
||||
on-air 15 70 51 43 68 70 53 54 53 FF C4
|
||||
(payload) ^^^^^^^^^^^ ^^^^^^^^^^^
|
||||
WR ser # DTU ser #
|
||||
|
||||
|
||||
Nachricht 0x01: WR an DTU: "Aktuelle DC Daten" (?)
|
||||
----------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
7E 95 72 22 02 00 72 22 02 00 01 00 01 01 4c 03 bd 0c 46 00 b5 00 03 00 05 00 00 BD 7F
|
||||
^^ ^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^ ^^
|
||||
Bedeutung SOF MID WR ser# WR ser# CMD ? PV1.u PV1.i PV1.p PV2.u PV2.i PV2.p ? CRC8 EOF
|
||||
Einheit BCD (letzte 8) BCD (letzte 8) ? [0.1V] [0.01A] [.1W] [0.1V] [0.01A] [.1W] ?
|
||||
Beispiel 72220200 72220200 ? 33.2V 9.57A 317.2W 18.1V 0.03A 0.5W ?
|
||||
|
||||
|
||||
Nachricht 0x02: WR an DTU: "Aktuelle AC Daten" (?)
|
||||
----------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
7E 95 72 22 02 00 72 22 02 00 02 28 23 00 00 24 44 00 3C 00 00 09 0F 13 88 0B D5 83 7F
|
||||
^^ ^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^^^^ ^^^^^ ^^^^^ ^^ ^^
|
||||
Bedeutung SOF MID WR ser# WR ser# CMD ? ? ? AC.u AC.f AC.p CRC8 EOF
|
||||
Einheit BCD (letzte 8) BCD (letzte 8) ? [0.1V] [0.01Hz] [0.1W]
|
||||
Beispiel 72220200 72220200 ? 9284 60 231.9V 50.00Hz 302.9W
|
||||
|
||||
|
||||
Nachricht 0x83: WR an DTU (?): "???" (nach CMD wäre das eher auch eine Antwort vom WR?)
|
||||
----------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
7E 95 72 22 02 00 72 22 02 00 83 00 03 00 83 03 E8 00 B2 00 0A FD 26 1E 7F
|
||||
^^ ^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^ ^^
|
||||
Bedeutung SOF MID WR ser# WR ser# CMD ? ? ? ? ? ? CRC8 EOF
|
||||
Einheit BCD (letzte 8) BCD (letzte 8) ?
|
||||
Beispiel 72220200 72220200 ? 131 1000 178 10
|
||||
|
||||
|
||||
|
||||
Hinweise
|
||||
========
|
||||
|
||||
Die "on-air (payload)" Bytes geben nur die Nutzlast der gesendeten Shockburst-Pakete an.
|
||||
Intern enthalten diese Pakete auch die Zieladresse, die Länge, eine CRC.
|
||||
|
||||
|
||||
Legende
|
||||
=======
|
||||
|
||||
MID: Message-ID. Antworten haben Bit 7 gesetzt,
|
||||
z.B. Frage 0x15 --> Antwort 0x95.
|
||||
z.B. Frage 0x07 --> Antwort 0x87.
|
||||
Für Kommunikation GD <--> NRF
|
||||
|
||||
CMD:
|
||||
Befehl an den WR hat Bit 7 gesetzt
|
||||
0x80 "Zeit setzen"
|
||||
0x81 "Anfrage DC-Daten", erwartete Antwort: 0x01
|
||||
0x82 "Anfrage AC-Daten", erwartete Antwort: 0x02
|
||||
0x83 "?"
|
||||
0x85 "?"
|
||||
0xFF "?"
|
||||
Antworten vom WR haben Bit 7 gelöscht:
|
||||
0x01 "Aktuelle DC-Daten"
|
||||
0x02 "Aktuelle AC-Daten"
|
||||
|
||||
SOF: Start-of-Frame 0x7e
|
||||
EOF: End-of-Frame 0x7f
|
||||
CRC8: CRC8 mit poly=1 init=0 xor=0, für alle Bytes zwischen SOF und CRC8.
|
||||
Beispiel in Python:
|
||||
>>> import crcmod
|
||||
>>> f = crcmod.mkCrcFun(0x101, initCrc=0, xorOut=0)
|
||||
>>> payload = bytes((0x95,0x72,0x22,0x02,0x00,0x72,0x22,0x02,0x00,0x83,0x00,0x03,0x00,0x83,0x03,0xE8,0x00,0xB2,0x00,0x0A,0xFD,0x26))
|
||||
>>> hex(f(payload))
|
||||
'0x1e'
|
||||
|
||||
CRC_M: CRC16 wie für "Modbus"-Protokoll, High-Byte gefolgt von Low-Byte
|
||||
Beispiel in Python:
|
||||
>>> import crcmod
|
||||
>>> f = crcmod.predefined.mkPredefinedCrcFun('modbus')
|
||||
>>> payload = bytes((0x0B,0x00,0x62,0x2F,0x45,0x96,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00))
|
||||
>>> hex(f(payload))
|
||||
'0x3bd6'
|
||||
|
||||
TIME: Aktuelle (DTU-)Zeit als Unix "time_t" (Sekunden seit 1970-01-01)
|
||||
|
||||
|
||||
Glossar
|
||||
=======
|
||||
|
||||
WR: Wechselrichter
|
||||
DTU: Data Terminal Unit (?). Die Hoymiles-Bezeichnung für den Kommunikations-Master.
|
||||
BCD: Binary Coded Decimal
|
||||
|
||||
|
||||
Notizen
|
||||
=======
|
||||
|
||||
0x014c = 332
|
||||
0x03bd = 957
|
||||
0x0c64 = 3172
|
||||
0x6209049b = 1644758171
|
||||
datetime.datetime.utcfromtimestamp(0x6209049b): datetime.datetime(2022, 2, 13, 13, 16, 11)
|
||||
|
||||
|
||||
Historie
|
||||
========
|
||||
|
||||
2022-03-09 / Petersilie / erste Version
|
||||
2022-03-10 / Petersilie / r2 / Nachrichten "02 28 23" und "82 00 03" ergänzt. Sauberer ausgerichtet. Python Beispiel für CRC.
|
||||
2022-03-12 / Petersilie / r3 / Erste on-air Formate hinzu. CMD-IDs hinzu. Neue Nachrichten von arnaldo_g hinzu. Übersicht hinzu.
|
||||
2022-03-15 / Petersilie / r4 / Nachricht 0x80: Mystery-Bytes am Ende "dechiffriert"
|
||||
2022-03-16 / Petersilie / r5 / ESP ist ein ESP8266, nicht ESP32 (danke an @tbnobody)
|
||||
2022-03-27 / Petersilie / Versionierung ab jetzt via Github.
|
153
tools/nano/NRF24_SendRcv/include/CircularBuffer.h
Normal file
153
tools/nano/NRF24_SendRcv/include/CircularBuffer.h
Normal file
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
CircularBuffer - An Arduino circular buffering library for arbitrary types.
|
||||
|
||||
Created by Ivo Pullens, Emmission, 2014 -- www.emmission.nl
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library 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
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef CircularBuffer_h
|
||||
#define CircularBuffer_h
|
||||
|
||||
#define DISABLE_IRQ \
|
||||
uint8_t sreg = SREG; \
|
||||
cli();
|
||||
|
||||
#define RESTORE_IRQ \
|
||||
SREG = sreg;
|
||||
|
||||
template <class T> class CircularBuffer
|
||||
{
|
||||
public:
|
||||
/** Constructor
|
||||
* @param buffer Preallocated buffer of at least size records.
|
||||
* @param size Number of records available in the buffer.
|
||||
*/
|
||||
CircularBuffer(T* buffer, const uint8_t size )
|
||||
: m_size(size), m_buff(buffer)
|
||||
{
|
||||
clear();
|
||||
}
|
||||
|
||||
/** Clear all entries in the circular buffer. */
|
||||
void clear(void)
|
||||
{
|
||||
m_front = 0;
|
||||
m_fill = 0;
|
||||
}
|
||||
|
||||
/** Test if the circular buffer is empty */
|
||||
inline bool empty(void) const
|
||||
{
|
||||
return !m_fill;
|
||||
}
|
||||
|
||||
/** Return the number of records stored in the buffer */
|
||||
inline uint8_t available(void) const
|
||||
{
|
||||
return m_fill;
|
||||
}
|
||||
|
||||
/** Test if the circular buffer is full */
|
||||
inline bool full(void) const
|
||||
{
|
||||
return m_fill == m_size;
|
||||
}
|
||||
|
||||
/** Aquire record on front of the buffer, for writing.
|
||||
* After filling the record, it has to be pushed to actually
|
||||
* add it to the buffer.
|
||||
* @return Pointer to record, or NULL when buffer is full.
|
||||
*/
|
||||
T* getFront(void) const
|
||||
{
|
||||
DISABLE_IRQ;
|
||||
T* f = NULL;
|
||||
if (!full())
|
||||
f = get(m_front);
|
||||
RESTORE_IRQ;
|
||||
return f;
|
||||
}
|
||||
|
||||
/** Push record to front of the buffer
|
||||
* @param record Record to push. If record was aquired previously (using getFront) its
|
||||
* data will not be copied as it is already present in the buffer.
|
||||
* @return True, when record was pushed successfully.
|
||||
*/
|
||||
bool pushFront(T* record)
|
||||
{
|
||||
bool ok = false;
|
||||
DISABLE_IRQ;
|
||||
if (!full())
|
||||
{
|
||||
T* f = get(m_front);
|
||||
if (f != record)
|
||||
*f = *record;
|
||||
m_front = (m_front+1) % m_size;
|
||||
m_fill++;
|
||||
ok = true;
|
||||
}
|
||||
RESTORE_IRQ;
|
||||
return ok;
|
||||
}
|
||||
|
||||
/** Aquire record on back of the buffer, for reading.
|
||||
* After reading the record, it has to be pop'ed to actually
|
||||
* remove it from the buffer.
|
||||
* @return Pointer to record, or NULL when buffer is empty.
|
||||
*/
|
||||
T* getBack(void) const
|
||||
{
|
||||
T* b = NULL;
|
||||
DISABLE_IRQ;
|
||||
if (!empty())
|
||||
b = get(back());
|
||||
RESTORE_IRQ;
|
||||
return b;
|
||||
}
|
||||
|
||||
/** Remove record from back of the buffer.
|
||||
* @return True, when record was pop'ed successfully.
|
||||
*/
|
||||
bool popBack(void)
|
||||
{
|
||||
bool ok = false;
|
||||
DISABLE_IRQ;
|
||||
if (!empty())
|
||||
{
|
||||
m_fill--;
|
||||
ok = true;
|
||||
}
|
||||
RESTORE_IRQ;
|
||||
return ok;
|
||||
}
|
||||
|
||||
protected:
|
||||
inline T * get(const uint8_t idx) const
|
||||
{
|
||||
return &(m_buff[idx]);
|
||||
}
|
||||
inline uint8_t back(void) const
|
||||
{
|
||||
return (m_front - m_fill + m_size) % m_size;
|
||||
}
|
||||
|
||||
const uint8_t m_size; // Total number of records that can be stored in the buffer.
|
||||
T* const m_buff; // Ptr to buffer holding all records.
|
||||
volatile uint8_t m_front; // Index of front element (not pushed yet).
|
||||
volatile uint8_t m_fill; // Amount of records currently pushed.
|
||||
};
|
||||
|
||||
#endif // CircularBuffer_h
|
8
tools/nano/NRF24_SendRcv/include/hm_crc.h
Normal file
8
tools/nano/NRF24_SendRcv/include/hm_crc.h
Normal file
|
@ -0,0 +1,8 @@
|
|||
|
||||
|
||||
#define BITS_TO_BYTES(x) (((x)+7)>>3)
|
||||
#define BYTES_TO_BITS(x) ((x)<<3)
|
||||
|
||||
extern uint16_t crc16_modbus(uint8_t *puchMsg, uint16_t usDataLen);
|
||||
extern uint8_t crc8(uint8_t *buf, const uint16_t bufLen);
|
||||
extern uint16_t crc16(uint8_t* buf, const uint16_t bufLen, const uint16_t startCRC, const uint16_t startBit, const uint16_t len_bits);
|
18
tools/nano/NRF24_SendRcv/include/hm_packets.h
Normal file
18
tools/nano/NRF24_SendRcv/include/hm_packets.h
Normal file
|
@ -0,0 +1,18 @@
|
|||
|
||||
|
||||
class HM_Packets
|
||||
{
|
||||
private:
|
||||
uint32_t unixTimeStamp;
|
||||
|
||||
void prepareBuffer(uint8_t *buf);
|
||||
void copyToBuffer(uint8_t *buf, uint32_t val);
|
||||
void copyToBufferBE(uint8_t *buf, uint32_t val);
|
||||
|
||||
public:
|
||||
void SetUnixTimeStamp(uint32_t ts);
|
||||
void UnixTimeStampTick();
|
||||
|
||||
int32_t GetTimePacket(uint8_t *buf, uint32_t wrAdr, uint32_t dtuAdr);
|
||||
int32_t GetCmdPacket(uint8_t *buf, uint32_t wrAdr, uint32_t dtuAdr, uint8_t mid, uint8_t cmd);
|
||||
};
|
47
tools/nano/NRF24_SendRcv/include/stdinout.cpp
Normal file
47
tools/nano/NRF24_SendRcv/include/stdinout.cpp
Normal file
|
@ -0,0 +1,47 @@
|
|||
#if ARDUINO >= 100
|
||||
#include "Arduino.h"
|
||||
#else
|
||||
#include "WProgram.h"
|
||||
#endif
|
||||
#include <stdio.h>
|
||||
#include "stdinout.h"
|
||||
|
||||
// Function that printf and related will use to print
|
||||
static int serial_putchar(char c, FILE *f)
|
||||
{
|
||||
if(c == '\n') {
|
||||
serial_putchar('\r', f);
|
||||
}
|
||||
|
||||
return Serial.write(c) == 1 ? 0 : 1;
|
||||
}
|
||||
// Function that scanf and related will use to read
|
||||
static int serial_getchar(FILE *)
|
||||
{
|
||||
// Wait until character is avilable
|
||||
while(Serial.available() <= 0) { ; }
|
||||
|
||||
return Serial.read();
|
||||
}
|
||||
|
||||
static FILE serial_stdinout;
|
||||
|
||||
static void setup_stdin_stdout()
|
||||
{
|
||||
// Set up stdout and stdin
|
||||
fdev_setup_stream(&serial_stdinout, serial_putchar, serial_getchar, _FDEV_SETUP_RW);
|
||||
stdout = &serial_stdinout;
|
||||
stdin = &serial_stdinout;
|
||||
stderr = &serial_stdinout;
|
||||
}
|
||||
|
||||
// Initialize the static variable to 0
|
||||
size_t initializeSTDINOUT::initnum = 0;
|
||||
|
||||
// Constructor that calls the function to set up stdin and stdout
|
||||
initializeSTDINOUT::initializeSTDINOUT()
|
||||
{
|
||||
if(initnum++ == 0) {
|
||||
setup_stdin_stdout();
|
||||
}
|
||||
}
|
17
tools/nano/NRF24_SendRcv/include/stdinout.h
Normal file
17
tools/nano/NRF24_SendRcv/include/stdinout.h
Normal file
|
@ -0,0 +1,17 @@
|
|||
#ifndef _STDINOUT_H
|
||||
#define _STDINOUT_H
|
||||
|
||||
// no need to make an instance of this yourself
|
||||
class initializeSTDINOUT
|
||||
{
|
||||
static size_t initnum;
|
||||
public:
|
||||
// Constructor
|
||||
initializeSTDINOUT();
|
||||
};
|
||||
|
||||
// Call the constructor in each compiled file this header is included in
|
||||
// static means the names won't collide
|
||||
static initializeSTDINOUT initializeSTDINOUT_obj;
|
||||
|
||||
#endif
|
46
tools/nano/NRF24_SendRcv/lib/README
Normal file
46
tools/nano/NRF24_SendRcv/lib/README
Normal file
|
@ -0,0 +1,46 @@
|
|||
|
||||
This directory is intended for project specific (private) libraries.
|
||||
PlatformIO will compile them to static libraries and link into executable file.
|
||||
|
||||
The source code of each library should be placed in a an own separate directory
|
||||
("lib/your_library_name/[here are source files]").
|
||||
|
||||
For example, see a structure of the following two libraries `Foo` and `Bar`:
|
||||
|
||||
|--lib
|
||||
| |
|
||||
| |--Bar
|
||||
| | |--docs
|
||||
| | |--examples
|
||||
| | |--src
|
||||
| | |- Bar.c
|
||||
| | |- Bar.h
|
||||
| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
|
||||
| |
|
||||
| |--Foo
|
||||
| | |- Foo.c
|
||||
| | |- Foo.h
|
||||
| |
|
||||
| |- README --> THIS FILE
|
||||
|
|
||||
|- platformio.ini
|
||||
|--src
|
||||
|- main.c
|
||||
|
||||
and a contents of `src/main.c`:
|
||||
```
|
||||
#include <Foo.h>
|
||||
#include <Bar.h>
|
||||
|
||||
int main (void)
|
||||
{
|
||||
...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
PlatformIO Library Dependency Finder will find automatically dependent
|
||||
libraries scanning project source files.
|
||||
|
||||
More information about PlatformIO Library Dependency Finder
|
||||
- https://docs.platformio.org/page/librarymanager/ldf.html
|
41
tools/nano/NRF24_SendRcv/platformio.ini
Normal file
41
tools/nano/NRF24_SendRcv/platformio.ini
Normal file
|
@ -0,0 +1,41 @@
|
|||
; PlatformIO Project Configuration File
|
||||
;
|
||||
; Build options: build flags, source filter
|
||||
; Upload options: custom upload port, speed and extra flags
|
||||
; Library options: dependencies, extra library storages
|
||||
; Advanced options: extra scripting
|
||||
;
|
||||
; Please visit documentation for the other options and examples
|
||||
; https://docs.platformio.org/page/projectconf.html
|
||||
|
||||
[env:uno]
|
||||
platform = atmelavr
|
||||
board = uno
|
||||
framework = arduino
|
||||
upload_protocol = avrispmkII
|
||||
upload_flags =
|
||||
-v
|
||||
-C
|
||||
$PROJECT_PACKAGES_DIR/tool-avrdude/avrdude.conf
|
||||
-p
|
||||
$BOARD_MCU
|
||||
-c
|
||||
avrispmkII
|
||||
upload_command = avrdude $UPLOAD_FLAGS -U flash:w:$SOURCE:i
|
||||
lib_deps = nrf24/RF24@^1.4.2
|
||||
|
||||
[env:nano]
|
||||
platform = atmelavr
|
||||
board = nanoatmega328
|
||||
framework = arduino
|
||||
upload_protocol = avrispmkII
|
||||
upload_flags =
|
||||
-v
|
||||
-C
|
||||
$PROJECT_PACKAGES_DIR/tool-avrdude/avrdude.conf
|
||||
-p
|
||||
$BOARD_MCU
|
||||
-c
|
||||
avrispmkII
|
||||
upload_command = avrdude $UPLOAD_FLAGS -U flash:w:$SOURCE:i
|
||||
lib_deps = nrf24/RF24@^1.4.2
|
543
tools/nano/NRF24_SendRcv/src/NRF24_sniff.cpp
Normal file
543
tools/nano/NRF24_SendRcv/src/NRF24_sniff.cpp
Normal file
|
@ -0,0 +1,543 @@
|
|||
#include <Arduino.h>
|
||||
#include <SPI.h>
|
||||
#include <CircularBuffer.h>
|
||||
#include <RF24.h>
|
||||
#include <RF24_config.h>
|
||||
|
||||
#include "hm_crc.h"
|
||||
#include "hm_packets.h"
|
||||
|
||||
// Macros
|
||||
#define DISABLE_EINT EIMSK = 0x00
|
||||
#define ENABLE_EINT EIMSK = 0x01
|
||||
|
||||
// Hardware configuration
|
||||
#define RF1_CE_PIN (9)
|
||||
#define RF1_CS_PIN (6)
|
||||
#define RF1_IRQ_PIN (2)
|
||||
|
||||
#define LED_PIN_STATUS (A0)
|
||||
|
||||
#define RF_MAX_ADDR_WIDTH (5) // Maximum address width, in bytes. MySensors use 5 bytes for addressing, where lowest byte is for node addressing.
|
||||
#define MAX_RF_PAYLOAD_SIZE (32)
|
||||
#define SER_BAUDRATE (115200)
|
||||
#define PACKET_BUFFER_SIZE (20) // Maximum number of packets that can be buffered between reception by NRF and transmission over serial port.
|
||||
|
||||
// Startup defaults until user reconfigures it
|
||||
#define DEFAULT_RECV_CHANNEL (3) // 3 = Default channel for Hoymiles
|
||||
#define DEFAULT_SEND_CHANNEL (40) // 40 = Default channel for Hoymiles
|
||||
#define DEFAULT_RF_DATARATE (RF24_250KBPS) // Datarate
|
||||
|
||||
#define DUMMY_RADIO_ID ((uint64_t)0xDEADBEEF01ULL)
|
||||
#define WR1_RADIO_ID ((uint64_t)0x1946107301ULL) // 0x1946107300ULL = WR1
|
||||
#define WR2_RADIO_ID ((uint64_t)0x3944107301ULL) // 0x3944107301ULL = WR2
|
||||
#define DTU_RADIO_ID ((uint64_t)0x1234567801ULL)
|
||||
|
||||
#include "NRF24_sniff_types.h"
|
||||
|
||||
unsigned long previousMillis = 0; // will store last time LED was updated
|
||||
const long interval = 250; // interval at which to blink (milliseconds)
|
||||
int ledState = LOW; // ledState used to set the LED
|
||||
|
||||
static HM_Packets hmPackets;
|
||||
static uint32_t tickMillis;
|
||||
static uint16_t tickSec;
|
||||
|
||||
static uint8_t sendBuf[MAX_RF_PAYLOAD_SIZE];
|
||||
|
||||
// Set up nRF24L01 radio on SPI bus plus CE/CS pins
|
||||
// If more than one RF24 unit is used the another CS pin than 10 must be used
|
||||
// This pin is used hard coded in SPI library
|
||||
static RF24 radio1(RF1_CE_PIN, RF1_CS_PIN);
|
||||
|
||||
static NRF24_packet_t bufferData[PACKET_BUFFER_SIZE];
|
||||
|
||||
static CircularBuffer<NRF24_packet_t> packetBuffer(bufferData, sizeof(bufferData) / sizeof(bufferData[0]));
|
||||
|
||||
static Serial_header_t serialHdr;
|
||||
|
||||
static uint16_t lastCRC;
|
||||
|
||||
static bool bOperate = true;
|
||||
static uint8_t checkCRC = 1;
|
||||
|
||||
int channels[] = {3, 23, 40, 61, 75};
|
||||
|
||||
// Function forward declaration
|
||||
static void DumpConfig();
|
||||
static void SendPacket(uint64_t dest, uint8_t *buf, uint8_t len);
|
||||
|
||||
inline static void dumpData(uint8_t *p, int len)
|
||||
{
|
||||
while (len--)
|
||||
{
|
||||
if (*p < 16)
|
||||
Serial.print(F("0"));
|
||||
Serial.print(*p++, HEX);
|
||||
}
|
||||
Serial.print(F(" "));
|
||||
}
|
||||
|
||||
static void handleNrf1Irq()
|
||||
{
|
||||
static uint8_t lostPacketCount = 0;
|
||||
uint8_t pipe;
|
||||
|
||||
// Loop until RX buffer(s) contain no more packets.
|
||||
while (radio1.available(&pipe))
|
||||
{
|
||||
if (!packetBuffer.full())
|
||||
{
|
||||
NRF24_packet_t *p = packetBuffer.getFront();
|
||||
p->timestamp = micros(); // Micros does not increase in interrupt, but it can be used.
|
||||
p->packetsLost = lostPacketCount;
|
||||
uint8_t packetLen = radio1.getPayloadSize();
|
||||
if (packetLen > MAX_RF_PAYLOAD_SIZE)
|
||||
packetLen = MAX_RF_PAYLOAD_SIZE;
|
||||
|
||||
radio1.read(p->packet, packetLen);
|
||||
|
||||
packetBuffer.pushFront(p);
|
||||
|
||||
lostPacketCount = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Buffer full. Increase lost packet counter.
|
||||
bool tx_ok, tx_fail, rx_ready;
|
||||
if (lostPacketCount < 255)
|
||||
lostPacketCount++;
|
||||
// Call 'whatHappened' to reset interrupt status.
|
||||
radio1.whatHappened(tx_ok, tx_fail, rx_ready);
|
||||
// Flush buffer to drop the packet.
|
||||
radio1.flush_rx();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void activateConf(void)
|
||||
{
|
||||
// Match MySensors' channel & datarate
|
||||
radio1.setChannel(DEFAULT_RECV_CHANNEL);
|
||||
radio1.setDataRate(DEFAULT_RF_DATARATE);
|
||||
|
||||
radio1.disableCRC();
|
||||
radio1.setAutoAck(0x00);
|
||||
|
||||
radio1.setPayloadSize(MAX_RF_PAYLOAD_SIZE);
|
||||
|
||||
// Configure listening pipe with the 'promiscuous' address and start listening
|
||||
radio1.setAddressWidth(5);
|
||||
radio1.openReadingPipe(1, DTU_RADIO_ID);
|
||||
|
||||
// Wen wan't only RX irqs
|
||||
radio1.maskIRQ(true, true, false);
|
||||
|
||||
// Use lo PA level, as a higher level will disturb CH340 serial usb adapter
|
||||
radio1.setPALevel(RF24_PA_LOW);
|
||||
|
||||
radio1.startListening();
|
||||
|
||||
// Attach interrupt handler to NRF IRQ output. Overwrites any earlier handler.
|
||||
attachInterrupt(digitalPinToInterrupt(RF1_IRQ_PIN), handleNrf1Irq, FALLING); // NRF24 Irq pin is active low.
|
||||
|
||||
// Initialize serial header's address member to promiscuous address.
|
||||
uint64_t addr = DTU_RADIO_ID;
|
||||
for (int8_t i = sizeof(serialHdr.address) - 1; i >= 0; --i)
|
||||
{
|
||||
serialHdr.address[i] = addr;
|
||||
addr >>= 8;
|
||||
}
|
||||
|
||||
DumpConfig();
|
||||
|
||||
tickMillis = millis() + 200;
|
||||
}
|
||||
|
||||
static void DumpConfig()
|
||||
{
|
||||
Serial.println(F("\nRadio 1:"));
|
||||
radio1.printDetails();
|
||||
|
||||
Serial.println("");
|
||||
}
|
||||
|
||||
static void DumpMenu()
|
||||
{
|
||||
Serial.println(F("\n\nConfiguration:"));
|
||||
Serial.println(F(" d: Dump current configuration"));
|
||||
Serial.println(F(" f: Filter packets for valid CRC"));
|
||||
Serial.println(F(" s: Start listening"));
|
||||
}
|
||||
|
||||
static void DumpPrompt()
|
||||
{
|
||||
Serial.print(F("\n?: "));
|
||||
}
|
||||
|
||||
static void Config(int cmd)
|
||||
{
|
||||
int i;
|
||||
|
||||
if (cmd == 'd')
|
||||
{
|
||||
activateConf();
|
||||
DumpConfig();
|
||||
}
|
||||
else if (cmd == 'f')
|
||||
{
|
||||
Serial.println(F("Filter for valid CRC: "));
|
||||
Serial.println(F(" 0: disable"));
|
||||
Serial.println(F(" 1: enable"));
|
||||
DumpPrompt();
|
||||
while (!Serial.available())
|
||||
;
|
||||
i = Serial.read() - 0x30;
|
||||
if ((i < 0) || (i > 1))
|
||||
{
|
||||
Serial.println(F("\nIllegal selection."));
|
||||
}
|
||||
else
|
||||
{
|
||||
Serial.print(F("\nCRC check changed to "));
|
||||
Serial.println(i);
|
||||
checkCRC = i;
|
||||
}
|
||||
}
|
||||
else if (cmd == 'h')
|
||||
{
|
||||
DumpMenu();
|
||||
}
|
||||
else if (cmd == 's')
|
||||
{
|
||||
activateConf();
|
||||
bOperate = true;
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
Serial.println(F("Unknown command. Type 'h' for command list."));
|
||||
}
|
||||
|
||||
DumpPrompt();
|
||||
}
|
||||
|
||||
void setup(void)
|
||||
{
|
||||
pinMode(LED_PIN_STATUS, OUTPUT);
|
||||
|
||||
Serial.begin(SER_BAUDRATE);
|
||||
Serial.flush();
|
||||
|
||||
hmPackets.SetUnixTimeStamp(0x623C8EA3);
|
||||
|
||||
Serial.println(F("-- Hoymiles test --"));
|
||||
|
||||
radio1.begin();
|
||||
|
||||
// Disable shockburst for receiving and decode payload manually
|
||||
radio1.setAutoAck(false);
|
||||
radio1.setRetries(0, 0);
|
||||
|
||||
// Configure nRF IRQ input
|
||||
pinMode(RF1_IRQ_PIN, INPUT);
|
||||
|
||||
activateConf();
|
||||
}
|
||||
|
||||
void loop(void)
|
||||
{
|
||||
while (!packetBuffer.empty())
|
||||
{
|
||||
if (!bOperate)
|
||||
{
|
||||
packetBuffer.popBack();
|
||||
continue;
|
||||
}
|
||||
|
||||
// One or more records present
|
||||
NRF24_packet_t *p = packetBuffer.getBack();
|
||||
|
||||
// Shift payload data due to 9-bit packet control field
|
||||
for (int16_t j = sizeof(p->packet) - 1; j >= 0; j--)
|
||||
{
|
||||
if (j > 0)
|
||||
p->packet[j] = (byte)(p->packet[j] >> 7) | (byte)(p->packet[j - 1] << 1);
|
||||
else
|
||||
p->packet[j] = (byte)(p->packet[j] >> 7);
|
||||
}
|
||||
|
||||
serialHdr.timestamp = p->timestamp;
|
||||
serialHdr.packetsLost = p->packetsLost;
|
||||
|
||||
// Check CRC
|
||||
uint16_t crc = 0xFFFF;
|
||||
crc = crc16((uint8_t *)&serialHdr.address, sizeof(serialHdr.address), crc, 0, BYTES_TO_BITS(sizeof(serialHdr.address)));
|
||||
// Payload length
|
||||
uint8_t payloadLen = ((p->packet[0] & 0x01) << 5) | (p->packet[1] >> 3);
|
||||
// Add one byte and one bit for 9-bit packet control field
|
||||
crc = crc16((uint8_t *)&p->packet[0], sizeof(p->packet), crc, 7, BYTES_TO_BITS(payloadLen + 1) + 1);
|
||||
|
||||
if (checkCRC)
|
||||
{
|
||||
// If CRC is invalid only show lost packets
|
||||
if (((crc >> 8) != p->packet[payloadLen + 2]) || ((crc & 0xFF) != p->packet[payloadLen + 3]))
|
||||
{
|
||||
if (p->packetsLost > 0)
|
||||
{
|
||||
Serial.print(F(" Lost: "));
|
||||
Serial.println(p->packetsLost);
|
||||
}
|
||||
packetBuffer.popBack();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Dump a decoded packet only once
|
||||
if (lastCRC == crc)
|
||||
{
|
||||
packetBuffer.popBack();
|
||||
continue;
|
||||
}
|
||||
lastCRC = crc;
|
||||
}
|
||||
|
||||
// Don't dump mysterious ack packages
|
||||
if(payloadLen == 0)
|
||||
{
|
||||
packetBuffer.popBack();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Write timestamp, packets lost, address and payload length
|
||||
printf(" %09lu ", serialHdr.timestamp);
|
||||
dumpData((uint8_t *)&serialHdr.packetsLost, sizeof(serialHdr.packetsLost));
|
||||
dumpData((uint8_t *)&serialHdr.address, sizeof(serialHdr.address));
|
||||
|
||||
// Trailing bit?!?
|
||||
dumpData(&p->packet[0], 2);
|
||||
|
||||
// Payload length from PCF
|
||||
dumpData(&payloadLen, sizeof(payloadLen));
|
||||
|
||||
// Packet control field - PID Packet identification
|
||||
uint8_t val = (p->packet[1] >> 1) & 0x03;
|
||||
Serial.print(val);
|
||||
Serial.print(F(" "));
|
||||
|
||||
if (payloadLen > 9)
|
||||
{
|
||||
dumpData(&p->packet[2], 1);
|
||||
dumpData(&p->packet[3], 4);
|
||||
dumpData(&p->packet[7], 4);
|
||||
|
||||
uint16_t remain = payloadLen - 2 - 1 - 4 - 4 + 4;
|
||||
|
||||
if (remain < 32)
|
||||
{
|
||||
dumpData(&p->packet[11], remain);
|
||||
printf_P(PSTR("%04X "), crc);
|
||||
|
||||
if (((crc >> 8) != p->packet[payloadLen + 2]) || ((crc & 0xFF) != p->packet[payloadLen + 3]))
|
||||
Serial.print(0);
|
||||
else
|
||||
Serial.print(1);
|
||||
}
|
||||
else
|
||||
{
|
||||
Serial.print(F("Ill remain "));
|
||||
Serial.print(remain);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
dumpData(&p->packet[2], payloadLen + 2);
|
||||
printf_P(PSTR("%04X "), crc);
|
||||
}
|
||||
|
||||
if (p->packetsLost > 0)
|
||||
{
|
||||
Serial.print(F(" Lost: "));
|
||||
Serial.print(p->packetsLost);
|
||||
}
|
||||
Serial.println(F(""));
|
||||
|
||||
// Remove record as we're done with it.
|
||||
packetBuffer.popBack();
|
||||
}
|
||||
|
||||
// Configuration using terminal commands
|
||||
if (Serial.available())
|
||||
{
|
||||
int cmd = Serial.read();
|
||||
if (bOperate && (cmd == 'c'))
|
||||
{
|
||||
bOperate = false;
|
||||
DumpMenu();
|
||||
DumpPrompt();
|
||||
}
|
||||
else if (bOperate && ((cmd == 's') || (cmd == 'w')))
|
||||
{
|
||||
uint64_t dest = cmd == 's' ? WR1_RADIO_ID : WR2_RADIO_ID;
|
||||
|
||||
int32_t size = hmPackets.GetCmdPacket((uint8_t *)&sendBuf, DTU_RADIO_ID >> 8, DTU_RADIO_ID >> 8, 0x07, 0x00);
|
||||
|
||||
SendPacket(dest, (uint8_t *)&sendBuf, size);
|
||||
}
|
||||
else if (bOperate && ((cmd == 'd') || (cmd == 'e')))
|
||||
{
|
||||
uint64_t dest = cmd == 'd' ? WR1_RADIO_ID : WR2_RADIO_ID;
|
||||
|
||||
int32_t size = hmPackets.GetTimePacket((uint8_t *)&sendBuf, dest >> 8, DTU_RADIO_ID >> 8);
|
||||
|
||||
SendPacket(dest, (uint8_t *)&sendBuf, size);
|
||||
}
|
||||
else if (bOperate && ((cmd == 'f') || (cmd == 'r')))
|
||||
{
|
||||
uint64_t dest = cmd == 'f' ? WR1_RADIO_ID : WR2_RADIO_ID;
|
||||
|
||||
int32_t size = hmPackets.GetCmdPacket((uint8_t *)&sendBuf, dest >> 8, DTU_RADIO_ID >> 8, 0x15, 0xFF);
|
||||
|
||||
SendPacket(dest, (uint8_t *)&sendBuf, size);
|
||||
}
|
||||
else if (bOperate && ((cmd == 'g') || (cmd == 't')))
|
||||
{
|
||||
uint64_t dest = cmd == 'g' ? WR1_RADIO_ID : WR2_RADIO_ID;
|
||||
|
||||
int32_t size = hmPackets.GetCmdPacket((uint8_t *)&sendBuf, dest >> 8, DTU_RADIO_ID >> 8, 0x15, 0x81);
|
||||
|
||||
SendPacket(dest, (uint8_t *)&sendBuf, size);
|
||||
}
|
||||
else if (bOperate && ((cmd == 'h') || (cmd == 'z')))
|
||||
{
|
||||
uint64_t dest = cmd == 'h' ? WR1_RADIO_ID : WR2_RADIO_ID;
|
||||
|
||||
int32_t size = hmPackets.GetCmdPacket((uint8_t *)&sendBuf, dest >> 8, DTU_RADIO_ID >> 8, 0x15, 0x82);
|
||||
|
||||
SendPacket(dest, (uint8_t *)&sendBuf, size);
|
||||
}
|
||||
else if (bOperate && (cmd == 'q'))
|
||||
{
|
||||
Serial.println(F("\nRadio1:"));
|
||||
radio1.printPrettyDetails();
|
||||
}
|
||||
else if (!bOperate)
|
||||
{
|
||||
Serial.println((char)cmd);
|
||||
Config(cmd);
|
||||
}
|
||||
}
|
||||
|
||||
// Status LED
|
||||
unsigned long currentMillis = millis();
|
||||
if ((currentMillis - previousMillis) >= interval)
|
||||
{
|
||||
// save the last time you blinked the LED
|
||||
previousMillis = currentMillis;
|
||||
// if the LED is off turn it on and vice-versa:
|
||||
ledState = not(ledState);
|
||||
// set the LED with the ledState of the variable:
|
||||
digitalWrite(LED_PIN_STATUS, ledState);
|
||||
}
|
||||
|
||||
// Second timer
|
||||
if (millis() >= tickMillis)
|
||||
{
|
||||
static uint8_t toggle = 0;
|
||||
static uint8_t tel = 0;
|
||||
tickMillis += 200;
|
||||
if (++tickSec >= 5)
|
||||
{
|
||||
hmPackets.UnixTimeStampTick();
|
||||
tickSec = 0;
|
||||
}
|
||||
|
||||
if (bOperate)
|
||||
{
|
||||
int32_t size;
|
||||
uint64_t dest = toggle ? WR1_RADIO_ID : WR2_RADIO_ID;
|
||||
|
||||
if (tel > 3)
|
||||
tel = 0;
|
||||
if (tel == 0)
|
||||
size = hmPackets.GetTimePacket((uint8_t *)&sendBuf, dest >> 8, DTU_RADIO_ID >> 8);
|
||||
else if (tel == 1)
|
||||
size = hmPackets.GetCmdPacket((uint8_t *)&sendBuf, dest >> 8, DTU_RADIO_ID >> 8, 0x15, 0x81);
|
||||
else if (tel == 2)
|
||||
size = hmPackets.GetCmdPacket((uint8_t *)&sendBuf, dest >> 8, DTU_RADIO_ID >> 8, 0x15, 0x80);
|
||||
else if (tel == 3)
|
||||
size = hmPackets.GetCmdPacket((uint8_t *)&sendBuf, dest >> 8, DTU_RADIO_ID >> 8, 0x15, 0x83);
|
||||
|
||||
SendPacket(dest, (uint8_t *)&sendBuf, size);
|
||||
|
||||
toggle = !toggle;
|
||||
if (!toggle)
|
||||
tel++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#define DEBUG_SEND 0
|
||||
static void SendPacket(uint64_t dest, uint8_t *buf, uint8_t len)
|
||||
{
|
||||
#if DEBUG_SEND
|
||||
Serial.print(F("Send... CH"));
|
||||
#endif
|
||||
DISABLE_EINT;
|
||||
radio1.stopListening();
|
||||
|
||||
#ifdef CHANNEL_HOP
|
||||
static uint8_t hop = 0;
|
||||
#if DEBUG_SEND
|
||||
if (channels[hop] < 10)
|
||||
Serial.print(F("0"));
|
||||
Serial.print(channels[hop]);
|
||||
#endif
|
||||
radio1.setChannel(channels[hop++]);
|
||||
if (hop >= sizeof(channels) / sizeof(channels[0]))
|
||||
hop = 0;
|
||||
#else
|
||||
#if DEBUG_SEND
|
||||
if (DEFAULT_SEND_CHANNEL < 10)
|
||||
Serial.print(F("0"));
|
||||
Serial.print(DEFAULT_SEND_CHANNEL);
|
||||
#endif
|
||||
radio1.setChannel(DEFAULT_SEND_CHANNEL);
|
||||
#endif
|
||||
|
||||
radio1.openWritingPipe(dest);
|
||||
#if DEBUG_SEND
|
||||
if (dest == WR1_RADIO_ID)
|
||||
Serial.print(F(" WR1 "));
|
||||
else
|
||||
Serial.print(F(" WR2 "));
|
||||
#endif
|
||||
|
||||
radio1.setCRCLength(RF24_CRC_16);
|
||||
radio1.enableDynamicPayloads();
|
||||
radio1.setAutoAck(true);
|
||||
radio1.setRetries(3, 15);
|
||||
|
||||
#if DEBUG_SEND
|
||||
uint32_t st = micros();
|
||||
bool res =
|
||||
#endif
|
||||
radio1.write(buf, len);
|
||||
#if DEBUG_SEND
|
||||
Serial.print(res);
|
||||
Serial.print(F(" "));
|
||||
Serial.print(micros());
|
||||
Serial.print(F(" "));
|
||||
Serial.println(micros() - st);
|
||||
#endif
|
||||
|
||||
// Try to avoid zero payload acks (has no effect)
|
||||
radio1.openWritingPipe(DUMMY_RADIO_ID);
|
||||
|
||||
radio1.setAutoAck(false);
|
||||
radio1.setRetries(0, 0);
|
||||
radio1.disableDynamicPayloads();
|
||||
radio1.setCRCLength(RF24_CRC_DISABLED);
|
||||
radio1.setChannel(DEFAULT_RECV_CHANNEL);
|
||||
radio1.startListening();
|
||||
ENABLE_EINT;
|
||||
}
|
55
tools/nano/NRF24_SendRcv/src/NRF24_sniff_types.h
Normal file
55
tools/nano/NRF24_SendRcv/src/NRF24_sniff_types.h
Normal file
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
This file is part of NRF24_Sniff.
|
||||
|
||||
Created by Ivo Pullens, Emmission, 2014 -- www.emmission.nl
|
||||
|
||||
NRF24_Sniff 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.
|
||||
|
||||
NRF24_Sniff 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.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with NRF24_Sniff. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef NRF24_sniff_types_h
|
||||
#define NRF24_sniff_types_h
|
||||
|
||||
typedef struct _NRF24_packet_t
|
||||
{
|
||||
uint32_t timestamp;
|
||||
uint8_t packetsLost;
|
||||
uint8_t packet[MAX_RF_PAYLOAD_SIZE];
|
||||
} NRF24_packet_t;
|
||||
|
||||
typedef struct _Serial_header_t
|
||||
{
|
||||
uint32_t timestamp;
|
||||
uint8_t packetsLost;
|
||||
uint8_t address[RF_MAX_ADDR_WIDTH]; // MSB first, always RF_MAX_ADDR_WIDTH bytes.
|
||||
} Serial_header_t;
|
||||
|
||||
typedef struct _Serial_config_t
|
||||
{
|
||||
uint8_t channel;
|
||||
uint8_t rate; // rf24_datarate_e: 0 = 1Mb/s, 1 = 2Mb/s, 2 = 250Kb/s
|
||||
uint8_t addressLen; // Number of bytes used in address, range [2..5]
|
||||
uint8_t addressPromiscLen; // Number of bytes used in promiscuous address, range [2..5]. E.g. addressLen=5, addressPromiscLen=4 => 1 byte unique identifier.
|
||||
uint64_t address; // Base address, LSB first.
|
||||
uint8_t crcLength; // Length of active CRC, range [0..2]
|
||||
uint8_t maxPayloadSize; // Maximum size of payload for nRF (including nRF header), range[4?..32]
|
||||
} Serial_config_t;
|
||||
|
||||
#define MSG_TYPE_PACKET (0)
|
||||
#define MSG_TYPE_CONFIG (1)
|
||||
|
||||
#define SET_MSG_TYPE(var,type) (((var) & 0x3F) | ((type) << 6))
|
||||
#define GET_MSG_TYPE(var) ((var) >> 6)
|
||||
#define GET_MSG_LEN(var) ((var) & 0x3F)
|
||||
|
||||
#endif // NRF24_sniff_types_h
|
142
tools/nano/NRF24_SendRcv/src/hm_crc.cpp
Normal file
142
tools/nano/NRF24_SendRcv/src/hm_crc.cpp
Normal file
|
@ -0,0 +1,142 @@
|
|||
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include "hm_crc.h"
|
||||
//#define OUTPUT_DEBUG_INFO
|
||||
|
||||
/* Table of CRC values for high-order byte */
|
||||
static const uint8_t auchCRCHi[] = {
|
||||
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
|
||||
0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
|
||||
0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01,
|
||||
0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
|
||||
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81,
|
||||
0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
|
||||
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01,
|
||||
0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
|
||||
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
|
||||
0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
|
||||
0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01,
|
||||
0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
|
||||
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
|
||||
0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
|
||||
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01,
|
||||
0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
|
||||
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
|
||||
0x40};
|
||||
|
||||
/* Table of CRC values for low-order byte */
|
||||
static const uint8_t auchCRCLo[] = {
|
||||
0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 0x07, 0xC7, 0x05, 0xC5, 0xC4,
|
||||
0x04, 0xCC, 0x0C, 0x0D, 0xCD, 0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09,
|
||||
0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A, 0x1E, 0xDE, 0xDF, 0x1F, 0xDD,
|
||||
0x1D, 0x1C, 0xDC, 0x14, 0xD4, 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3,
|
||||
0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3, 0xF2, 0x32, 0x36, 0xF6, 0xF7,
|
||||
0x37, 0xF5, 0x35, 0x34, 0xF4, 0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A,
|
||||
0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29, 0xEB, 0x2B, 0x2A, 0xEA, 0xEE,
|
||||
0x2E, 0x2F, 0xEF, 0x2D, 0xED, 0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26,
|
||||
0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60, 0x61, 0xA1, 0x63, 0xA3, 0xA2,
|
||||
0x62, 0x66, 0xA6, 0xA7, 0x67, 0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F,
|
||||
0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68, 0x78, 0xB8, 0xB9, 0x79, 0xBB,
|
||||
0x7B, 0x7A, 0xBA, 0xBE, 0x7E, 0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5,
|
||||
0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 0x70, 0xB0, 0x50, 0x90, 0x91,
|
||||
0x51, 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C,
|
||||
0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B, 0x99, 0x59, 0x58, 0x98, 0x88,
|
||||
0x48, 0x49, 0x89, 0x4B, 0x8B, 0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C,
|
||||
0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 0x43, 0x83, 0x41, 0x81, 0x80,
|
||||
0x40};
|
||||
|
||||
uint16_t crc16_modbus(uint8_t *puchMsg, uint16_t usDataLen)
|
||||
{
|
||||
uint8_t uchCRCHi = 0xFF; /* high byte of CRC initialized */
|
||||
uint8_t uchCRCLo = 0xFF; /* low byte of CRC initialized */
|
||||
uint16_t uIndex; /* will index into CRC lookup table */
|
||||
while (usDataLen--) /* pass through message buffer */
|
||||
{
|
||||
uIndex = uchCRCLo ^ *puchMsg++; /* calculate the CRC */
|
||||
uchCRCLo = uchCRCHi ^ auchCRCHi[uIndex];
|
||||
uchCRCHi = auchCRCLo[uIndex];
|
||||
}
|
||||
return (uchCRCHi << 8 | uchCRCLo);
|
||||
}
|
||||
|
||||
// Hoymiles CRC8 calculation with poly 0x01, Initial value 0x00 and final XOR 0x00
|
||||
uint8_t crc8(uint8_t *buf, const uint16_t bufLen)
|
||||
{
|
||||
uint32_t crc;
|
||||
uint16_t i, bit;
|
||||
|
||||
crc = 0x00;
|
||||
for (i = 0; i < bufLen; i++)
|
||||
{
|
||||
crc ^= buf[i];
|
||||
for (bit = 0; bit < 8; bit++)
|
||||
{
|
||||
if ((crc & 0x80) != 0)
|
||||
{
|
||||
crc <<= 1;
|
||||
crc ^= 0x01;
|
||||
}
|
||||
else
|
||||
{
|
||||
crc <<= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (crc & 0xFF);
|
||||
}
|
||||
|
||||
// NRF24 CRC16 calculation with poly 0x1021 = (1) 0001 0000 0010 0001 = x^16+x^12+x^5+1
|
||||
uint16_t crc16(uint8_t *buf, const uint16_t bufLen, const uint16_t startCRC, const uint16_t startBit, const uint16_t len_bits)
|
||||
{
|
||||
uint16_t crc = startCRC;
|
||||
if ((len_bits > 0) && (len_bits <= BYTES_TO_BITS(bufLen)))
|
||||
{
|
||||
// The length of the data might not be a multiple of full bytes.
|
||||
// Therefore we proceed over the data bit-by-bit (like the NRF24 does) to
|
||||
// calculate the CRC.
|
||||
uint16_t data;
|
||||
uint8_t byte, shift;
|
||||
uint16_t bitoffs = startBit;
|
||||
|
||||
// Get a new byte for the next 8 bits.
|
||||
byte = buf[bitoffs >> 3];
|
||||
#ifdef OUTPUT_DEBUG_INFO
|
||||
printf("\nStart CRC %04X, %u bits:", startCRC, len_bits);
|
||||
printf("\nbyte %02X:", byte);
|
||||
#endif
|
||||
while (bitoffs < len_bits + startBit)
|
||||
{
|
||||
shift = bitoffs & 7;
|
||||
// Shift the active bit to the position of bit 15
|
||||
data = ((uint16_t)byte) << (8 + shift);
|
||||
#ifdef OUTPUT_DEBUG_INFO
|
||||
printf(" bit %u %u,", shift, data & 0x8000 ? 1 : 0);
|
||||
#endif
|
||||
// Assure all other bits are 0
|
||||
data &= 0x8000;
|
||||
crc ^= data;
|
||||
if (crc & 0x8000)
|
||||
{
|
||||
crc = (crc << 1) ^ 0x1021; // 0x1021 = (1) 0001 0000 0010 0001 = x^16+x^12+x^5+1
|
||||
}
|
||||
else
|
||||
{
|
||||
crc = (crc << 1);
|
||||
}
|
||||
++bitoffs;
|
||||
if (0 == (bitoffs & 7))
|
||||
{
|
||||
// Get a new byte for the next 8 bits.
|
||||
byte = buf[bitoffs >> 3];
|
||||
#ifdef OUTPUT_DEBUG_INFO
|
||||
printf("crc %04X:", crc);
|
||||
if (bitoffs < len_bits + startBit)
|
||||
printf("\nbyte %02X:", byte);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
74
tools/nano/NRF24_SendRcv/src/hm_packets.cpp
Normal file
74
tools/nano/NRF24_SendRcv/src/hm_packets.cpp
Normal file
|
@ -0,0 +1,74 @@
|
|||
#include "Arduino.h"
|
||||
|
||||
#include "hm_crc.h"
|
||||
#include "hm_packets.h"
|
||||
|
||||
void HM_Packets::SetUnixTimeStamp(uint32_t ts)
|
||||
{
|
||||
unixTimeStamp = ts;
|
||||
}
|
||||
|
||||
void HM_Packets::UnixTimeStampTick()
|
||||
{
|
||||
unixTimeStamp++;
|
||||
}
|
||||
|
||||
void HM_Packets::prepareBuffer(uint8_t *buf)
|
||||
{
|
||||
// minimal buffer size of 32 bytes is assumed
|
||||
memset(buf, 0x00, 32);
|
||||
}
|
||||
|
||||
void HM_Packets::copyToBuffer(uint8_t *buf, uint32_t val)
|
||||
{
|
||||
buf[0]= (uint8_t)(val >> 24);
|
||||
buf[1]= (uint8_t)(val >> 16);
|
||||
buf[2]= (uint8_t)(val >> 8);
|
||||
buf[3]= (uint8_t)(val & 0xFF);
|
||||
}
|
||||
|
||||
void HM_Packets::copyToBufferBE(uint8_t *buf, uint32_t val)
|
||||
{
|
||||
memcpy(buf, &val, sizeof(uint32_t));
|
||||
}
|
||||
|
||||
|
||||
int32_t HM_Packets::GetTimePacket(uint8_t *buf, uint32_t wrAdr, uint32_t dtuAdr)
|
||||
{
|
||||
prepareBuffer(buf);
|
||||
|
||||
buf[0] = 0x15;
|
||||
copyToBufferBE(&buf[1], wrAdr);
|
||||
copyToBufferBE(&buf[5], dtuAdr);
|
||||
buf[9] = 0x80;
|
||||
buf[10] = 0x0B;
|
||||
buf[11] = 0x00;
|
||||
|
||||
copyToBuffer(&buf[12], unixTimeStamp);
|
||||
|
||||
buf[19] = 0x05;
|
||||
|
||||
// CRC16
|
||||
uint16_t crc16 = crc16_modbus(&buf[10], 14);
|
||||
buf[24] = crc16 >> 8;
|
||||
buf[25] = crc16 & 0xFF;
|
||||
|
||||
// crc8
|
||||
buf[26] = crc8(&buf[0], 26);
|
||||
|
||||
return 27;
|
||||
}
|
||||
|
||||
int32_t HM_Packets::GetCmdPacket(uint8_t *buf, uint32_t wrAdr, uint32_t dtuAdr, uint8_t mid, uint8_t cmd)
|
||||
{
|
||||
buf[0] = mid;
|
||||
copyToBufferBE(&buf[1], wrAdr);
|
||||
copyToBufferBE(&buf[5], dtuAdr);
|
||||
buf[9] = cmd;
|
||||
|
||||
// crc8
|
||||
buf[10] = crc8(&buf[0], 10);
|
||||
|
||||
return 11;
|
||||
}
|
||||
|
48
tools/nano/NRF24_SendRcv/src/stdinout.cpp
Normal file
48
tools/nano/NRF24_SendRcv/src/stdinout.cpp
Normal file
|
@ -0,0 +1,48 @@
|
|||
#if ARDUINO >= 100
|
||||
#include "Arduino.h"
|
||||
#else
|
||||
#include "WProgram.h"
|
||||
#endif
|
||||
#include <stdio.h>
|
||||
#include "stdinout.h"
|
||||
|
||||
// Function that printf and related will use to print
|
||||
static int serial_putchar(char c, FILE *f)
|
||||
{
|
||||
if(c == '\n') {
|
||||
serial_putchar('\r', f);
|
||||
}
|
||||
|
||||
return Serial.write(c) == 1 ? 0 : 1;
|
||||
}
|
||||
// Function that scanf and related will use to read
|
||||
static int serial_getchar(FILE *)
|
||||
{
|
||||
// Wait until character is avilable
|
||||
while(Serial.available() <= 0) { ; }
|
||||
int ch = Serial.read();
|
||||
Serial.write(ch);
|
||||
return ch;
|
||||
}
|
||||
|
||||
static FILE serial_stdinout;
|
||||
|
||||
static void setup_stdin_stdout()
|
||||
{
|
||||
// Set up stdout and stdin
|
||||
fdev_setup_stream(&serial_stdinout, serial_putchar, serial_getchar, _FDEV_SETUP_RW);
|
||||
stdout = &serial_stdinout;
|
||||
stdin = &serial_stdinout;
|
||||
stderr = &serial_stdinout;
|
||||
}
|
||||
|
||||
// Initialize the static variable to 0
|
||||
size_t initializeSTDINOUT::initnum = 0;
|
||||
|
||||
// Constructor that calls the function to set up stdin and stdout
|
||||
initializeSTDINOUT::initializeSTDINOUT()
|
||||
{
|
||||
if(initnum++ == 0) {
|
||||
setup_stdin_stdout();
|
||||
}
|
||||
}
|
11
tools/nano/NRF24_SendRcv/test/README
Normal file
11
tools/nano/NRF24_SendRcv/test/README
Normal file
|
@ -0,0 +1,11 @@
|
|||
|
||||
This directory is intended for PlatformIO Unit Testing and project tests.
|
||||
|
||||
Unit Testing is a software testing method by which individual units of
|
||||
source code, sets of one or more MCU program modules together with associated
|
||||
control data, usage procedures, and operating procedures, are tested to
|
||||
determine whether they are fit for use. Unit testing finds problems early
|
||||
in the development cycle.
|
||||
|
||||
More information about PlatformIO Unit Testing:
|
||||
- https://docs.platformio.org/page/plus/unit-testing.html
|
Loading…
Add table
Add a link
Reference in a new issue