mirror of
https://github.com/Links2004/arduinoWebSockets.git
synced 2025-07-14 15:56:30 +02:00
Add minimal example for ESP OTA
This commit is contained in:
27
examples/esp8266/WebSocketClientOTA/README.md
Normal file
27
examples/esp8266/WebSocketClientOTA/README.md
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
## Minimal example of WebsocketClientOTA and Python server
|
||||||
|
|
||||||
|
Take this as small example, how achieve OTA update on ESP8266 and ESP32.
|
||||||
|
|
||||||
|
Python server was wrote from train so take it only as bare example.
|
||||||
|
It's working, but it's not mean to run in production.
|
||||||
|
|
||||||
|
|
||||||
|
### Usage:
|
||||||
|
|
||||||
|
Start server:
|
||||||
|
```bash
|
||||||
|
cd python_ota_server
|
||||||
|
python3 -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip3 install -r requirements.txt
|
||||||
|
python3 main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Flash ESP with example sketch and start it.
|
||||||
|
|
||||||
|
Change version inside example sketch to higher and compile it and save it to bin file.
|
||||||
|
|
||||||
|
Rename it to `mydevice-1.0.1-esp8266.bin` and place it inside new folder firmware (server create it).
|
||||||
|
|
||||||
|
When the ESP connect to server, it check if version flashed is equal to fw in firmware folder. If higher FW version is present,
|
||||||
|
start the flash process.
|
263
examples/esp8266/WebSocketClientOTA/WebSocketClientOTA.ino
Normal file
263
examples/esp8266/WebSocketClientOTA/WebSocketClientOTA.ino
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
/*
|
||||||
|
* WebSocketClientOTA.ino
|
||||||
|
*
|
||||||
|
* Created on: 25.10.2021
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
|
||||||
|
#ifdef ESP8266
|
||||||
|
#include <ESP8266WiFi.h>
|
||||||
|
#include <ESP8266mDNS.h>
|
||||||
|
#include <Updater.h>
|
||||||
|
#endif
|
||||||
|
#ifdef ESP32
|
||||||
|
#include "WiFi.h"
|
||||||
|
#include "ESPmDNS.h"
|
||||||
|
#include <Update.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <WiFiUdp.h>
|
||||||
|
#include <ESP8266WiFiMulti.h>
|
||||||
|
|
||||||
|
#include <WebSocketsClient.h>
|
||||||
|
|
||||||
|
#include <Hash.h>
|
||||||
|
|
||||||
|
ESP8266WiFiMulti WiFiMulti;
|
||||||
|
WebSocketsClient webSocket;
|
||||||
|
|
||||||
|
#define USE_SERIAL Serial
|
||||||
|
|
||||||
|
// Variables:
|
||||||
|
// Settable:
|
||||||
|
const char *version = "1.0.0";
|
||||||
|
const char *name = "mydevice";
|
||||||
|
|
||||||
|
// Others:
|
||||||
|
#ifdef ESP8266
|
||||||
|
const char *chip = "esp8266";
|
||||||
|
#endif
|
||||||
|
#ifdef ESP32
|
||||||
|
const char *chip = "esp32";
|
||||||
|
#endif
|
||||||
|
|
||||||
|
uint32_t maxSketchSpace = 0;
|
||||||
|
int SketchSize = 0;
|
||||||
|
bool ws_conn = false;
|
||||||
|
|
||||||
|
String IpAddress2String(const IPAddress& ipAddress)
|
||||||
|
{
|
||||||
|
return String(ipAddress[0]) + String(".") +
|
||||||
|
String(ipAddress[1]) + String(".") +
|
||||||
|
String(ipAddress[2]) + String(".") +
|
||||||
|
String(ipAddress[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void greetings_(){
|
||||||
|
StaticJsonDocument<200> doc;
|
||||||
|
doc["type"] = "greetings";
|
||||||
|
doc["mac"] = WiFi.macAddress();
|
||||||
|
doc["ip"] = IpAddress2String(WiFi.localIP());
|
||||||
|
doc["version"] = version;
|
||||||
|
doc["name"] = name;
|
||||||
|
doc["chip"] = chip;
|
||||||
|
|
||||||
|
char data[200];
|
||||||
|
serializeJson(doc, data);
|
||||||
|
webSocket.sendTXT(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void register_(){
|
||||||
|
StaticJsonDocument<200> doc;
|
||||||
|
doc["type"] = "register";
|
||||||
|
doc["mac"] = WiFi.macAddress();
|
||||||
|
|
||||||
|
char data[200];
|
||||||
|
serializeJson(doc, data);
|
||||||
|
webSocket.sendTXT(data);
|
||||||
|
ws_conn = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef void (*CALLBACK_FUNCTION)(JsonDocument &msg);
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char type[50];
|
||||||
|
CALLBACK_FUNCTION func;
|
||||||
|
} RESPONSES_STRUCT;
|
||||||
|
|
||||||
|
void OTA(JsonDocument &msg){
|
||||||
|
USE_SERIAL.print(F("[WSc] OTA mode: "));
|
||||||
|
const char* go = "go";
|
||||||
|
const char* ok = "ok";
|
||||||
|
if(strncmp( msg["value"], go, strlen(go)) == 0 ) {
|
||||||
|
USE_SERIAL.print(F("go\n"));
|
||||||
|
SketchSize = int(msg["size"]);
|
||||||
|
maxSketchSpace = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000;
|
||||||
|
USE_SERIAL.printf("[WSc] Max sketch size: %u\n", maxSketchSpace);
|
||||||
|
USE_SERIAL.printf("[WSc] Sketch size: %d\n", SketchSize);
|
||||||
|
USE_SERIAL.setDebugOutput(true);
|
||||||
|
if (!Update.begin(maxSketchSpace)) { //start with max available size
|
||||||
|
Update.printError(Serial);
|
||||||
|
ESP.restart();
|
||||||
|
}
|
||||||
|
} else if (strncmp( msg["value"], ok, strlen(ok)) == 0) {
|
||||||
|
USE_SERIAL.print(F("OK\n"));
|
||||||
|
register_();
|
||||||
|
} else {
|
||||||
|
USE_SERIAL.print(F("unknown value : "));
|
||||||
|
USE_SERIAL.print(msg["value"].as<char>());
|
||||||
|
USE_SERIAL.print(F("\n"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void STATE(JsonDocument &msg){
|
||||||
|
// Do something with message
|
||||||
|
}
|
||||||
|
|
||||||
|
RESPONSES_STRUCT responses[] = {
|
||||||
|
{"ota", OTA},
|
||||||
|
{"state", STATE},
|
||||||
|
};
|
||||||
|
|
||||||
|
void text(uint8_t * payload, size_t length){
|
||||||
|
// Convert mesage to something usable
|
||||||
|
char msgch[length];
|
||||||
|
for (unsigned int i = 0; i < length; i++)
|
||||||
|
{
|
||||||
|
USE_SERIAL.print((char)payload[i]);
|
||||||
|
msgch[i] = ((char)payload[i]);
|
||||||
|
}
|
||||||
|
msgch[length] = '\0';
|
||||||
|
|
||||||
|
// Parse Json
|
||||||
|
StaticJsonDocument<200> doc_in;
|
||||||
|
DeserializationError error = deserializeJson(doc_in, msgch);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
USE_SERIAL.print(F("deserializeJson() failed: "));
|
||||||
|
USE_SERIAL.println(error.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle each TYPE of message
|
||||||
|
int b = 0;
|
||||||
|
|
||||||
|
for( b=0 ; strlen(responses[b].type) ; b++ )
|
||||||
|
{
|
||||||
|
if( strncmp(doc_in["type"], responses[b].type, strlen(responses[b].type)) == 0 ) {
|
||||||
|
responses[b].func(doc_in);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void webSocketEvent(WStype_t type, uint8_t * payload, size_t length) {
|
||||||
|
|
||||||
|
switch(type) {
|
||||||
|
case WStype_DISCONNECTED:
|
||||||
|
USE_SERIAL.printf("[WSc] Disconnected!\n");
|
||||||
|
break;
|
||||||
|
case WStype_CONNECTED: {
|
||||||
|
USE_SERIAL.printf("[WSc] Connected to url: %s\n", payload);
|
||||||
|
|
||||||
|
// send message to server when Connected
|
||||||
|
// webSocket.sendTXT("Connected");
|
||||||
|
greetings_();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case WStype_TEXT:
|
||||||
|
USE_SERIAL.printf("[WSc] get text: %s\n", payload);
|
||||||
|
|
||||||
|
// send message to server
|
||||||
|
// webSocket.sendTXT("message here");
|
||||||
|
text(payload, length);
|
||||||
|
break;
|
||||||
|
case WStype_BIN:
|
||||||
|
USE_SERIAL.printf("[WSc] get binary length: %u\n", length);
|
||||||
|
// hexdump(payload, length);
|
||||||
|
if (Update.write(payload, length) != length) {
|
||||||
|
Update.printError(Serial);
|
||||||
|
ESP.restart();
|
||||||
|
}
|
||||||
|
yield();
|
||||||
|
SketchSize -= length;
|
||||||
|
USE_SERIAL.printf("[WSc] Sketch size left: %u\n", SketchSize);
|
||||||
|
if (SketchSize < 1){
|
||||||
|
if (Update.end(true)) { //true to set the size to the current progress
|
||||||
|
USE_SERIAL.printf("Update Success: \nRebooting...\n");
|
||||||
|
delay(5);
|
||||||
|
yield();
|
||||||
|
ESP.restart();
|
||||||
|
} else {
|
||||||
|
Update.printError(USE_SERIAL);
|
||||||
|
ESP.restart();
|
||||||
|
}
|
||||||
|
USE_SERIAL.setDebugOutput(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// send data to server
|
||||||
|
// webSocket.sendBIN(payload, length);
|
||||||
|
break;
|
||||||
|
case WStype_PING:
|
||||||
|
// pong will be send automatically
|
||||||
|
USE_SERIAL.printf("[WSc] get ping\n");
|
||||||
|
break;
|
||||||
|
case WStype_PONG:
|
||||||
|
// answer to a ping we send
|
||||||
|
USE_SERIAL.printf("[WSc] get pong\n");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
// USE_SERIAL.begin(921600);
|
||||||
|
USE_SERIAL.begin(115200);
|
||||||
|
|
||||||
|
//Serial.setDebugOutput(true);
|
||||||
|
USE_SERIAL.setDebugOutput(true);
|
||||||
|
|
||||||
|
USE_SERIAL.print(F("\nMAC: "));
|
||||||
|
USE_SERIAL.println(WiFi.macAddress());
|
||||||
|
USE_SERIAL.print(F("\nDevice: "));
|
||||||
|
USE_SERIAL.println(name);
|
||||||
|
USE_SERIAL.printf("\nVersion: %s\n", version);
|
||||||
|
|
||||||
|
for(uint8_t t = 4; t > 0; t--) {
|
||||||
|
USE_SERIAL.printf("[SETUP] BOOT WAIT %d...\n", t);
|
||||||
|
USE_SERIAL.flush();
|
||||||
|
delay(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
WiFiMulti.addAP("SSID", "PASS");
|
||||||
|
|
||||||
|
//WiFi.disconnect();
|
||||||
|
while(WiFiMulti.run() != WL_CONNECTED) {
|
||||||
|
delay(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// server address, port and URL
|
||||||
|
webSocket.begin("10.0.1.5", 8081, "/");
|
||||||
|
|
||||||
|
// event handler
|
||||||
|
webSocket.onEvent(webSocketEvent);
|
||||||
|
|
||||||
|
// use HTTP Basic Authorization this is optional remove if not needed
|
||||||
|
// webSocket.setAuthorization("USER", "PASS");
|
||||||
|
|
||||||
|
// try ever 5000 again if connection has failed
|
||||||
|
webSocket.setReconnectInterval(5000);
|
||||||
|
|
||||||
|
// start heartbeat (optional)
|
||||||
|
// ping server every 15000 ms
|
||||||
|
// expect pong from server within 3000 ms
|
||||||
|
// consider connection disconnected if pong is not received 2 times
|
||||||
|
webSocket.enableHeartbeat(15000, 3000, 2);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
webSocket.loop();
|
||||||
|
}
|
235
examples/esp8266/WebSocketClientOTA/python_ota_server/main.py
Normal file
235
examples/esp8266/WebSocketClientOTA/python_ota_server/main.py
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
"""Minimal example of Python websocket server
|
||||||
|
handling OTA updates for ESP32 amd ESP8266
|
||||||
|
|
||||||
|
Check and upload of firmware works.
|
||||||
|
Register and state function are jus for example.
|
||||||
|
"""
|
||||||
|
# pylint: disable=W0703,E1101
|
||||||
|
import asyncio
|
||||||
|
import copy
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from os import listdir
|
||||||
|
from os.path import join as join_pth
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import websockets
|
||||||
|
from packaging import version
|
||||||
|
|
||||||
|
# Logger settings
|
||||||
|
logging.basicConfig(filename="ws_server.log")
|
||||||
|
Logger = logging.getLogger('WS-OTA')
|
||||||
|
Logger.addHandler(logging.StreamHandler())
|
||||||
|
Logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
# Path to directory with FW
|
||||||
|
fw_path = join_pth(Path().absolute(), "firmware")
|
||||||
|
|
||||||
|
|
||||||
|
def create_path(path: str) -> None:
|
||||||
|
"""Check if path exist or create it"""
|
||||||
|
Path(path).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def shell(command):
|
||||||
|
"""Handle execution of shell commands"""
|
||||||
|
with subprocess.Popen(command, shell=True,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
universal_newlines=True
|
||||||
|
) as process:
|
||||||
|
for stdout_line in iter(process.stdout.readline, ""):
|
||||||
|
Logger.debug(stdout_line)
|
||||||
|
process.stdout.close()
|
||||||
|
return_code = process.wait()
|
||||||
|
Logger.debug("Shell returned: %s", return_code)
|
||||||
|
|
||||||
|
return process.returncode
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def binary_send(websocket, fw_file):
|
||||||
|
"""Read firmware file, divide it to chunks and send them"""
|
||||||
|
with open(fw_file, "rb") as binaryfile:
|
||||||
|
|
||||||
|
while True:
|
||||||
|
chunk = binaryfile.read(2048)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
await websocket.send(chunk)
|
||||||
|
except Exception as exception:
|
||||||
|
Logger.exception(exception)
|
||||||
|
return False
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
|
||||||
|
def version_checker(name, vdev, vapp):
|
||||||
|
"""Parse and compare FW version"""
|
||||||
|
|
||||||
|
if version.parse(vdev) < version.parse(vapp):
|
||||||
|
Logger.info("Client(%s) version %s is smaller than %s: Go for update", name, vdev, vapp)
|
||||||
|
return True
|
||||||
|
Logger.info("Client(%s) version %s is greater or equal to %s: Not updating", name, vdev, vapp)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class WsOtaHandler (threading.Thread):
|
||||||
|
"""Thread handling ota update
|
||||||
|
|
||||||
|
Runing ota directly from message would kill WS
|
||||||
|
as message bus would timeout.
|
||||||
|
"""
|
||||||
|
def __init__(self, name, message, websocket):
|
||||||
|
threading.Thread.__init__(self, daemon=True)
|
||||||
|
self.name = name
|
||||||
|
self.msg = message
|
||||||
|
self.websocket = websocket
|
||||||
|
|
||||||
|
def run(self, ):
|
||||||
|
try:
|
||||||
|
asyncio.run(self.start_)
|
||||||
|
except Exception as exception:
|
||||||
|
Logger.exception(exception)
|
||||||
|
finally:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def start_(self):
|
||||||
|
"""Start _ota se asyncio future"""
|
||||||
|
msg_task = asyncio.ensure_future(
|
||||||
|
self._ota())
|
||||||
|
|
||||||
|
done, pending = await asyncio.wait(
|
||||||
|
[msg_task],
|
||||||
|
return_when=asyncio.FIRST_COMPLETED,
|
||||||
|
)
|
||||||
|
Logger.info("WS Ota Handler done: %s", done)
|
||||||
|
for task in pending:
|
||||||
|
task.cancel()
|
||||||
|
|
||||||
|
async def _ota(self):
|
||||||
|
"""Check for new fw and update or pass"""
|
||||||
|
device_name = self.msg['name']
|
||||||
|
device_chip = self.msg['chip']
|
||||||
|
device_version = self.msg['version']
|
||||||
|
fw_version = ''
|
||||||
|
fw_name = ''
|
||||||
|
fw_device = ''
|
||||||
|
|
||||||
|
for filename in listdir(fw_path):
|
||||||
|
fw_info = filename.split("-")
|
||||||
|
fw_device = fw_info[0]
|
||||||
|
if fw_device == device_name:
|
||||||
|
fw_version = fw_info[1]
|
||||||
|
fw_name = filename
|
||||||
|
break
|
||||||
|
|
||||||
|
if not fw_version:
|
||||||
|
Logger.info("Client(%s): No fw found!", device_name)
|
||||||
|
msg = '{"type": "ota", "value":"ok"}'
|
||||||
|
await self.websocket.send(msg)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not version_checker(device_name, device_version, fw_version):
|
||||||
|
return
|
||||||
|
|
||||||
|
fw_file = join_pth(fw_path, fw_name)
|
||||||
|
if device_chip == 'esp8266' and not fw_file.endswith('.gz'):
|
||||||
|
# We can compress fw to make it smaller for upload
|
||||||
|
fw_cpress = fw_file
|
||||||
|
fw_file = fw_cpress + ".gz"
|
||||||
|
cpress = f"gzip -9 {fw_cpress}"
|
||||||
|
cstate = shell(cpress)
|
||||||
|
if cstate:
|
||||||
|
Logger.error("Cannot compress firmware: %s", fw_name)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get size of fw
|
||||||
|
size = Path(fw_file).stat().st_size
|
||||||
|
|
||||||
|
# Request ota mode
|
||||||
|
msg = '{"type": "ota", "value":"go", "size":' + str(size) + '}'
|
||||||
|
await self.websocket.send(msg)
|
||||||
|
|
||||||
|
# send file by chunks trough websocket
|
||||||
|
await binary_send(self.websocket, fw_file)
|
||||||
|
|
||||||
|
|
||||||
|
async def _register(websocket, message):
|
||||||
|
mac = message.get('mac')
|
||||||
|
name = message.get('name')
|
||||||
|
Logger.info("Client(%s) mac: %s", name, mac)
|
||||||
|
# Some code
|
||||||
|
|
||||||
|
response = {'response_type': 'registry', 'state': 'ok'}
|
||||||
|
await websocket.send(json.dumps(response))
|
||||||
|
|
||||||
|
|
||||||
|
async def _state(websocket, message):
|
||||||
|
mac = message.get('mac')
|
||||||
|
name = message.get('name')
|
||||||
|
Logger.info("Client(%s) mac: %s", name, mac)
|
||||||
|
# Some code
|
||||||
|
|
||||||
|
response = {'response_type': 'state', 'state': 'ok'}
|
||||||
|
await websocket.send(json.dumps(response))
|
||||||
|
|
||||||
|
|
||||||
|
async def _unhandleld(websocket, msg):
|
||||||
|
Logger.info("Unhandled message from device: %s", str(msg))
|
||||||
|
response = {'response_type': 'response', 'state': 'nok'}
|
||||||
|
await websocket.send(json.dumps(response))
|
||||||
|
|
||||||
|
|
||||||
|
async def _greetings(websocket, message):
|
||||||
|
WsOtaHandler('thread_ota', copy.deepcopy(message), websocket).start()
|
||||||
|
|
||||||
|
|
||||||
|
async def message_received(websocket, message) -> None:
|
||||||
|
"""Handle incoming messages
|
||||||
|
|
||||||
|
Check if message contain json and run waned function
|
||||||
|
"""
|
||||||
|
switcher = {"greetings": _greetings,
|
||||||
|
"register": _register,
|
||||||
|
"state": _state
|
||||||
|
}
|
||||||
|
|
||||||
|
if message[0:1] == "{":
|
||||||
|
try:
|
||||||
|
msg_json = json.loads(message)
|
||||||
|
except Exception as exception:
|
||||||
|
Logger.error(exception)
|
||||||
|
return
|
||||||
|
|
||||||
|
type_ = msg_json.get('type')
|
||||||
|
name = msg_json.get('name')
|
||||||
|
func = switcher.get(type_, _unhandleld)
|
||||||
|
Logger.debug("Client(%s)said: %s", name, type_)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await func(websocket, msg_json)
|
||||||
|
except Exception as exception:
|
||||||
|
Logger.error(exception)
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=W0613
|
||||||
|
async def ws_server(websocket, path) -> None:
|
||||||
|
"""Run in cycle and wait for new messages"""
|
||||||
|
async for message in websocket:
|
||||||
|
await message_received(websocket, message)
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
"""Server starter
|
||||||
|
|
||||||
|
Normal user can bind only port nubers greater than 1024
|
||||||
|
"""
|
||||||
|
async with websockets.serve(ws_server, "10.0.1.5", 8081):
|
||||||
|
await asyncio.Future() # run forever
|
||||||
|
|
||||||
|
|
||||||
|
create_path(fw_path)
|
||||||
|
asyncio.run(main())
|
@ -0,0 +1,2 @@
|
|||||||
|
packaging
|
||||||
|
websockets
|
Reference in New Issue
Block a user