Skip to content

Commit

Permalink
ESP-NOW support (#51)
Browse files Browse the repository at this point in the history
Initial ESP Now support
  • Loading branch information
ecoreng authored Dec 15, 2024
1 parent 8e078f5 commit 9f104f0
Show file tree
Hide file tree
Showing 18 changed files with 958 additions and 174 deletions.
9 changes: 9 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"files.associations": {
"functional": "cpp",
"array": "cpp",
"string": "cpp",
"string_view": "cpp",
"span": "cpp"
}
}
29 changes: 17 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,35 @@ Head over to the discord server to discuss this firmware: [DISCORD](https://disc
## What is this?
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.

## Why would you want to do this?
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.

## How To
[Getting started with ESP-SimHub for ESP32 and ESP8266](http://ecrowne.com/esp-simhub/getting-started)

- Download this code
- Install [Git](https://git-scm.com/downloads)
- Install [VSCode](https://code.visualstudio.com/Download) and [Platformio](https://platformio.org/platformio-ide)
- Open with VSCode
- Adjust it however you need
- Select the environment you want to use, either ESP8266 or ESP32 from the dropdown in the platformio bottom bar
- Tweak src/main.cpp to your needs
- Upload it to your device

## Why Platformio?
It's better in almost every way than Arduino IDE, but more generic and more strict. It's worth learning it, trust me.
## How does the SimHub Wireless connection work?
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).

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.

- 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.

- 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.

## How does the SimHub WiFi connection works?
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).
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.

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.
## How to configure the firmware to use ESPNow
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.

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
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.
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.

## How to configure the firmware to use WiFi
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`.
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`.

## How to connect the ESP to your WiFi
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.
Expand Down
4 changes: 4 additions & 0 deletions lib/ESPNowSerialBridge/ESPNowSerialBridge.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#include "ESPNowSerialBridge.h"

// Static member definition
ESPNowSerialBridge* ESPNowSerialBridge::instance = nullptr;
316 changes: 316 additions & 0 deletions lib/ESPNowSerialBridge/ESPNowSerialBridge.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,316 @@
#pragma once

#include <Arduino.h>
#include <FullLoopbackStream.h>
#ifdef ESP32
#include <esp_now.h>
#include <WiFi.h>
#else
#include <espnow.h>
#include <ESP8266WiFi.h>
#pragma message "ESP8266 ESP-NOW is very experimental, use at your own risk"
#endif
#include <EspNowProtocol.h>



class ESPNowSerialBridge
{
public:
uint8_t peerMac[6];
uint8_t channel;
EspNowMessage outgoingMessage;
EspNowMessage incomingMessage;
EspNowMessage bridgeMessage;
#ifdef ESP32
esp_now_peer_info_t peerInfo;
#endif

ESPNowSerialBridge(const uint8_t* peerMac, uint8_t channel) {
memcpy(this->peerMac, peerMac, 6);
this->channel = channel;
}

void begin(unsigned long baud) {
if (baud == 0 || baud == 19200) {
#if DEBUG_BRIDGE
Serial.println("default baud rate, no change");
#endif
return;
}

// Clear any existing SimHub bytes
memset(bridgeMessage.bridgeBytes, 0, bridgeMessage.length);

// Convert baud rate to bytes and create baud command
auto baudBytes = numberToBytes(baud);
auto length = 3;

size_t commandSize;
auto command = makeCommand(baudBytes, length, BAUDRATE_COMMAND, commandSize);

memcpy(bridgeMessage.bridgeBytes, command, commandSize);
bridgeMessage.length = commandSize;

#if DEBUG_BRIDGE
Serial.print(">> Bridge message bytes (byte code): ");
for (int i = 0; i < bridgeMessage.length; i++) {
Serial.printf("%02X ", bridgeMessage.bridgeBytes[i]);
}
Serial.println();
#endif
this->flushMessage(bridgeMessage);
}

void setup(FullLoopbackStream *outgoingStream, FullLoopbackStream *incomingStream) {
Serial.println("Setting up ESPNowSerialBridge");

// Store the instance pointer in the static member
instance = this;

this->outgoingStream = outgoingStream;
this->incomingStream = incomingStream;


// Set device as a Wi-Fi Station
WiFi.mode(WIFI_STA);
Serial.println("WiFi mode set to STA");

// Init ESP-NOW
#ifdef ESP32
if (esp_now_init() != ESP_OK) {
#else
if (esp_now_init() != 0) {
#endif
Serial.println("Error initializing ESP-NOW");
return;
}
Serial.println("ESP-NOW initialized");

#ifdef ESP32
// Register peer
memcpy(peerInfo.peer_addr, this->peerMac, 6);
peerInfo.channel = this->channel;
peerInfo.encrypt = false;
#else
esp_now_set_self_role(ESP_NOW_ROLE_CONTROLLER);
#endif

esp_now_register_send_cb(staticOnDataSent);
esp_now_register_recv_cb(staticHandleData);

// Register peer
#ifdef ESP32
if (esp_now_add_peer(&peerInfo) != ESP_OK){
#else
if (esp_now_add_peer(this->peerMac, ESP_NOW_ROLE_COMBO, this->channel, NULL, 0) != 0) {
#endif
Serial.println("Failed to add peer");
return;
}
Serial.println("Peer registered");


Serial.print("This device's MAC address: ");
Serial.println(WiFi.getMode() == WIFI_AP ? WiFi.softAPmacAddress() : WiFi.macAddress());
}

void loop() {
this->flush();
}

void flush() {
// if there is data available in the wifi stream, it's meant
// to go from Serial port to TCP client
size_t availableLength = this->outgoingStream->available();
if (availableLength)
{

int bytesRead = this->outgoingStream->readBytes(outgoingMessage.simHubBytes, availableLength);
outgoingMessage.version = MESSAGE_VERSION;
outgoingMessage.length = bytesRead;
memset(outgoingMessage.bridgeBytes, 0, sizeof(outgoingMessage.bridgeBytes));
this->outgoingStream->clear();

#if DEBUG_BRIDGE
Serial.print(">> Response bytes (byte code): ");
for (int i = 0; i < bytesRead; i++) {
Serial.printf("%02X ", outgoingMessage.simHubBytes[i]);
}
Serial.println();
#endif
this->flushMessage(outgoingMessage);
}
}

void flushMessage(EspNowMessage& message) {
// Copy message struct to buffer
uint8_t buffer[sizeof(EspNowMessage)];
memcpy(buffer, &message, sizeof(EspNowMessage));
// Send buffer to ESP-NOW
#ifdef ESP32
esp_err_t result = esp_now_send(peerInfo.peer_addr, buffer, sizeof(EspNowMessage));
if (result != ESP_OK) {
const char* error_msg = esp_err_to_name(result);
Serial.printf(">> Error with response: %s (0x%x)\n", error_msg, result);

// Common error handling
switch(result) {
case ESP_ERR_ESPNOW_NOT_INIT:
Serial.println(">> ESP-NOW not initialized");
break;
case ESP_ERR_ESPNOW_ARG:
Serial.println(">> Invalid argument");
break;
case ESP_ERR_ESPNOW_NO_MEM:
Serial.println(">> Out of memory");
break;
case ESP_ERR_ESPNOW_FULL:
Serial.println(">> Peer list is full");
break;
case ESP_ERR_ESPNOW_NOT_FOUND:
Serial.println(">> Peer not found");
break;
case ESP_ERR_ESPNOW_INTERNAL:
Serial.println(">> Internal error");
break;
case ESP_ERR_ESPNOW_EXIST:
Serial.println(">> Peer already exists");
break;
case ESP_ERR_ESPNOW_IF:
Serial.println(">> Interface error");
break;
}
}
#else
int result = esp_now_send(this->peerMac, buffer, sizeof(EspNowMessage));
if (result != 0) {
Serial.printf(">> Error with response: %d\n", result);
}
#endif
}

// Callback when data is sent
#ifdef ESP32
void onDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
if (status != ESP_NOW_SEND_SUCCESS) {
#else
void onDataSent(const uint8_t *mac_addr, uint8_t status) {
if (status != 0) {
#endif
#if DEBUG_BRIDGE
Serial.print("\n>> Last Packet Send Status: ");
#ifdef ESP32
Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Success" : "Fail");
#else
Serial.println(status == 0 ? "Success" : "Fail");
#endif
#endif
}
}

// Callback when data is received
#ifdef ESP32
// void handleData(const esp_now_recv_info_t *peerInfo, const uint8_t *data, int len) {
void handleData(const uint8_t *peerInfo, const uint8_t *data, uint8_t len) {
#else
void handleData(const uint8_t *peerInfo, const uint8_t *data, uint8_t len) {
#endif
byte buffer[sizeof(EspNowMessage)];
memcpy(&incomingMessage, data, sizeof(EspNowMessage));
if (incomingMessage.version != MESSAGE_VERSION) {
#if DEBUG_BRIDGE
Serial.println("<< Mismatching version for message, it could be corrupted or bridge device is outdated");
#endif
this->incomingStream->clear();
return;
}

#if DEBUG_BRIDGE
Serial.print("<< Request received (byte code): ");
if (incomingMessage.length > 0 && incomingMessage.bridgeBytes[0] == COMMAND_HEADER) {
for (int i = 0; i < incomingMessage.length; i++) {
Serial.printf("%02X ", incomingMessage.bridgeBytes[i]);
}
} else {
for (int i = 0; i < incomingMessage.length; i++) {
Serial.printf("%02X ", incomingMessage.simHubBytes[i]);
}
}
Serial.println();
#endif
this->incomingStream->clear();

if (incomingMessage.length > 0 && incomingMessage.bridgeBytes[0] == COMMAND_HEADER) {
// treat it as bridge bytes
this->handleBridgeMessage(incomingMessage);
} else {
// treat it as serial bytes
this->incomingStream->write(incomingMessage.simHubBytes, incomingMessage.length);
}
}

void handleBridgeMessage(EspNowMessage& message) {
uint8_t length = message.bridgeBytes[1];
uint8_t command = message.bridgeBytes[2];

if (message.bridgeBytes[2 + length + 1] != COMMAND_END) {
// invalid command / length
return;
}

switch (command) {
case PING_COMMAND: {
#if DEBUG_BRIDGE
Serial.println(">> PING received");
#endif
bridgeMessage.version = MESSAGE_VERSION;
size_t resultingByteCount = 0;
uint8_t* pingBytes = makeCommand(nullptr, 0, PONG_COMMAND, resultingByteCount);
bridgeMessage.length = resultingByteCount;
memcpy(bridgeMessage.bridgeBytes, pingBytes, resultingByteCount);
// print the bytes sent to the bridge
Serial.print(">> PONG bytes (byte code): ");
for (int i = 0; i < bridgeMessage.length; i++) {
Serial.printf("%02X ", bridgeMessage.bridgeBytes[i]);
}
Serial.println();
this->flushMessage(bridgeMessage);
break;
}
default:
break;
}
}


// Static callback function that ESP-NOW can work with
#ifdef ESP32
// static void staticHandleData(const esp_now_recv_info_t *peerInfo, const uint8_t *data, int len) {
static void staticHandleData(const uint8_t *peerInfo, const uint8_t *data, int len) {
#else
static void staticHandleData(uint8_t *peerInfo, uint8_t *data, uint8_t len) {
#endif
// Forward to instance method if instance exists
if (ESPNowSerialBridge::instance) {
ESPNowSerialBridge::instance->handleData(peerInfo, data, len);
}
}

// Static callback function for send status
#ifdef ESP32
static void staticOnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
#else
static void staticOnDataSent(uint8_t *mac_addr, uint8_t status) {
#endif
// Forward to instance method if instance exists
if (ESPNowSerialBridge::instance) {
ESPNowSerialBridge::instance->onDataSent(mac_addr, status);
}
}

// Static member to hold instance pointer
static ESPNowSerialBridge* instance;
FullLoopbackStream *incomingStream;
FullLoopbackStream *outgoingStream;
};
Loading

0 comments on commit 9f104f0

Please sign in to comment.