Skip to content

Commit 9f104f0

Browse files
authored
ESP-NOW support (#51)
Initial ESP Now support
1 parent 8e078f5 commit 9f104f0

18 files changed

Lines changed: 958 additions & 174 deletions

.vscode/settings.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"files.associations": {
3+
"functional": "cpp",
4+
"array": "cpp",
5+
"string": "cpp",
6+
"string_view": "cpp",
7+
"span": "cpp"
8+
}
9+
}

README.md

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,35 @@ Head over to the discord server to discuss this firmware: [DISCORD](https://disc
66
## What is this?
77
Firmware source code for ESP32 and ESP8266 that is compatible with SimHub. It's derived from what SimHub lets you setup, but tweaked with a thin compatibility layer for the ESP32 and ESP8266.
88

9-
## Why would you want to do this?
10-
ESP8266 is cheaper and more powerful that most arduinos boards and fairly popular in the IoT Community. The ESP32 is a very handy little monster to use too.
11-
129
## How To
10+
[Getting started with ESP-SimHub for ESP32 and ESP8266](http://ecrowne.com/esp-simhub/getting-started)
11+
1312
- Download this code
1413
- Install [Git](https://git-scm.com/downloads)
1514
- Install [VSCode](https://code.visualstudio.com/Download) and [Platformio](https://platformio.org/platformio-ide)
1615
- Open with VSCode
17-
- Adjust it however you need
16+
- Select the environment you want to use, either ESP8266 or ESP32 from the dropdown in the platformio bottom bar
17+
- Tweak src/main.cpp to your needs
1818
- Upload it to your device
1919

20-
## Why Platformio?
21-
It's better in almost every way than Arduino IDE, but more generic and more strict. It's worth learning it, trust me.
20+
## How does the SimHub Wireless connection work?
21+
SimHub assumes there is a serial port that it can talk to, uses a certain protocol that accounts for errors [Automatic Repeat Request](https://en.wikipedia.org/wiki/Automatic_repeat_request).
22+
23+
Instead of having the feature device connected to the computer directly, we use "bridges" that forward the data to the feature device and back to the computer.
24+
25+
- For WiFi we create a "virtual com port" using Perle TruePort or similar software which will forward all communication to a certain IP and the feature device is connected to that IP.
26+
27+
- For ESP-Now we use a 2 ESPs, one with the features and one connected to the computer where SimHub is running. One device just forwards the data to the feature device and back to the computer, while the other device receives and consumes the data.
2228

23-
## How does the SimHub WiFi connection works?
24-
SimHub assumes there is a serial port that it can talk to, and a certain protocol that accounts for errors [Automatic Repeat Request](https://en.wikipedia.org/wiki/Automatic_repeat_request).
29+
For both protocols, on the Feature device side, we modify the code to create a the appropriate listener receiving data (either from WiFi or ESP-Now), we put the received data in a buffer (similar to the serial port) and we allow the modified Arduino SimHub client to consume it from the buffer, as if it was a normal seial connection.
2530

26-
Instead of having a local device connected to the computer directly, we create a "virual com port" that forwards all communication to a certain IP.
31+
## How to configure the firmware to use ESPNow
32+
Set the [preprocessor directive](https://cplusplus.com/doc/tutorial/preprocessor/) called CONNECTION_TYPE to ESPNOW `#define CONNECTION_TYPE ESP_NOW` (in src/main.cpp). Set DEBUG_BRIDGE to true as well, and set `monitor_speed = 115200` in `platformio.ini`. Configure the MAC address of the ESP that will remain connected to the computer (not this one) in `#define ESPNOW_PEER_MAC {0x34, 0x85, 0x18, 0x90, 0x7A, 0x00}` (in src/main.cpp) for a device with MAC address `34:85:18:90:7A:00`. Upload the ESP-SimHub firmware to the ESP with the features.
2733

28-
On the Microcontroller side, we modify the code to create a socket server receiving data on the right port, we put that data in a buffer (similar to
29-
the serial port) and we allow the modified Arduino SimHub client to consume it from the buffer, as if it was a normal seial connection.
34+
Switch the environment to `env:espnow-bridge`, configure the code in `src/main-espnow.cpp`, set the MAC address of the ESP that has all the features (not this one) in `#define ESPNOW_PEER_MAC {0x34, 0x85, 0x18, 0x90, 0x7A, 0x01}` (in src/main-espnow.cpp) for a device with MAC address `34:85:18:90:7A:01`, and upload it to the ESP that will remain connected to the computer.
3035

3136
## How to configure the firmware to use WiFi
32-
Set the [preprocessor directive](https://cplusplus.com/doc/tutorial/preprocessor/) called INCLUDE_WIFI to true (in src/main.cpp). Set DEBUG_TCP_BRIDGE to true as well, and set `monitor_speed = 115200` in `platformio.ini`.
37+
Set the [preprocessor directive](https://cplusplus.com/doc/tutorial/preprocessor/) called CONNECTION_TYPE to WIFI `#define CONNECTION_TYPE WIFI` (in src/main.cpp). Set DEBUG_BRIDGE to true as well, and set `monitor_speed = 115200` in `platformio.ini`.
3338

3439
## How to connect the ESP to your WiFi
3540
Upload a [WiFi enabled](https://github.com/eCrowneEng/ESP-SimHub/blob/main/src/main.cpp#L4) firmware. Connect USB to computer and open the serial monitor with `115200` baud rate (to catch debug messages). Power the ESP up.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#include "ESPNowSerialBridge.h"
2+
3+
// Static member definition
4+
ESPNowSerialBridge* ESPNowSerialBridge::instance = nullptr;
Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
#pragma once
2+
3+
#include <Arduino.h>
4+
#include <FullLoopbackStream.h>
5+
#ifdef ESP32
6+
#include <esp_now.h>
7+
#include <WiFi.h>
8+
#else
9+
#include <espnow.h>
10+
#include <ESP8266WiFi.h>
11+
#pragma message "ESP8266 ESP-NOW is very experimental, use at your own risk"
12+
#endif
13+
#include <EspNowProtocol.h>
14+
15+
16+
17+
class ESPNowSerialBridge
18+
{
19+
public:
20+
uint8_t peerMac[6];
21+
uint8_t channel;
22+
EspNowMessage outgoingMessage;
23+
EspNowMessage incomingMessage;
24+
EspNowMessage bridgeMessage;
25+
#ifdef ESP32
26+
esp_now_peer_info_t peerInfo;
27+
#endif
28+
29+
ESPNowSerialBridge(const uint8_t* peerMac, uint8_t channel) {
30+
memcpy(this->peerMac, peerMac, 6);
31+
this->channel = channel;
32+
}
33+
34+
void begin(unsigned long baud) {
35+
if (baud == 0 || baud == 19200) {
36+
#if DEBUG_BRIDGE
37+
Serial.println("default baud rate, no change");
38+
#endif
39+
return;
40+
}
41+
42+
// Clear any existing SimHub bytes
43+
memset(bridgeMessage.bridgeBytes, 0, bridgeMessage.length);
44+
45+
// Convert baud rate to bytes and create baud command
46+
auto baudBytes = numberToBytes(baud);
47+
auto length = 3;
48+
49+
size_t commandSize;
50+
auto command = makeCommand(baudBytes, length, BAUDRATE_COMMAND, commandSize);
51+
52+
memcpy(bridgeMessage.bridgeBytes, command, commandSize);
53+
bridgeMessage.length = commandSize;
54+
55+
#if DEBUG_BRIDGE
56+
Serial.print(">> Bridge message bytes (byte code): ");
57+
for (int i = 0; i < bridgeMessage.length; i++) {
58+
Serial.printf("%02X ", bridgeMessage.bridgeBytes[i]);
59+
}
60+
Serial.println();
61+
#endif
62+
this->flushMessage(bridgeMessage);
63+
}
64+
65+
void setup(FullLoopbackStream *outgoingStream, FullLoopbackStream *incomingStream) {
66+
Serial.println("Setting up ESPNowSerialBridge");
67+
68+
// Store the instance pointer in the static member
69+
instance = this;
70+
71+
this->outgoingStream = outgoingStream;
72+
this->incomingStream = incomingStream;
73+
74+
75+
// Set device as a Wi-Fi Station
76+
WiFi.mode(WIFI_STA);
77+
Serial.println("WiFi mode set to STA");
78+
79+
// Init ESP-NOW
80+
#ifdef ESP32
81+
if (esp_now_init() != ESP_OK) {
82+
#else
83+
if (esp_now_init() != 0) {
84+
#endif
85+
Serial.println("Error initializing ESP-NOW");
86+
return;
87+
}
88+
Serial.println("ESP-NOW initialized");
89+
90+
#ifdef ESP32
91+
// Register peer
92+
memcpy(peerInfo.peer_addr, this->peerMac, 6);
93+
peerInfo.channel = this->channel;
94+
peerInfo.encrypt = false;
95+
#else
96+
esp_now_set_self_role(ESP_NOW_ROLE_CONTROLLER);
97+
#endif
98+
99+
esp_now_register_send_cb(staticOnDataSent);
100+
esp_now_register_recv_cb(staticHandleData);
101+
102+
// Register peer
103+
#ifdef ESP32
104+
if (esp_now_add_peer(&peerInfo) != ESP_OK){
105+
#else
106+
if (esp_now_add_peer(this->peerMac, ESP_NOW_ROLE_COMBO, this->channel, NULL, 0) != 0) {
107+
#endif
108+
Serial.println("Failed to add peer");
109+
return;
110+
}
111+
Serial.println("Peer registered");
112+
113+
114+
Serial.print("This device's MAC address: ");
115+
Serial.println(WiFi.getMode() == WIFI_AP ? WiFi.softAPmacAddress() : WiFi.macAddress());
116+
}
117+
118+
void loop() {
119+
this->flush();
120+
}
121+
122+
void flush() {
123+
// if there is data available in the wifi stream, it's meant
124+
// to go from Serial port to TCP client
125+
size_t availableLength = this->outgoingStream->available();
126+
if (availableLength)
127+
{
128+
129+
int bytesRead = this->outgoingStream->readBytes(outgoingMessage.simHubBytes, availableLength);
130+
outgoingMessage.version = MESSAGE_VERSION;
131+
outgoingMessage.length = bytesRead;
132+
memset(outgoingMessage.bridgeBytes, 0, sizeof(outgoingMessage.bridgeBytes));
133+
this->outgoingStream->clear();
134+
135+
#if DEBUG_BRIDGE
136+
Serial.print(">> Response bytes (byte code): ");
137+
for (int i = 0; i < bytesRead; i++) {
138+
Serial.printf("%02X ", outgoingMessage.simHubBytes[i]);
139+
}
140+
Serial.println();
141+
#endif
142+
this->flushMessage(outgoingMessage);
143+
}
144+
}
145+
146+
void flushMessage(EspNowMessage& message) {
147+
// Copy message struct to buffer
148+
uint8_t buffer[sizeof(EspNowMessage)];
149+
memcpy(buffer, &message, sizeof(EspNowMessage));
150+
// Send buffer to ESP-NOW
151+
#ifdef ESP32
152+
esp_err_t result = esp_now_send(peerInfo.peer_addr, buffer, sizeof(EspNowMessage));
153+
if (result != ESP_OK) {
154+
const char* error_msg = esp_err_to_name(result);
155+
Serial.printf(">> Error with response: %s (0x%x)\n", error_msg, result);
156+
157+
// Common error handling
158+
switch(result) {
159+
case ESP_ERR_ESPNOW_NOT_INIT:
160+
Serial.println(">> ESP-NOW not initialized");
161+
break;
162+
case ESP_ERR_ESPNOW_ARG:
163+
Serial.println(">> Invalid argument");
164+
break;
165+
case ESP_ERR_ESPNOW_NO_MEM:
166+
Serial.println(">> Out of memory");
167+
break;
168+
case ESP_ERR_ESPNOW_FULL:
169+
Serial.println(">> Peer list is full");
170+
break;
171+
case ESP_ERR_ESPNOW_NOT_FOUND:
172+
Serial.println(">> Peer not found");
173+
break;
174+
case ESP_ERR_ESPNOW_INTERNAL:
175+
Serial.println(">> Internal error");
176+
break;
177+
case ESP_ERR_ESPNOW_EXIST:
178+
Serial.println(">> Peer already exists");
179+
break;
180+
case ESP_ERR_ESPNOW_IF:
181+
Serial.println(">> Interface error");
182+
break;
183+
}
184+
}
185+
#else
186+
int result = esp_now_send(this->peerMac, buffer, sizeof(EspNowMessage));
187+
if (result != 0) {
188+
Serial.printf(">> Error with response: %d\n", result);
189+
}
190+
#endif
191+
}
192+
193+
// Callback when data is sent
194+
#ifdef ESP32
195+
void onDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
196+
if (status != ESP_NOW_SEND_SUCCESS) {
197+
#else
198+
void onDataSent(const uint8_t *mac_addr, uint8_t status) {
199+
if (status != 0) {
200+
#endif
201+
#if DEBUG_BRIDGE
202+
Serial.print("\n>> Last Packet Send Status: ");
203+
#ifdef ESP32
204+
Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Success" : "Fail");
205+
#else
206+
Serial.println(status == 0 ? "Success" : "Fail");
207+
#endif
208+
#endif
209+
}
210+
}
211+
212+
// Callback when data is received
213+
#ifdef ESP32
214+
// void handleData(const esp_now_recv_info_t *peerInfo, const uint8_t *data, int len) {
215+
void handleData(const uint8_t *peerInfo, const uint8_t *data, uint8_t len) {
216+
#else
217+
void handleData(const uint8_t *peerInfo, const uint8_t *data, uint8_t len) {
218+
#endif
219+
byte buffer[sizeof(EspNowMessage)];
220+
memcpy(&incomingMessage, data, sizeof(EspNowMessage));
221+
if (incomingMessage.version != MESSAGE_VERSION) {
222+
#if DEBUG_BRIDGE
223+
Serial.println("<< Mismatching version for message, it could be corrupted or bridge device is outdated");
224+
#endif
225+
this->incomingStream->clear();
226+
return;
227+
}
228+
229+
#if DEBUG_BRIDGE
230+
Serial.print("<< Request received (byte code): ");
231+
if (incomingMessage.length > 0 && incomingMessage.bridgeBytes[0] == COMMAND_HEADER) {
232+
for (int i = 0; i < incomingMessage.length; i++) {
233+
Serial.printf("%02X ", incomingMessage.bridgeBytes[i]);
234+
}
235+
} else {
236+
for (int i = 0; i < incomingMessage.length; i++) {
237+
Serial.printf("%02X ", incomingMessage.simHubBytes[i]);
238+
}
239+
}
240+
Serial.println();
241+
#endif
242+
this->incomingStream->clear();
243+
244+
if (incomingMessage.length > 0 && incomingMessage.bridgeBytes[0] == COMMAND_HEADER) {
245+
// treat it as bridge bytes
246+
this->handleBridgeMessage(incomingMessage);
247+
} else {
248+
// treat it as serial bytes
249+
this->incomingStream->write(incomingMessage.simHubBytes, incomingMessage.length);
250+
}
251+
}
252+
253+
void handleBridgeMessage(EspNowMessage& message) {
254+
uint8_t length = message.bridgeBytes[1];
255+
uint8_t command = message.bridgeBytes[2];
256+
257+
if (message.bridgeBytes[2 + length + 1] != COMMAND_END) {
258+
// invalid command / length
259+
return;
260+
}
261+
262+
switch (command) {
263+
case PING_COMMAND: {
264+
#if DEBUG_BRIDGE
265+
Serial.println(">> PING received");
266+
#endif
267+
bridgeMessage.version = MESSAGE_VERSION;
268+
size_t resultingByteCount = 0;
269+
uint8_t* pingBytes = makeCommand(nullptr, 0, PONG_COMMAND, resultingByteCount);
270+
bridgeMessage.length = resultingByteCount;
271+
memcpy(bridgeMessage.bridgeBytes, pingBytes, resultingByteCount);
272+
// print the bytes sent to the bridge
273+
Serial.print(">> PONG bytes (byte code): ");
274+
for (int i = 0; i < bridgeMessage.length; i++) {
275+
Serial.printf("%02X ", bridgeMessage.bridgeBytes[i]);
276+
}
277+
Serial.println();
278+
this->flushMessage(bridgeMessage);
279+
break;
280+
}
281+
default:
282+
break;
283+
}
284+
}
285+
286+
287+
// Static callback function that ESP-NOW can work with
288+
#ifdef ESP32
289+
// static void staticHandleData(const esp_now_recv_info_t *peerInfo, const uint8_t *data, int len) {
290+
static void staticHandleData(const uint8_t *peerInfo, const uint8_t *data, int len) {
291+
#else
292+
static void staticHandleData(uint8_t *peerInfo, uint8_t *data, uint8_t len) {
293+
#endif
294+
// Forward to instance method if instance exists
295+
if (ESPNowSerialBridge::instance) {
296+
ESPNowSerialBridge::instance->handleData(peerInfo, data, len);
297+
}
298+
}
299+
300+
// Static callback function for send status
301+
#ifdef ESP32
302+
static void staticOnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
303+
#else
304+
static void staticOnDataSent(uint8_t *mac_addr, uint8_t status) {
305+
#endif
306+
// Forward to instance method if instance exists
307+
if (ESPNowSerialBridge::instance) {
308+
ESPNowSerialBridge::instance->onDataSent(mac_addr, status);
309+
}
310+
}
311+
312+
// Static member to hold instance pointer
313+
static ESPNowSerialBridge* instance;
314+
FullLoopbackStream *incomingStream;
315+
FullLoopbackStream *outgoingStream;
316+
};

0 commit comments

Comments
 (0)