Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,15 @@ The factory firmware is based on an [Arduino example sketch](examples/factory-v1

This repository provides an Arduino library that implements a driver for the magnetic sensors and computes wind speed and direction, exposing the values through a simple API.

[**How to install WindNerd Core library and program the board**](docs/PROGRAM.md)
[**How to install WindNerd Core library and program the board**](docs/PROGRAM.md)

## 4G Integration

The WindNerd Core can be integrated with 4G modules for remote data transmission. An example implementation with the **Luat Air780E** 4G board is provided, featuring:

- Periodic wake-up every 15 minutes
- Automatic transmission of wind speed and direction data
- Power-efficient operation with sleep mode
- AT command-based communication protocol

[**4G Integration Guide**](docs/4G-INTEGRATION.md) | [**Example Sketch**](examples/4g-air780e/)
127 changes: 127 additions & 0 deletions examples/air780e/MD5.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#include "MD5.h"
#include <cstdint>
#include <cstring>
#include <cstdlib>
#include <cstdio>

namespace {

inline uint32_t leftrotate(uint32_t x, uint32_t c) {
return (x << c) | (x >> (32 - c));
}

void md5(const unsigned char* initial_msg, size_t initial_len, unsigned char* digest) {
static const uint32_t r[] = {
7,12,17,22, 7,12,17,22, 7,12,17,22, 7,12,17,22,
5,9,14,20, 5,9,14,20, 5,9,14,20, 5,9,14,20,
4,11,16,23, 4,11,16,23, 4,11,16,23, 4,11,16,23,
6,10,15,21, 6,10,15,21, 6,10,15,21, 6,10,15,21
};

static const uint32_t k[] = {
0xd76aa478,0xe8c7b756,0x242070db,0xc1bdceee,
0xf57c0faf,0x4787c62a,0xa8304613,0xfd469501,
0x698098d8,0x8b44f7af,0xffff5bb1,0x895cd7be,
0x6b901122,0xfd987193,0xa679438e,0x49b40821,
0xf61e2562,0xc040b340,0x265e5a51,0xe9b6c7aa,
0xd62f105d,0x02441453,0xd8a1e681,0xe7d3fbc8,
0x21e1cde6,0xc33707d6,0xf4d50d87,0x455a14ed,
0xa9e3e905,0xfcefa3f8,0x676f02d9,0x8d2a4c8a,
0xfffa3942,0x8771f681,0x6d9d6122,0xfde5380c,
0xa4beea44,0x4bdecfa9,0xf6bb4b60,0xbebfbc70,
0x289b7ec6,0xeaa127fa,0xd4ef3085,0x04881d05,
0xd9d4d039,0xe6db99e5,0x1fa27cf8,0xc4ac5665,
0xf4292244,0x432aff97,0xab9423a7,0xfc93a039,
0x655b59c3,0x8f0ccc92,0xffeff47d,0x85845dd1,
0x6fa87e4f,0xfe2ce6e0,0xa3014314,0x4e0811a1,
0xf7537e82,0xbd3af235,0x2ad7d2bb,0xeb86d391
};

uint32_t h0 = 0x67452301;
uint32_t h1 = 0xefcdab89;
uint32_t h2 = 0x98badcfe;
uint32_t h3 = 0x10325476;

size_t new_len = (((initial_len + 8) / 64) + 1) * 64;
unsigned char* msg = (unsigned char*)calloc(new_len + 64, 1);
if (!msg) return;
memcpy(msg, initial_msg, initial_len);
msg[initial_len] = 0x80;

uint64_t bits_len = (uint64_t)initial_len * 8;
memcpy(msg + new_len - 8, &bits_len, 8);

for (size_t offset = 0; offset < new_len; offset += 64) {
uint32_t w[16];
for (int i = 0; i < 16; ++i) {
const unsigned char* p = msg + offset + i*4;
w[i] = (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
}

uint32_t a = h0;
uint32_t b = h1;
uint32_t c = h2;
uint32_t d = h3;

for (uint32_t i = 0; i < 64; ++i) {
uint32_t f, g;
if (i < 16) {
f = (b & c) | ((~b) & d);
g = i;
} else if (i < 32) {
f = (d & b) | ((~d) & c);
g = (5*i + 1) % 16;
} else if (i < 48) {
f = b ^ c ^ d;
g = (3*i + 5) % 16;
} else {
f = c ^ (b | (~d));
g = (7*i) % 16;
}
uint32_t tmp = d;
d = c;
c = b;
uint32_t to_add = a + f + k[i] + w[g];
b = b + leftrotate(to_add, r[i]);
a = tmp;
}

h0 += a;
h1 += b;
h2 += c;
h3 += d;
}

free(msg);

memcpy(digest + 0, &h0, 4);
memcpy(digest + 4, &h1, 4);
memcpy(digest + 8, &h2, 4);
memcpy(digest + 12, &h3, 4);
}

} // namespace

unsigned char* MD5::make_hash(char* input) {
if (!input) return nullptr;
size_t len = strlen(input);
unsigned char* out = (unsigned char*)malloc(16);
if (!out) return nullptr;
md5((const unsigned char*)input, len, out);
return out;
}

char* MD5::make_digest(const unsigned char* hash, std::size_t len) {
if (!hash || len == 0) return nullptr;
const char hex[] = "0123456789abcdef";
size_t out_len = len * 2 + 1;
char* out = (char*)malloc(out_len);
if (!out) return nullptr;
for (size_t i = 0; i < len; ++i) {
unsigned char v = hash[i];
out[i*2] = hex[(v >> 4) & 0x0F];
out[i*2 + 1] = hex[v & 0x0F];
}
out[out_len - 1] = '\0';
return out;
}
17 changes: 17 additions & 0 deletions examples/air780e/MD5.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#ifndef MD5_H
#define MD5_H

#include <cstddef>

class MD5 {
public:
// Compute raw 16-byte MD5 hash of a NUL-terminated C string.
// Caller must free() the returned buffer using free().
static unsigned char* make_hash(char* input);

// Convert raw hash bytes into a lowercase hex NUL-terminated string.
// Caller must free() the returned buffer using free().
static char* make_digest(const unsigned char* hash, std::size_t len);
};

#endif // MD5_H
144 changes: 144 additions & 0 deletions examples/air780e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Air780E Dual Upload - WindNerd WTP + Windguru

This example uploads wind data to **both** WindNerd WTP and Windguru platforms using the Air780E 4G LTE modem. Designed for **long-term unattended deployment** with solar + battery power.

## Features

- **Dual platform upload**: Sends data to WindNerd WTP (POST) and Windguru (GET)
- **Auto APN detection**: Modem auto-detects carrier APN (fallback configurable)
- **Unique salt generation**: STM32 UID + 32-bit random (64-bit total, collision-proof)
- **Deep sleep**: Both MCU and modem enter low power modes between uploads
- **Bearer management**: Proper SAPBR setup for Air780E module
- **Configurable intervals**: Easy to adjust reporting frequency
- **Data efficient**: Optional sample upload toggle saves ~90% data

### Reliability Features (for unattended operation)

- **Independent Watchdog (IWDG)**: Auto-resets MCU if firmware hangs (10s timeout)
- **State timeout**: Aborts upload cycle if any state takes >90 seconds
- **Periodic modem reset**: AT+CFUN=1,1 every 96 cycles (~24 hours at 15-min intervals)

## Configuration

Edit these values in the sketch:

\`\`\`cpp
// WindNerd WTP
#define WTP_SECRET_KEY "your-secret-key" // From windnerd.net device settings
#define ENABLE_WTP_SAMPLES false // true = send 3-sec samples, false = reports only

// Windguru
#define WINDGURU_UID "your-station-uid" // From windguru.cz station settings
#define WINDGURU_PASSWORD "your-password" // Upload password
#define ENABLE_WINDGURU true // Set false to disable Windguru uploads

// Network
#define APN_FALLBACK "internet" // Fallback APN (usually auto-detected)

// Timing
#define REPORT_INTERVAL_MN 15 // Minutes between uploads

// Reliability
#define WATCHDOG_TIMEOUT_SEC 10 // MCU reset if stuck for 10s
#define STATE_TIMEOUT_SEC 90 // Abort cycle if state exceeds 90s
#define MODEM_RESET_CYCLES 96 // Full modem reset every N cycles
\`\`\`

## Data Sent

### WindNerd WTP (HTTP POST)
- \`k=\` - Device secret key
- \`r,wa=,wd=,wn=,wx=\` - Report lines (avg, direction, min, max)
- \`s,wi=,wd=\` - Sample lines (optional, if ENABLE_WTP_SAMPLES=true)

### Windguru (HTTP GET)
| Parameter | Description |
|-----------|-------------|
| \`uid\` | Station UID |
| \`salt\` | Unique ID (STM32 UID + random) |
| \`hash\` | MD5(salt + uid + password) |
| \`interval\` | Measurement interval in seconds |
| \`wind_avg\` | Average wind speed (knots) |
| \`wind_max\` | Maximum wind speed (knots) |
| \`wind_min\` | Minimum wind speed (knots) |
| \`wind_direction\` | Wind direction (degrees) |

## Upload Sequence

1. **Diagnostics**: CSQ (signal), CREG (GSM), CEREG (LTE), CGATT (GPRS)
2. **APN Query**: AT+CGCONTRDP (auto-detect carrier APN)
3. **Bearer setup**: SAPBR APN and open connection
4. **WindNerd WTP**: HTTP POST with wind reports (and optional samples)
5. **Windguru**: HTTP GET with wind data + interval
6. **Sleep**: AT+CSCLK=2 for modem, HAL_PWR_EnterSLEEPMode for MCU

## Reliability Mechanisms

### Watchdog Timer (IWDG)
The STM32's Independent Watchdog runs on the internal LSI clock (~32kHz) and operates independently of the main clock. If the firmware hangs for any reason:

- \`kickWatchdog()\` must be called within 10 seconds
- Called automatically in \`loop()\` and \`processModem()\`
- MCU auto-resets if watchdog times out
- Watchdog persists across software resets (only power cycle disables)

### State Timeout
If any modem state takes longer than 90 seconds (e.g., stuck waiting for network):

- Current upload cycle is aborted
- HTTP session is cleaned up (\`AT+HTTPTERM\`)
- State machine jumps to SLEEP state
- Next cycle starts fresh on next interval

### Periodic Modem Reset
Every 96 upload cycles (~24 hours at 15-min intervals):

- Full modem reset via \`AT+CFUN=1,1\`
- Clears accumulated connection issues
- Prevents long-term modem memory leaks
- Modem restarts fresh for next cycle

## Salt Generation

The Windguru API requires a unique salt for each upload. This implementation uses:

\`\`\`
salt = STM32_UID[31:0] + random(32-bit)
\`\`\`

- **STM32 UID**: 32-bit portion of the chip's unique 96-bit factory ID (survives reflash)
- **Random**: 32-bit random seeded from full 96-bit UID (different each upload)
- **Total**: 64-bit uniqueness = ~18 quintillion combinations

At 35,000 uploads/year, collision probability is negligible for centuries.

## Data Usage Estimates

| Interval | With Samples | Reports Only |
|----------|--------------|--------------|
| 2 min | ~1.8 GB/year | ~180 MB/year |
| 15 min | ~245 MB/year | **~25 MB/year** |
| 30 min | ~123 MB/year | ~13 MB/year |

## Hardware

- **MCU**: STM32G031F8 @ 8MHz (low power clock config)
- **Modem**: Air780E 4G LTE on USART2
- **Debug**: USART1 @ 115200 baud
- **Power**: 2x 18650 batteries + solar panel (unattended operation)

## Debug Output

Connect FTDI RX to modem TX (USART2) to see AT commands and responses.
Debug serial (USART1) shows state transitions and error messages.

## Memory Usage

- **Flash**: ~62KB (94% of 64KB)
- **RAM**: ~5KB (60% of 8KB)

Optimizations:
- No EEPROM library (uses STM32 UID directly)
- Char arrays instead of String class
- Reduced buffer sizes
- No sample storage (reports only mode)
Loading