mirror of
https://github.com/KevinMidboe/hivemonitor-esp32-firmware.git
synced 2025-10-29 09:30:26 +00:00
Gateway for listening for ESPNOW and proxying to mqtt over wifi
This commit is contained in:
203
src/gateway.py
Normal file
203
src/gateway.py
Normal file
@@ -0,0 +1,203 @@
|
||||
import network, time, utime, json, machine, esp32, ntptime
|
||||
import espnow
|
||||
from machine import Pin, ADC
|
||||
from umqtt.simple import MQTTClient
|
||||
from ubinascii import b2a_base64
|
||||
|
||||
from configuration_server import serveSetupServer, getStorageVar
|
||||
|
||||
REBOOT_DELAY = 2
|
||||
wifiChannel = -1
|
||||
# BASE_TOPIC = getStorageVar('mqtt_topic')
|
||||
|
||||
|
||||
class Wifi():
|
||||
def __init__(self):
|
||||
self.sta = network.WLAN(network.STA_IF)
|
||||
self.channel = -1
|
||||
|
||||
# The power saving mode causes the device to turn off the radio
|
||||
# periodically (typically for hundreds of milliseconds), making
|
||||
# it unreliable in receiving
|
||||
def disablePowerSavings(self):
|
||||
self.sta.config(pm=self.sta.PM_NONE)
|
||||
|
||||
def setChannel(self):
|
||||
global wifiChannel
|
||||
self.channel = self.sta.config("channel")
|
||||
wifiChannel = self.channel
|
||||
print("Proxy running on channel:", self.channel)
|
||||
|
||||
def connect(self, ssid, password):
|
||||
self.sta.active(True)
|
||||
self.sta.connect(ssid, password)
|
||||
while not self.sta.isconnected(): # Wait until connected...
|
||||
time.sleep(0.5)
|
||||
print('waiting to connect to wifi...')
|
||||
|
||||
self.sta.config(pm=self.sta.PM_NONE)
|
||||
print('Connected to wifi "%s"' % ssid)
|
||||
|
||||
self.disablePowerSavings()
|
||||
self.setChannel()
|
||||
|
||||
|
||||
class ESPNowServer():
|
||||
def __init__(self):
|
||||
self.ap = network.WLAN(network.AP_IF)
|
||||
self.server = None
|
||||
self.mqttClient = None
|
||||
|
||||
def setup(self):
|
||||
self.server = espnow.ESPNow()
|
||||
self.server.active(True)
|
||||
print("Successtully activate ESP now on AP antenna")
|
||||
|
||||
def receive(self, mqttClient):
|
||||
count = 0
|
||||
mqttClient.publishGatewayStatus()
|
||||
|
||||
while True:
|
||||
# TODO change to irecv, bytearray instead of bytestring
|
||||
peer, msg = self.server.recv()
|
||||
if msg is not None and msg not in [b'end'] and 'ack' not in msg:
|
||||
payload = parse_json(msg.decode('utf-8'))
|
||||
hiveName = payload['hive_name']
|
||||
|
||||
mqttClient.relayTelemetry(hiveName, payload)
|
||||
count = count + 1
|
||||
|
||||
elif count > 20:
|
||||
mqttClient.publishGatewayStatus()
|
||||
count = 0
|
||||
|
||||
|
||||
class TelemetryMQTT():
|
||||
def __init__(self, clientId, brokerUrl):
|
||||
self.clientId = clientId
|
||||
self.mqttUrl = brokerUrl
|
||||
self.client = None
|
||||
self.keepalive = 60
|
||||
|
||||
def connect(self):
|
||||
self.client = MQTTClient(self.clientId, self.mqttUrl, port=31883, keepalive=self.keepalive)
|
||||
|
||||
try:
|
||||
self.client.connect(clean_session=True)
|
||||
print("Connected to MQTT broker")
|
||||
except:
|
||||
print('Unable to connect to MQTT broker')
|
||||
reboot()
|
||||
|
||||
def publishGatewayStatus(self, persist=True):
|
||||
topic = 'telemetry/gateway/{}'.format(self.clientId)
|
||||
payload = str({
|
||||
'gateway_name': self.clientId,
|
||||
't': getTime(),
|
||||
'temperature': readTemperature(),
|
||||
'channel': wifiChannel
|
||||
}).replace("'", '"').encode()
|
||||
|
||||
self.client.publish(topic, payload, persist)
|
||||
|
||||
|
||||
def relayTelemetry(self, topic, msg, persist=True):
|
||||
topic = 'telemetry/hive/{}'.format(topic)
|
||||
msg['t'] = getTime()
|
||||
payload = str(msg).replace("'", '"').encode()
|
||||
|
||||
self.client.publish(topic, payload, persist)
|
||||
|
||||
|
||||
def resetAntennas(): # Reset wifi to AP_IF off, STA_IF on and disconnected
|
||||
sta = network.WLAN(network.STA_IF)
|
||||
ap = network.WLAN(network.AP_IF)
|
||||
sta.active(False)
|
||||
ap.active(False)
|
||||
|
||||
sta.active(True)
|
||||
while not sta.active():
|
||||
time.sleep(0.1)
|
||||
print('sta.active():')
|
||||
while sta.isconnected():
|
||||
time.sleep(0.1)
|
||||
print('sta.isconnected():')
|
||||
|
||||
|
||||
def reboot(delay = REBOOT_DELAY):
|
||||
print (f'Rebooting device in {delay} seconds (Ctrl-C to escape).')
|
||||
utime.sleep(delay)
|
||||
machine.reset()
|
||||
|
||||
|
||||
def parse_json(json_str):
|
||||
try:
|
||||
# Evaluate the JSON string to convert it into a Python dictionary
|
||||
result_dict = eval(json_str)
|
||||
if not isinstance(result_dict, dict):
|
||||
raise ValueError("JSON does not represent a valid object.")
|
||||
return result_dict
|
||||
except Exception as e:
|
||||
raise ValueError("Invalid JSON format:", str(e))
|
||||
|
||||
|
||||
def syncTime():
|
||||
ntptime.settime()
|
||||
|
||||
|
||||
def getTime():
|
||||
[ y, M, d, h, m, s, dow, doy] = time.localtime()
|
||||
return '{}-{:02d}-{:02d}T{:02d}:{:02d}:{:02d}Z'.format(y, M, d, h, m, s)
|
||||
|
||||
|
||||
def readTemperature():
|
||||
fahrenheit = esp32.raw_temperature()
|
||||
celcius = (fahrenheit - 32) / 1.8
|
||||
return "{0:.2f}".format(celcius)
|
||||
|
||||
|
||||
# def readBatteryLevel():
|
||||
# adc = ADC(Pin(13, Pin.IN))
|
||||
# voltage = (adc.read() / 4095) * 2 * 3.3 * 1.1
|
||||
# # return 22
|
||||
# return "{0:.2f}".format(voltage)
|
||||
|
||||
|
||||
def setupAndProxy():
|
||||
resetAntennas()
|
||||
espNowServer = ESPNowServer()
|
||||
espNowServer.setup()
|
||||
|
||||
ssid = getStorageVar('ssid')
|
||||
password = getStorageVar('pass')
|
||||
wifi = Wifi()
|
||||
wifi.connect(ssid, password)
|
||||
|
||||
brokerUrl = getStorageVar('mqtt_broker')
|
||||
mqttClient = TelemetryMQTT('House', brokerUrl)
|
||||
mqttClient.connect()
|
||||
|
||||
syncTime()
|
||||
return espNowServer, mqttClient
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
modeSwitch = Pin(26, Pin.IN, Pin.PULL_UP)
|
||||
mode = modeSwitch.value()
|
||||
|
||||
try:
|
||||
if mode == 1:
|
||||
[espNowServer, mqttClient] = setupAndProxy()
|
||||
print('activated and waiting for messages')
|
||||
|
||||
espNowServer.receive(mqttClient)
|
||||
else:
|
||||
print('serving setup server')
|
||||
serveSetupServer()
|
||||
|
||||
except KeyboardInterrupt as err:
|
||||
raise err # use Ctrl-C to exit to micropython repl
|
||||
except Exception as err:
|
||||
print ('Error during execution:', err)
|
||||
# raise err
|
||||
reboot()
|
||||
105
src/setup/gateway.html
Normal file
105
src/setup/gateway.html
Normal file
@@ -0,0 +1,105 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width,initial-scale=1,maximum-scale=1"
|
||||
/>
|
||||
<title></title>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="./styles.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Hive setup page</h1>
|
||||
|
||||
<section>
|
||||
<p>Configure your microcontroller with the input fields below.</p>
|
||||
|
||||
<button class="light" id="device-info">View device info</button>
|
||||
<button class="light" id="identify">Identify</button>
|
||||
|
||||
<form action="/save" method="POST">
|
||||
<h3>General</h3>
|
||||
|
||||
<div>
|
||||
<label for="name">Hive name</label>
|
||||
<input id="name" name="name" enterkeyhint="send" value="{{ name }}" />
|
||||
</div>
|
||||
|
||||
<h3>MQTT</h3>
|
||||
<div>
|
||||
<label for="mqtt_broker">MQTT broker</label>
|
||||
<input
|
||||
id="mqtt_broker"
|
||||
name="mqtt_broker"
|
||||
enterkeyhint="send"
|
||||
value="{{ mqtt_broker }}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="mqtt_topic">MQTT topic</label>
|
||||
<input
|
||||
id="mqtt_topic"
|
||||
name="mqtt_topic"
|
||||
enterkeyhint="send"
|
||||
value="{{ mqtt_topic }}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3>Wifi</h3>
|
||||
<div>
|
||||
<label for="ssid">SSID</label>
|
||||
<input id="ssid" name="ssid" enterkeyhint="send" value="{{ ssid }}" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="pass">Password</label>
|
||||
<input
|
||||
id="pass"
|
||||
name="pass"
|
||||
enterkeyhint="send"
|
||||
type="password"
|
||||
value="{{ pass }}"
|
||||
/>
|
||||
<i id="password">🙈</i>
|
||||
</div>
|
||||
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<dialog id="dialog">
|
||||
<form>
|
||||
<p><b>MAC address: </b><span>{{ mac }}</span></p>
|
||||
<p><b>CPU freq: </b><span>{{ freq }} Mhz</span></p>
|
||||
|
||||
<button value="cancel" formmethod="dialog">Close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
</body>
|
||||
|
||||
<script>
|
||||
function togglePasswordVisiblity(event) {
|
||||
const label = event.target;
|
||||
const input = label.parentElement.getElementsByTagName('input')[0];
|
||||
label.innerText = input.type === 'password' ? '🐵' : '🙈';
|
||||
input.type = input.type === 'password' ? 'text' : 'password';
|
||||
}
|
||||
|
||||
function identifyApiCall() {
|
||||
fetch('identify', { method: 'POST' });
|
||||
}
|
||||
|
||||
const deviceBtn = document.getElementById('device-info');
|
||||
const identifyBtn = document.getElementById('identify');
|
||||
const passwordBtn = document.getElementById('password');
|
||||
const dialog = document.getElementById("dialog");
|
||||
|
||||
deviceBtn.addEventListener('click', () => dialog.showModal());
|
||||
passwordBtn.addEventListener('click', togglePasswordVisiblity);
|
||||
identifyBtn.addEventListener('click', identifyApiCall);
|
||||
</script>
|
||||
</html>
|
||||
Reference in New Issue
Block a user