Fix negative Lat an Long value entry

5.12.0l
 * Fix negative Latitude and Longitude value entry (#2461)
This commit is contained in:
Theo Arends 2018-04-17 15:34:18 +02:00
parent 8e0a25f681
commit 5858ecdb79
33 changed files with 4054 additions and 36 deletions

View File

@ -0,0 +1,12 @@
# ESPAsyncUDP
_Library patched with the [PR #21](https://github.com/me-no-dev/ESPAsyncUDP/pull/21)_
Async UDP Library for ESP8266 Arduino [![Build Status](https://travis-ci.org/me-no-dev/ESPAsyncUDP.svg?branch=master)](https://travis-ci.org/me-no-dev/ESPAsyncUDP)
[![Join the chat at https://gitter.im/me-no-dev/ESPAsyncWebServer](https://badges.gitter.im/me-no-dev/ESPAsyncWebServer.svg)](https://gitter.im/me-no-dev/ESPAsyncWebServer?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
This is a fully asynchronous UDP library, aimed at enabling trouble-free, multi-connection network environment for Espressif's ESP8266 MCUs.
The library is easy to use and includes support for Unicast, Broadcast and Multicast environments
Latest GIT version of ESP8266 Arduino might be required for this library to work

View File

@ -0,0 +1,51 @@
#include <ESP8266WiFi.h>
#include "ESPAsyncUDP.h"
const char * ssid = "***********";
const char * password = "***********";
AsyncUDP udp;
void setup()
{
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
if (WiFi.waitForConnectResult() != WL_CONNECTED) {
Serial.println("WiFi Failed");
while(1) {
delay(1000);
}
}
if(udp.connect(IPAddress(192,168,1,100), 1234)) {
Serial.println("UDP connected");
udp.onPacket([](AsyncUDPPacket packet) {
Serial.print("UDP Packet Type: ");
Serial.print(packet.isBroadcast()?"Broadcast":packet.isMulticast()?"Multicast":"Unicast");
Serial.print(", From: ");
Serial.print(packet.remoteIP());
Serial.print(":");
Serial.print(packet.remotePort());
Serial.print(", To: ");
Serial.print(packet.localIP());
Serial.print(":");
Serial.print(packet.localPort());
Serial.print(", Length: ");
Serial.print(packet.length());
Serial.print(", Data: ");
Serial.write(packet.data(), packet.length());
Serial.println();
//reply to the client
packet.printf("Got %u bytes of data", packet.length());
});
//Send unicast
udp.print("Hello Server!");
}
}
void loop()
{
delay(1000);
//Send broadcast on port 1234
udp.broadcastTo("Anyone here?", 1234);
}

View File

@ -0,0 +1,52 @@
#include <ESP8266WiFi.h>
#include "ESPAsyncUDP.h"
const char * ssid = "***********";
const char * password = "***********";
AsyncUDP udp;
void setup()
{
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
if (WiFi.waitForConnectResult() != WL_CONNECTED) {
Serial.println("WiFi Failed");
while(1) {
delay(1000);
}
}
if(udp.listenMulticast(IPAddress(239,1,2,3), 1234)) {
Serial.print("UDP Listening on IP: ");
Serial.println(WiFi.localIP());
udp.onPacket([](AsyncUDPPacket packet) {
Serial.print("UDP Packet Type: ");
Serial.print(packet.isBroadcast()?"Broadcast":packet.isMulticast()?"Multicast":"Unicast");
Serial.print(", From: ");
Serial.print(packet.remoteIP());
Serial.print(":");
Serial.print(packet.remotePort());
Serial.print(", To: ");
Serial.print(packet.localIP());
Serial.print(":");
Serial.print(packet.localPort());
Serial.print(", Length: ");
Serial.print(packet.length());
Serial.print(", Data: ");
Serial.write(packet.data(), packet.length());
Serial.println();
//reply to the client
packet.printf("Got %u bytes of data", packet.length());
});
//Send multicast
udp.print("Hello!");
}
}
void loop()
{
delay(1000);
//Send multicast
udp.print("Anyone here?");
}

View File

@ -0,0 +1,50 @@
#include <ESP8266WiFi.h>
#include "ESPAsyncUDP.h"
const char * ssid = "***********";
const char * password = "***********";
AsyncUDP udp;
void setup()
{
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
if (WiFi.waitForConnectResult() != WL_CONNECTED) {
Serial.println("WiFi Failed");
while(1) {
delay(1000);
}
}
if(udp.listen(1234)) {
Serial.print("UDP Listening on IP: ");
Serial.println(WiFi.localIP());
udp.onPacket([](AsyncUDPPacket packet) {
Serial.print("UDP Packet Type: ");
Serial.print(packet.isBroadcast()?"Broadcast":packet.isMulticast()?"Multicast":"Unicast");
Serial.print(", From: ");
Serial.print(packet.remoteIP());
Serial.print(":");
Serial.print(packet.remotePort());
Serial.print(", To: ");
Serial.print(packet.localIP());
Serial.print(":");
Serial.print(packet.localPort());
Serial.print(", Length: ");
Serial.print(packet.length());
Serial.print(", Data: ");
Serial.write(packet.data(), packet.length());
Serial.println();
//reply to the client
packet.printf("Got %u bytes of data", packet.length());
});
}
}
void loop()
{
delay(1000);
//Send broadcast
udp.broadcast("Anyone here?");
}

View File

@ -0,0 +1,33 @@
#######################################
# Syntax Coloring Map For Ultrasound
#######################################
#######################################
# Datatypes (KEYWORD1)
#######################################
AsyncUDP KEYWORD1
AsyncUDPPacket KEYWORD1
#######################################
# Methods and Functions (KEYWORD2)
#######################################
connect KEYWORD2
connected KEYWORD2
listen KEYWORD2
listenMulticast KEYWORD2
close KEYWORD2
write KEYWORD2
broadcast KEYWORD2
onPacket KEYWORD2
data KEYWORD2
length KEYWORD2
localIP KEYWORD2
localPort KEYWORD2
remoteIP KEYWORD2
remotePort KEYWORD2
#######################################
# Constants (LITERAL1)
#######################################

View File

@ -0,0 +1,17 @@
{
"name":"ESPAsyncUDP",
"description":"Asynchronous UDP Library for ESP8266",
"keywords":"async,udp,server,client,multicast,broadcast",
"authors":
{
"name": "Hristo Gochkov",
"maintainer": true
},
"repository":
{
"type": "git",
"url": "https://github.com/me-no-dev/ESPAsyncUDP.git"
},
"frameworks": "arduino",
"platforms":"espressif"
}

View File

@ -0,0 +1,9 @@
name=ESP Async UDP
version=1.0.0
author=Me-No-Dev
maintainer=Me-No-Dev
sentence=Async UDP Library for ESP8266
paragraph=Async UDP Library for ESP8266
category=Other
url=https://github.com/me-no-dev/ESPAsyncUDP
architectures=*

View File

@ -0,0 +1,425 @@
#include "Arduino.h"
#include "ESPAsyncUDP.h"
extern "C" {
#include "user_interface.h"
#include "lwip/opt.h"
#include "lwip/inet.h"
#include "lwip/udp.h"
#include "lwip/igmp.h"
}
AsyncUDPMessage::AsyncUDPMessage(size_t size)
{
_index = 0;
if(size > 1460) {
size = 1460;
}
_size = size;
_buffer = (uint8_t *)malloc(size);
}
AsyncUDPMessage::~AsyncUDPMessage()
{
if(_buffer) {
free(_buffer);
}
}
size_t AsyncUDPMessage::write(const uint8_t *data, size_t len)
{
if(_buffer == NULL) {
return 0;
}
size_t s = space();
if(len > s) {
len = s;
}
memcpy(_buffer + _index, data, len);
_index += len;
return len;
}
size_t AsyncUDPMessage::write(uint8_t data)
{
return write(&data, 1);
}
size_t AsyncUDPMessage::space()
{
if(_buffer == NULL) {
return 0;
}
return _size - _index;
}
uint8_t * AsyncUDPMessage::data()
{
return _buffer;
}
size_t AsyncUDPMessage::length()
{
return _index;
}
void AsyncUDPMessage::flush()
{
_index = 0;
}
AsyncUDPPacket::AsyncUDPPacket(AsyncUDP *udp, ip_addr_t *localIp, uint16_t localPort, ip_addr_t *remoteIp, uint16_t remotePort, uint8_t *data, size_t len)
{
_udp = udp;
_localIp = localIp;
_localPort = localPort;
_remoteIp = remoteIp;
_remotePort = remotePort;
_data = data;
_len = len;
}
AsyncUDPPacket::~AsyncUDPPacket()
{
}
uint8_t * AsyncUDPPacket::data()
{
return _data;
}
size_t AsyncUDPPacket::length()
{
return _len;
}
IPAddress AsyncUDPPacket::localIP()
{
return IPAddress(_localIp->addr);
}
uint16_t AsyncUDPPacket::localPort()
{
return _localPort;
}
IPAddress AsyncUDPPacket::remoteIP()
{
return IPAddress(_remoteIp->addr);
}
uint16_t AsyncUDPPacket::remotePort()
{
return _remotePort;
}
bool AsyncUDPPacket::isBroadcast()
{
return _localIp->addr == 0xFFFFFFFF || _localIp->addr == (uint32_t)(0);
}
bool AsyncUDPPacket::isMulticast()
{
return ip_addr_ismulticast(_localIp);
}
size_t AsyncUDPPacket::write(const uint8_t *data, size_t len)
{
return _udp->writeTo(data, len, _remoteIp, _remotePort);
}
size_t AsyncUDPPacket::write(uint8_t data)
{
return write(&data, 1);
}
size_t AsyncUDPPacket::send(AsyncUDPMessage &message)
{
return write(message.data(), message.length());
}
AsyncUDP::AsyncUDP()
{
_pcb = NULL;
_connected = false;
_handler = NULL;
}
AsyncUDP::~AsyncUDP()
{
close();
}
AsyncUDP::operator bool()
{
return _connected;
}
bool AsyncUDP::connected()
{
return _connected;
}
void AsyncUDP::onPacket(AuPacketHandlerFunctionWithArg cb, void * arg)
{
onPacket(std::bind(cb, arg, std::placeholders::_1));
}
void AsyncUDP::onPacket(AuPacketHandlerFunction cb)
{
_handler = cb;
}
void AsyncUDP::_recv(udp_pcb *upcb, pbuf *pb, ip_addr_t *addr, uint16_t port)
{
(void)upcb; // its unused, avoid warning
while(pb != NULL) {
if(_handler) {
uint8_t * data = (uint8_t*)((pb)->payload);
size_t len = pb->len;
ip_hdr* iphdr = reinterpret_cast<ip_hdr*>(data - UDP_HLEN - IP_HLEN);
ip_addr_t daddr;
daddr.addr = iphdr->dest.addr;
udp_hdr* udphdr = reinterpret_cast<udp_hdr*>(((uint8_t*)((pb)->payload)) - UDP_HLEN);
uint16_t dport = ntohs(udphdr->dest);
AsyncUDPPacket packet(this, &daddr, dport, addr, port, data, len);
_handler(packet);
}
pbuf * this_pb = pb;
pb = pb->next;
this_pb->next = NULL;
pbuf_free(this_pb);
}
}
#if LWIP_VERSION_MAJOR == 1
void AsyncUDP::_s_recv(void *arg, udp_pcb *upcb, pbuf *p, ip_addr_t *addr, uint16_t port)
#else
void AsyncUDP::_s_recv(void *arg, udp_pcb *upcb, pbuf *p, const ip_addr_t *addr, uint16_t port)
#endif
{
reinterpret_cast<AsyncUDP*>(arg)->_recv(upcb, p, (ip_addr_t *)addr, port);
}
bool AsyncUDP::listen(ip_addr_t *addr, uint16_t port)
{
close();
_pcb = udp_new();
if(_pcb == NULL) {
return false;
}
err_t err = udp_bind(_pcb, addr, port);
if(err != ERR_OK) {
close();
return false;
}
udp_recv(_pcb, &_s_recv, (void *) this);
_connected = true;
return true;
}
bool AsyncUDP::listenMulticast(ip_addr_t *addr, uint16_t port, uint8_t ttl)
{
close();
if(!ip_addr_ismulticast(addr)) {
return false;
}
ip_addr_t multicast_if_addr;
struct ip_info ifIpInfo;
int mode = wifi_get_opmode();
if(mode & STATION_MODE) {
wifi_get_ip_info(STATION_IF, &ifIpInfo);
multicast_if_addr.addr = ifIpInfo.ip.addr;
} else if (mode & SOFTAP_MODE) {
wifi_get_ip_info(SOFTAP_IF, &ifIpInfo);
multicast_if_addr.addr = ifIpInfo.ip.addr;
} else {
return false;
}
if (igmp_joingroup(&multicast_if_addr, addr)!= ERR_OK) {
return false;
}
if(!listen(IPADDR_ANY, port)) {
return false;
}
#if LWIP_VERSION_MAJOR == 1
udp_set_multicast_netif_addr(_pcb, multicast_if_addr);
#else
udp_set_multicast_netif_addr(_pcb, &multicast_if_addr);
#endif
udp_set_multicast_ttl(_pcb, ttl);
ip_addr_copy(_pcb->remote_ip, *addr);
_pcb->remote_port = port;
return true;
}
bool AsyncUDP::connect(ip_addr_t *addr, uint16_t port)
{
close();
_pcb = udp_new();
if(_pcb == NULL) {
return false;
}
err_t err = udp_connect(_pcb, addr, port);
if(err != ERR_OK) {
close();
return false;
}
udp_recv(_pcb, &_s_recv, (void *) this);
_connected = true;
return true;
}
void AsyncUDP::close()
{
if(_pcb != NULL) {
if(_connected) {
udp_disconnect(_pcb);
}
udp_remove(_pcb);
_connected = false;
_pcb = NULL;
}
}
size_t AsyncUDP::writeTo(const uint8_t *data, size_t len, ip_addr_t *addr, uint16_t port)
{
if(!_pcb && !connect(addr, port)) {
return 0;
}
if(len > 1460) {
len = 1460;
}
pbuf* pbt = pbuf_alloc(PBUF_TRANSPORT, len, PBUF_RAM);
if(pbt != NULL) {
uint8_t* dst = reinterpret_cast<uint8_t*>(pbt->payload);
memcpy(dst, data, len);
err_t err = udp_sendto(_pcb, pbt, addr, port);
pbuf_free(pbt);
if(err < ERR_OK) {
return 0;
}
return len;
}
return 0;
}
bool AsyncUDP::listen(const IPAddress addr, uint16_t port)
{
ip_addr_t laddr;
laddr.addr = addr;
return listen(&laddr, port);
}
bool AsyncUDP::listen(uint16_t port)
{
return listen(IPAddress((uint32_t)INADDR_ANY), port);
}
bool AsyncUDP::listenMulticast(const IPAddress addr, uint16_t port, uint8_t ttl)
{
ip_addr_t laddr;
laddr.addr = addr;
return listenMulticast(&laddr, port, ttl);
}
bool AsyncUDP::connect(const IPAddress addr, uint16_t port)
{
ip_addr_t daddr;
daddr.addr = addr;
return connect(&daddr, port);
}
size_t AsyncUDP::writeTo(const uint8_t *data, size_t len, const IPAddress addr, uint16_t port)
{
ip_addr_t daddr;
daddr.addr = addr;
return writeTo(data, len, &daddr, port);
}
size_t AsyncUDP::write(const uint8_t *data, size_t len)
{
if(_pcb)
{
return writeTo(data, len, &(_pcb->remote_ip), _pcb->remote_port);
}
}
size_t AsyncUDP::write(uint8_t data)
{
return write(&data, 1);
}
size_t AsyncUDP::broadcastTo(uint8_t *data, size_t len, uint16_t port)
{
ip_addr_t daddr;
daddr.addr = 0xFFFFFFFF;
return writeTo(data, len, &daddr, port);
}
size_t AsyncUDP::broadcastTo(const char * data, uint16_t port)
{
return broadcastTo((uint8_t *)data, strlen(data), port);
}
size_t AsyncUDP::broadcast(uint8_t *data, size_t len)
{
if(_pcb->local_port != 0) {
return broadcastTo(data, len, _pcb->local_port);
}
return 0;
}
size_t AsyncUDP::broadcast(const char * data)
{
return broadcast((uint8_t *)data, strlen(data));
}
size_t AsyncUDP::sendTo(AsyncUDPMessage &message, ip_addr_t *addr, uint16_t port)
{
if(!message) {
return 0;
}
return writeTo(message.data(), message.length(), addr, port);
}
size_t AsyncUDP::sendTo(AsyncUDPMessage &message, const IPAddress addr, uint16_t port)
{
if(!message) {
return 0;
}
return writeTo(message.data(), message.length(), addr, port);
}
size_t AsyncUDP::send(AsyncUDPMessage &message)
{
if((!message) || (!_pcb)) {
return 0;
}
return writeTo(message.data(), message.length(), &(_pcb->remote_ip), _pcb->remote_port);
}
size_t AsyncUDP::broadcastTo(AsyncUDPMessage &message, uint16_t port)
{
if(!message) {
return 0;
}
return broadcastTo(message.data(), message.length(), port);
}
size_t AsyncUDP::broadcast(AsyncUDPMessage &message)
{
if(!message) {
return 0;
}
return broadcast(message.data(), message.length());
}

View File

@ -0,0 +1,130 @@
#ifndef ESPASYNCUDP_H
#define ESPASYNCUDP_H
#include "IPAddress.h"
#include "Print.h"
#include <functional>
#include "lwip/init.h"
class AsyncUDP;
class AsyncUDPPacket;
class AsyncUDPMessage;
struct udp_pcb;
struct pbuf;
#if LWIP_VERSION_MAJOR == 1
struct ip_addr;
typedef struct ip_addr ip_addr_t;
#else
struct ip4_addr;
typedef struct ip4_addr ip_addr_t;
#endif
class AsyncUDPMessage : public Print
{
protected:
uint8_t *_buffer;
size_t _index;
size_t _size;
public:
AsyncUDPMessage(size_t size=1460);
virtual ~AsyncUDPMessage();
size_t write(const uint8_t *data, size_t len);
size_t write(uint8_t data);
size_t space();
uint8_t * data();
size_t length();
void flush();
operator bool()
{
return _buffer != NULL;
}
};
class AsyncUDPPacket : public Print
{
protected:
AsyncUDP *_udp;
ip_addr_t *_localIp;
uint16_t _localPort;
ip_addr_t *_remoteIp;
uint16_t _remotePort;
uint8_t *_data;
size_t _len;
public:
AsyncUDPPacket(AsyncUDP *udp, ip_addr_t *localIp, uint16_t localPort, ip_addr_t *remoteIp, uint16_t remotePort, uint8_t *data, size_t len);
virtual ~AsyncUDPPacket();
uint8_t * data();
size_t length();
bool isBroadcast();
bool isMulticast();
IPAddress localIP();
uint16_t localPort();
IPAddress remoteIP();
uint16_t remotePort();
size_t send(AsyncUDPMessage &message);
size_t write(const uint8_t *data, size_t len);
size_t write(uint8_t data);
};
typedef std::function<void(AsyncUDPPacket& packet)> AuPacketHandlerFunction;
typedef std::function<void(void * arg, AsyncUDPPacket& packet)> AuPacketHandlerFunctionWithArg;
class AsyncUDP : public Print
{
protected:
udp_pcb *_pcb;
bool _connected;
AuPacketHandlerFunction _handler;
void _recv(udp_pcb *upcb, pbuf *pb, ip_addr_t *addr, uint16_t port);
#if LWIP_VERSION_MAJOR == 1
static void _s_recv(void *arg, udp_pcb *upcb, pbuf *p, ip_addr_t *addr, uint16_t port);
#else
static void _s_recv(void *arg, udp_pcb *upcb, pbuf *p, const ip_addr_t *addr, uint16_t port);
#endif
public:
AsyncUDP();
virtual ~AsyncUDP();
void onPacket(AuPacketHandlerFunctionWithArg cb, void * arg=NULL);
void onPacket(AuPacketHandlerFunction cb);
bool listen(ip_addr_t *addr, uint16_t port);
bool listen(const IPAddress addr, uint16_t port);
bool listen(uint16_t port);
bool listenMulticast(ip_addr_t *addr, uint16_t port, uint8_t ttl=1);
bool listenMulticast(const IPAddress addr, uint16_t port, uint8_t ttl=1);
bool connect(ip_addr_t *addr, uint16_t port);
bool connect(const IPAddress addr, uint16_t port);
void close();
size_t writeTo(const uint8_t *data, size_t len, ip_addr_t *addr, uint16_t port);
size_t writeTo(const uint8_t *data, size_t len, const IPAddress addr, uint16_t port);
size_t write(const uint8_t *data, size_t len);
size_t write(uint8_t data);
size_t broadcastTo(uint8_t *data, size_t len, uint16_t port);
size_t broadcastTo(const char * data, uint16_t port);
size_t broadcast(uint8_t *data, size_t len);
size_t broadcast(const char * data);
size_t sendTo(AsyncUDPMessage &message, ip_addr_t *addr, uint16_t port);
size_t sendTo(AsyncUDPMessage &message, const IPAddress addr, uint16_t port);
size_t send(AsyncUDPMessage &message);
size_t broadcastTo(AsyncUDPMessage &message, uint16_t port);
size_t broadcast(AsyncUDPMessage &message);
bool connected();
operator bool();
};
#endif

View File

@ -0,0 +1,23 @@
#!/bin/bash
function build_sketches()
{
local arduino=$1
local srcpath=$2
local platform=$3
local sketches=$(find $srcpath -name *.ino)
for sketch in $sketches; do
local sketchdir=$(dirname $sketch)
if [[ -f "$sketchdir/.$platform.skip" ]]; then
echo -e "\n\n ------------ Skipping $sketch ------------ \n\n";
continue
fi
echo -e "\n\n ------------ Building $sketch ------------ \n\n";
$arduino --verify $sketch;
local result=$?
if [ $result -ne 0 ]; then
echo "Build failed ($1)"
return $result
fi
done
}

View File

@ -0,0 +1,72 @@
/**
* esp-knx-ip library for KNX/IP communication on an ESP8266
* Author: Nico Weichbrodt <envy>
* License: MIT
*/
typedef enum __dpt_1_001
{
DPT_1_001_OFF = 0x00,
DPT_1_001_ON = 0x01,
} dpt_1_001_t;
typedef enum __dpt_2_001
{
DPT_2_001_NO_OFF = 0b00,
DPT_2_001_NO_ON = 0b01,
DPT_2_001_YES_OFF = 0b10,
DPT_2_001_YES_ON = 0b11,
} dpt_2_001_t;
typedef enum __dpt_3_007
{
DPT_3_007_DECREASE_STOP = 0x00,
DPT_3_007_DECREASE_100 = 0x01,
DPT_3_007_DECREASE_50 = 0x02,
DPT_3_007_DECREASE_25 = 0x03,
DPT_3_007_DECREASE_12 = 0x04,
DPT_3_007_DECREASE_6 = 0x05,
DPT_3_007_DECREASE_3 = 0x06,
DPT_3_007_DECREASE_1 = 0x07,
DPT_3_007_INCREASE_STOP = 0x08,
DPT_3_007_INCREASE_100 = 0x09,
DPT_3_007_INCREASE_50 = 0x0A,
DPT_3_007_INCREASE_25 = 0x0B,
DPT_3_007_INCREASE_12 = 0x0C,
DPT_3_007_INCREASE_6 = 0x0D,
DPT_3_007_INCREASE_3 = 0x0E,
DPT_3_007_INCREASE_1 = 0x0F,
} dpt_3_007_t;
typedef enum __weekday
{
DPT_10_001_WEEKDAY_MONDAY = 1,
DPT_10_001_WEEKDAY_TUESDAY = 2,
DPT_10_001_WEEKDAY_WEDNESDAY = 3,
DPT_10_001_WEEKDAY_THURSDAY = 4,
DPT_10_001_WEEKDAY_FRIDAY = 5,
DPT_10_001_WEEKDAY_SATURDAY = 6,
DPT_10_001_WEEKDAY_SUNDAY = 8,
} weekday_t;
typedef struct __time_of_day
{
weekday_t weekday;
uint8_t hours;
uint8_t minutes;
uint8_t seconds;
} time_of_day_t;
typedef struct __date
{
uint8_t day;
uint8_t month;
uint8_t year;
} date_t;
typedef struct __color
{
uint8_t red;
uint8_t green;
uint8_t blue;
} color_t;

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Nico Weichbrodt <nico@weichbrodt.me>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,107 @@
# ESP-KNX-IP #
This is a library for the ESP8266 to enable KNXnet/IP communication. It uses UDP multicast on 224.0.23.12:3671.
It is intended to be used with the Arduino platform for the ESP8266.
## Prerequisities / Dependencies ##
* You need version 2.4.0 of the esp8266 board libraries.
* I only tested with lwip v1.4. v2 might work, you need to test yourself.
* You need the [ESPAsyncUDP](https://github.com/me-no-dev/ESPAsyncUDP) library.
* You need a KNXnet/IP **router**. A gateway will **not** work. Alternatively use [knxd](https://github.com/knxd/knxd).
## Caveats ##
Receiving packets should work immediately.
Sending sometimes only works after a substantial amount of time (max 5 minutes in my experiments). In my case, this was fixed by disabling IGMP snooping on the switch(es).
## How to use ##
The library is under development. API may change multiple times in the future.
API documentation is available [here](https://github.com/envy/esp-knx-ip/wiki/API)
A simple example:
```c++
#include <esp-knx-ip.h>
const char* ssid = "my-ssid"; // your network SSID (name)
const char* pass = "my-pw"; // your network password
config_id_t my_GA;
config_id_t param_id;
int8_t some_var = 0;
void setup()
{
// Register a callback that is called when a configurable group address is receiving a telegram
knx.register_callback("Set/Get callback", my_callback);
knx.register_callback("Write callback", my_other_callback);
int default_val = 21;
param_id = knx.config_register_int("My Parameter", default_val);
// Register a configurable group address for sending out answers
my_GA = knx.config_register_ga("Answer GA");
knx.load(); // Try to load a config from EEPROM
WiFi.begin(ssid, pass);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
}
knx.start(); // Start everything. Must be called after WiFi connection has been established
}
void loop()
{
knx.loop();
}
void my_callback(message_t const &msg, void *arg)
{
switch (msg.ct)
{
case KNX_CT_WRITE:
// Save received data
some_var = knx.data_to_1byte_int(msg.data);
break;
case KNX_CT_READ:
// Answer with saved data
knx.answer1ByteInt(msg.received_on, some_var);
break;
}
}
void my_other_callback(message_t const &msg, void *arg)
{
switch (msg.ct)
{
case KNX_CT_WRITE:
// Write an answer somewhere else
int value = knx.config_get_int(param_id);
address_t ga = knx.config_get_ga(my_GA);
knx.answer1ByteInt(ga, (int8_t)value);
break;
}
}
```
## How to configure (buildtime) ##
Open the `esp-knx-ip.h` and take a look at the config options at the top inside the block marked `CONFIG`
## How to configure (runtime) ##
Simply visit the IP of your ESP with a webbrowser. You can configure the following:
* KNX physical address
* Which group address should trigger which callback
* Which group address are to be used by the program (e.g. for status replies)
The configuration is dynamically generated from the code.

View File

@ -0,0 +1,358 @@
/**
* esp-knx-ip library for KNX/IP communication on an ESP8266
* Author: Nico Weichbrodt <envy>
* License: MIT
*/
#include "esp-knx-ip.h"
/**
* Physical address functions
*/
void ESPKNXIP::physical_address_set(address_t const &addr)
{
physaddr = addr;
}
address_t ESPKNXIP::physical_address_get()
{
return physaddr;
}
/**
* Configuration functions start here
*/
config_id_t ESPKNXIP::config_register_string(String name, uint8_t len, String _default, enable_condition_t cond)
{
if (registered_configs >= MAX_CONFIGS)
return -1;
if (_default.length() >= len)
return -1;
config_id_t id = registered_configs;
custom_configs[id].name = name;
custom_configs[id].type = CONFIG_TYPE_STRING;
custom_configs[id].len = sizeof(uint8_t) + len;
custom_configs[id].cond = cond;
if (id == 0)
custom_configs[id].offset = 0;
else
custom_configs[id].offset = custom_configs[id - 1].offset + custom_configs[id - 1].len;
__config_set_string(id, _default);
registered_configs++;
DEBUG_PRINT("Registered config >");
DEBUG_PRINT(name);
DEBUG_PRINT("< @ ");
DEBUG_PRINT(id);
DEBUG_PRINT("/string[");
DEBUG_PRINT(custom_configs[id].offset);
DEBUG_PRINT("+");
DEBUG_PRINT(custom_configs[id].len);
DEBUG_PRINTLN("]");
return id;
}
config_id_t ESPKNXIP::config_register_int(String name, int32_t _default, enable_condition_t cond)
{
if (registered_configs >= MAX_CONFIGS)
return -1;
config_id_t id = registered_configs;
custom_configs[id].name = name;
custom_configs[id].type = CONFIG_TYPE_INT;
custom_configs[id].len = sizeof(uint8_t) + sizeof(int32_t);
custom_configs[id].cond = cond;
if (id == 0)
custom_configs[id].offset = 0;
else
custom_configs[id].offset = custom_configs[id - 1].offset + custom_configs[id - 1].len;
__config_set_int(id, _default);
registered_configs++;
DEBUG_PRINT("Registered config >");
DEBUG_PRINT(name);
DEBUG_PRINT("< @ ");
DEBUG_PRINT(id);
DEBUG_PRINT("/int[");
DEBUG_PRINT(custom_configs[id].offset);
DEBUG_PRINT("+");
DEBUG_PRINT(custom_configs[id].len);
DEBUG_PRINTLN("]");
return id;
}
config_id_t ESPKNXIP::config_register_bool(String name, bool _default, enable_condition_t cond)
{
if (registered_configs >= MAX_CONFIGS)
return -1;
config_id_t id = registered_configs;
custom_configs[id].name = name;
custom_configs[id].type = CONFIG_TYPE_BOOL;
custom_configs[id].len = sizeof(uint8_t) + sizeof(uint8_t);
custom_configs[id].cond = cond;
if (id == 0)
custom_configs[id].offset = 0;
else
custom_configs[id].offset = custom_configs[id - 1].offset + custom_configs[id - 1].len;
__config_set_bool(id, _default);
registered_configs++;
DEBUG_PRINT("Registered config >");
DEBUG_PRINT(name);
DEBUG_PRINT("< @ ");
DEBUG_PRINT(id);
DEBUG_PRINT("/bool[");
DEBUG_PRINT(custom_configs[id].offset);
DEBUG_PRINT("+");
DEBUG_PRINT(custom_configs[id].len);
DEBUG_PRINTLN("]");
return id;
}
config_id_t ESPKNXIP::config_register_options(String name, option_entry_t *options, uint8_t _default, enable_condition_t cond)
{
if (registered_configs >= MAX_CONFIGS)
return -1;
if (options == nullptr || options->name == nullptr)
return -1;
config_id_t id = registered_configs;
custom_configs[id].name = name;
custom_configs[id].type = CONFIG_TYPE_OPTIONS;
custom_configs[id].len = sizeof(uint8_t) + sizeof(uint8_t);
custom_configs[id].cond = cond;
if (id == 0)
custom_configs[id].offset = 0;
else
custom_configs[id].offset = custom_configs[id - 1].offset + custom_configs[id - 1].len;
custom_configs[id].data.options = options;
__config_set_options(id, _default);
registered_configs++;
DEBUG_PRINT("Registered config >");
DEBUG_PRINT(name);
DEBUG_PRINT("< @ ");
DEBUG_PRINT(id);
DEBUG_PRINT("/opt[");
DEBUG_PRINT(custom_configs[id].offset);
DEBUG_PRINT("+");
DEBUG_PRINT(custom_configs[id].len);
DEBUG_PRINTLN("]");
return id;
}
config_id_t ESPKNXIP::config_register_ga(String name, enable_condition_t cond)
{
if (registered_configs >= MAX_CONFIGS)
return -1;
config_id_t id = registered_configs;
custom_configs[id].name = name;
custom_configs[id].type = CONFIG_TYPE_GA;
custom_configs[id].len = sizeof(uint8_t) + sizeof(address_t);
custom_configs[id].cond = cond;
if (id == 0)
custom_configs[id].offset = 0;
else
custom_configs[id].offset = custom_configs[id - 1].offset + custom_configs[id - 1].len;
address_t t;
t.value = 0;
__config_set_ga(id, t);
registered_configs++;
DEBUG_PRINT("Registered config >");
DEBUG_PRINT(name);
DEBUG_PRINT("< @ ");
DEBUG_PRINT(id);
DEBUG_PRINT("/ga[");
DEBUG_PRINT(custom_configs[id].offset);
DEBUG_PRINT("+");
DEBUG_PRINT(custom_configs[id].len);
DEBUG_PRINTLN("]");
return id;
}
void ESPKNXIP::__config_set_flags(config_id_t id, config_flags_t flags)
{
DEBUG_PRINT("Setting flag @ ");
DEBUG_PRINT(custom_configs[id].offset);
DEBUG_PRINT(" to ");
DEBUG_PRINT(custom_config_data[custom_configs[id].offset], BIN);
DEBUG_PRINT(" | ");
DEBUG_PRINT(flags, BIN);
custom_config_data[custom_configs[id].offset] |= (uint8_t)flags;
DEBUG_PRINT(" = ");
DEBUG_PRINTLN(custom_config_data[custom_configs[id].offset], BIN);
}
void ESPKNXIP::config_set_string(config_id_t id, String val)
{
if (id >= registered_configs)
return;
if (custom_configs[id].type != CONFIG_TYPE_STRING)
return;
if (val.length() >= custom_configs[id].len)
return;
__config_set_flags(id, CONFIG_FLAGS_VALUE_SET);
__config_set_string(id, val);
}
void ESPKNXIP::__config_set_string(config_id_t id, String &val)
{
memcpy(&custom_config_data[custom_configs[id].offset + sizeof(uint8_t)], val.c_str(), val.length()+1);
}
void ESPKNXIP::config_set_int(config_id_t id, int32_t val)
{
if (id >= registered_configs)
return;
if (custom_configs[id].type != CONFIG_TYPE_INT)
return;
__config_set_flags(id, CONFIG_FLAGS_VALUE_SET);
__config_set_int(id, val);
}
void ESPKNXIP::__config_set_int(config_id_t id, int32_t val)
{
// This does not work for some reason:
// Could be due to pointer alignment
//int32_t *v = (int32_t *)(custom_config_data + custom_configs[id].offset);
//*v = val;
custom_config_data[custom_configs[id].offset + sizeof(uint8_t) + 0] = (uint8_t)((val & 0xFF000000) >> 24);
custom_config_data[custom_configs[id].offset + sizeof(uint8_t) + 1] = (uint8_t)((val & 0x00FF0000) >> 16);
custom_config_data[custom_configs[id].offset + sizeof(uint8_t) + 2] = (uint8_t)((val & 0x0000FF00) >> 8);
custom_config_data[custom_configs[id].offset + sizeof(uint8_t) + 3] = (uint8_t)((val & 0x000000FF) >> 0);
}
void ESPKNXIP::config_set_bool(config_id_t id, bool val)
{
if (id >= registered_configs)
return;
if (custom_configs[id].type != CONFIG_TYPE_BOOL)
return;
__config_set_flags(id, CONFIG_FLAGS_VALUE_SET);
__config_set_bool(id, val);
}
void ESPKNXIP::__config_set_bool(config_id_t id, bool val)
{
custom_config_data[custom_configs[id].offset + sizeof(uint8_t)] = val ? 1 : 0;
}
void ESPKNXIP::config_set_options(config_id_t id, uint8_t val)
{
if (id >= registered_configs)
return;
if (custom_configs[id].type != CONFIG_TYPE_OPTIONS)
return;
option_entry_t *cur = custom_configs[id].data.options;
while (cur->name != nullptr)
{
if (cur->value == val)
{
__config_set_flags(id, CONFIG_FLAGS_VALUE_SET);
__config_set_options(id, val);
break;
}
cur++;
}
}
void ESPKNXIP::__config_set_options(config_id_t id, uint8_t val)
{
custom_config_data[custom_configs[id].offset + sizeof(uint8_t)] = val;
}
void ESPKNXIP::config_set_ga(config_id_t id, address_t const &val)
{
if (id >= registered_configs)
return;
if (custom_configs[id].type != CONFIG_TYPE_GA)
return;
__config_set_flags(id, CONFIG_FLAGS_VALUE_SET);
__config_set_ga(id, val);
}
void ESPKNXIP::__config_set_ga(config_id_t id, address_t const &val)
{
custom_config_data[custom_configs[id].offset + sizeof(uint8_t) + 0] = val.bytes.high;
custom_config_data[custom_configs[id].offset + sizeof(uint8_t) + 1] = val.bytes.low;
}
String ESPKNXIP::config_get_string(config_id_t id)
{
if (id >= registered_configs)
return String("");
return String((char *)&custom_config_data[custom_configs[id].offset + sizeof(uint8_t)]);
}
int32_t ESPKNXIP::config_get_int(config_id_t id)
{
if (id >= registered_configs)
return 0;
int32_t v = (custom_config_data[custom_configs[id].offset + sizeof(uint8_t) + 0] << 24) +
(custom_config_data[custom_configs[id].offset + sizeof(uint8_t) + 1] << 16) +
(custom_config_data[custom_configs[id].offset + sizeof(uint8_t) + 2] << 8) +
(custom_config_data[custom_configs[id].offset + sizeof(uint8_t) + 3] << 0);
return v;
}
bool ESPKNXIP::config_get_bool(config_id_t id)
{
if (id >= registered_configs)
return false;
return custom_config_data[custom_configs[id].offset + sizeof(uint8_t)] != 0;
}
uint8_t ESPKNXIP::config_get_options(config_id_t id)
{
if (id >= registered_configs)
return false;
return custom_config_data[custom_configs[id].offset + sizeof(uint8_t)];
}
address_t ESPKNXIP::config_get_ga(config_id_t id)
{
address_t t;
if (id >= registered_configs)
{
t.value = 0;
return t;
}
t.bytes.high = custom_config_data[custom_configs[id].offset + sizeof(uint8_t) + 0];
t.bytes.low = custom_config_data[custom_configs[id].offset + sizeof(uint8_t) + 1];
return t;
}

View File

@ -0,0 +1,87 @@
/**
* esp-knx-ip library for KNX/IP communication on an ESP8266
* Author: Nico Weichbrodt <envy>
* License: MIT
*/
#include "esp-knx-ip.h"
/**
* Conversion functions
*/
bool ESPKNXIP::data_to_bool(uint8_t *data)
{
return (data[0] & 0x01) == 1 ? true : false;
}
int8_t ESPKNXIP::data_to_1byte_int(uint8_t *data)
{
return (int8_t)data[1];
}
uint8_t ESPKNXIP::data_to_1byte_uint(uint8_t *data)
{
return data[1];
}
int16_t ESPKNXIP::data_to_2byte_int(uint8_t *data)
{
return (int16_t)((data[1] << 8) | data[2]);
}
uint16_t ESPKNXIP::data_to_2byte_uint(uint8_t *data)
{
return (uint16_t)((data[1] << 8) | data[2]);
}
float ESPKNXIP::data_to_2byte_float(uint8_t *data)
{
//uint8_t sign = (data[1] & 0b10000000) >> 7;
uint8_t expo = (data[1] & 0b01111000) >> 3;
int16_t mant = ((data[1] & 0b10000111) << 8) | data[2];
return 0.01f * mant * pow(2, expo);
}
time_of_day_t ESPKNXIP::data_to_3byte_time(uint8_t *data)
{
time_of_day_t time;
time.weekday = (weekday_t)((data[1] & 0b11100000) >> 5);
time.hours = (data[1] & 0b00011111);
time.minutes = (data[2] & 0b00111111);
time.seconds = (data[3] & 0b00111111);
return time;
}
date_t ESPKNXIP::data_to_3byte_data(uint8_t *data)
{
date_t date;
date.day = (data[1] & 0b00011111);
date.month = (data[2] & 0b00001111);
date.year = (data[3] & 0b01111111);
return date;
}
color_t ESPKNXIP::data_to_3byte_color(uint8_t *data)
{
color_t color;
color.red = data[1];
color.green = data[2];
color.blue = data[3];
return color;
}
int32_t ESPKNXIP::data_to_4byte_int(uint8_t *data)
{
return (int32_t)((data[1] << 24) | (data[2] << 16) | (data[3] << 8) | (data[4] << 0));
}
uint32_t ESPKNXIP::data_to_4byte_uint(uint8_t *data)
{
return (uint32_t)((data[1] << 24) | (data[2] << 16) | (data[3] << 8) | (data[4] << 0));
}
float ESPKNXIP::data_to_4byte_float(uint8_t *data)
{
return (float)((data[1] << 24) | (data[2] << 16) | (data[3] << 8) |data[4]);
}

View File

@ -0,0 +1,180 @@
/**
* esp-knx-ip library for KNX/IP communication on an ESP8266
* Author: Nico Weichbrodt <envy>
* License: MIT
*/
#include "esp-knx-ip.h"
/**
* Send functions
*/
void ESPKNXIP::send(address_t const &receiver, knx_command_type_t ct, uint8_t data_len, uint8_t *data)
{
if (receiver.value == 0)
return;
#if SEND_CHECKSUM
uint32_t len = 6 + 2 + 8 + data_len + 1; // knx_pkt + cemi_msg + cemi_service + data + checksum
#else
uint32_t len = 6 + 2 + 8 + data_len; // knx_pkt + cemi_msg + cemi_service + data
#endif
DEBUG_PRINT(F("Creating packet with len "));
DEBUG_PRINTLN(len)
uint8_t buf[len];
knx_ip_pkt_t *knx_pkt = (knx_ip_pkt_t *)buf;
knx_pkt->header_len = 0x06;
knx_pkt->protocol_version = 0x10;
knx_pkt->service_type = __ntohs(KNX_ST_ROUTING_INDICATION);
knx_pkt->total_len.len = __ntohs(len);
cemi_msg_t *cemi_msg = (cemi_msg_t *)knx_pkt->pkt_data;
cemi_msg->message_code = KNX_MT_L_DATA_IND;
cemi_msg->additional_info_len = 0;
cemi_service_t *cemi_data = &cemi_msg->data.service_information;
cemi_data->control_1.bits.confirm = 0;
cemi_data->control_1.bits.ack = 0;
cemi_data->control_1.bits.priority = B11;
cemi_data->control_1.bits.system_broadcast = 0x01;
cemi_data->control_1.bits.repeat = 0x01;
cemi_data->control_1.bits.reserved = 0;
cemi_data->control_1.bits.frame_type = 0x01;
cemi_data->control_2.bits.extended_frame_format = 0x00;
cemi_data->control_2.bits.hop_count = 0x06;
cemi_data->control_2.bits.dest_addr_type = 0x01;
cemi_data->source = physaddr;
cemi_data->destination = receiver;
//cemi_data->destination.bytes.high = (area << 3) | line;
//cemi_data->destination.bytes.low = member;
cemi_data->data_len = data_len;
cemi_data->pci.apci = (ct & 0x0C) >> 2;
cemi_data->pci.tpci_seq_number = 0x00; // ???
cemi_data->pci.tpci_comm_type = KNX_COT_UDP; // ???
memcpy(cemi_data->data, data, data_len);
cemi_data->data[0] = (cemi_data->data[0] & 0x3F) | ((ct & 0x03) << 6);
#if SEND_CHECKSUM
// Calculate checksum, which is just XOR of all bytes
uint8_t cs = buf[0] ^ buf[1];
for (uint32_t i = 2; i < len - 1; ++i)
{
cs ^= buf[i];
}
buf[len - 1] = cs;
#endif
#ifdef ESP_KNX_DEBUG
DEBUG_PRINT(F("Sending packet:"));
for (int i = 0; i < len; ++i)
{
DEBUG_PRINT(F(" 0x"));
DEBUG_PRINT(buf[i], 16);
}
DEBUG_PRINTLN(F(""));
#endif
udp.writeTo(buf, len, MULTICAST_IP, MULTICAST_PORT);
}
void ESPKNXIP::send_1bit(address_t const &receiver, knx_command_type_t ct, uint8_t bit)
{
uint8_t buf[] = {(uint8_t)(bit & 0b00000001)};
send(receiver, ct, 1, buf);
}
void ESPKNXIP::send_2bit(address_t const &receiver, knx_command_type_t ct, uint8_t twobit)
{
uint8_t buf[] = {(uint8_t)(twobit & 0b00000011)};
send(receiver, ct, 1, buf);
}
void ESPKNXIP::send_4bit(address_t const &receiver, knx_command_type_t ct, uint8_t fourbit)
{
uint8_t buf[] = {(uint8_t)(fourbit & 0b00001111)};
send(receiver, ct, 1, buf);
}
void ESPKNXIP::send_1byte_int(address_t const &receiver, knx_command_type_t ct, int8_t val)
{
uint8_t buf[] = {0x00, (uint8_t)val};
send(receiver, ct, 2, buf);
}
void ESPKNXIP::send_1byte_uint(address_t const &receiver, knx_command_type_t ct, uint8_t val)
{
uint8_t buf[] = {0x00, val};
send(receiver, ct, 2, buf);
}
void ESPKNXIP::send_2byte_int(address_t const &receiver, knx_command_type_t ct, int16_t val)
{
uint8_t buf[] = {0x00, (uint8_t)(val >> 8), (uint8_t)(val & 0x00FF)};
send(receiver, ct, 3, buf);
}
void ESPKNXIP::send_2byte_uint(address_t const &receiver, knx_command_type_t ct, uint16_t val)
{
uint8_t buf[] = {0x00, (uint8_t)(val >> 8), (uint8_t)(val & 0x00FF)};
send(receiver, ct, 3, buf);
}
void ESPKNXIP::send_2byte_float(address_t const &receiver, knx_command_type_t ct, float val)
{
float v = val * 100.0f;
int e = 0;
for (; v < -2048.0f; v /= 2)
++e;
for (; v > 2047.0f; v /= 2)
++e;
long m = round(v) & 0x7FF;
short msb = (short) (e << 3 | m >> 8);
if (val < 0.0f)
msb |= 0x80;
uint8_t buf[] = {0x00, (uint8_t)msb, (uint8_t)m};
send(receiver, ct, 3, buf);
}
void ESPKNXIP::send_3byte_time(address_t const &receiver, knx_command_type_t ct, uint8_t weekday, uint8_t hours, uint8_t minutes, uint8_t seconds)
{
weekday <<= 5;
uint8_t buf[] = {0x00, (uint8_t)(((weekday << 5) & 0xE0) | (hours & 0x1F)), (uint8_t)(minutes & 0x3F), (uint8_t)(seconds & 0x3F)};
send(receiver, ct, 4, buf);
}
void ESPKNXIP::send_3byte_date(address_t const &receiver, knx_command_type_t ct, uint8_t day, uint8_t month, uint8_t year)
{
uint8_t buf[] = {0x00, (uint8_t)(day & 0x1F), (uint8_t)(month & 0x0F), year};
send(receiver, ct, 4, buf);
}
void ESPKNXIP::send_3byte_color(address_t const &receiver, knx_command_type_t ct, uint8_t red, uint8_t green, uint8_t blue)
{
uint8_t buf[] = {0x00, red, green, blue};
send(receiver, ct, 4, buf);
}
void ESPKNXIP::send_4byte_int(address_t const &receiver, knx_command_type_t ct, int32_t val)
{
uint8_t buf[] = {0x00,
(uint8_t)((val & 0xFF000000) >> 24),
(uint8_t)((val & 0x00FF0000) >> 16),
(uint8_t)((val & 0x0000FF00) >> 8),
(uint8_t)((val & 0x000000FF) >> 0)};
send(receiver, ct, 5, buf);
}
void ESPKNXIP::send_4byte_uint(address_t const &receiver, knx_command_type_t ct, uint32_t val)
{
uint8_t buf[] = {0x00,
(uint8_t)((val & 0xFF000000) >> 24),
(uint8_t)((val & 0x00FF0000) >> 16),
(uint8_t)((val & 0x0000FF00) >> 8),
(uint8_t)((val & 0x000000FF) >> 0)};
send(receiver, ct, 5, buf);
}
void ESPKNXIP::send_4byte_float(address_t const &receiver, knx_command_type_t ct, float val)
{
uint8_t buf[] = {0x00, ((uint8_t *)&val)[3], ((uint8_t *)&val)[2], ((uint8_t *)&val)[1], ((uint8_t *)&val)[0]};
send(receiver, ct, 5, buf);
}

View File

@ -0,0 +1,540 @@
/**
* esp-knx-ip library for KNX/IP communication on an ESP8266
* Author: Nico Weichbrodt <envy>
* License: MIT
*/
#include "esp-knx-ip.h"
void ESPKNXIP::__handle_root()
{
String m = F("<html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width, initial-scale=1, shrink-to-fit=no'>");
#if USE_BOOTSTRAP
m += F("<link rel='stylesheet' href='https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css' integrity='sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm' crossorigin='anonymous'>");
m += F("<style>.input-group-insert > .input-group-text { border-radius: 0; }</style>");
#endif
m += F("</head><body><div class='container-fluid'>");
m += F("<h2>ESP KNX</h2>");
// Feedback
if (registered_feedbacks > 0)
{
m += F("<h4>Feedback</h4>");
for (feedback_id_t i = 0; i < registered_feedbacks; ++i)
{
if (feedbacks[i].cond && !feedbacks[i].cond())
{
continue;
}
m += F("<form action='" __FEEDBACK_PATH "' method='POST'>");
m += F("<div class='row'><div class='col-auto'><div class='input-group'>");
m += F("<div class='input-group-prepend'><span class='input-group-text'>");
m += feedbacks[i].name;
m += F("</span></div>");
switch (feedbacks[i].type)
{
case FEEDBACK_TYPE_INT:
m += F("<span class='input-group-text'>");
m += String(*(int32_t *)feedbacks[i].data);
m += F("</span>");
break;
case FEEDBACK_TYPE_FLOAT:
m += F("<span class='input-group-text'>");
m += feedbacks[i].options.float_options.prefix;
m += String(*(float *)feedbacks[i].data, feedbacks[i].options.float_options.precision);
m += feedbacks[i].options.float_options.suffix;
m += F("</span>");
break;
case FEEDBACK_TYPE_BOOL:
m += F("<span class='input-group-text'>");
m += (*(bool *)feedbacks[i].data) ? F("True") : F("False");
m += F("</span>");
break;
case FEEDBACK_TYPE_ACTION:
m += F("<input class='form-control' type='hidden' name='id' value='");
m += i;
m += F("' /><div class='input-group-append'><button type='submit' class='btn btn-primary'>");
m += feedbacks[i].options.action_options.btn_text;
m += F("</button></div>");
break;
}
m += F("</div></div></div>");
m += F("</form>");
}
}
if (registered_callbacks > 0)
m += F("<h4>Callbacks</h4>");
if (registered_callback_assignments > 0)
{
for (uint8_t i = 0; i < registered_callback_assignments; ++i)
{
// Skip empty slots
if ((callback_assignments[i].slot_flags & SLOT_FLAGS_USED) == 0)
{
continue;
}
// Skip disabled callbacks
if (callbacks[callback_assignments[i].callback_id].cond && !callbacks[callback_assignments[i].callback_id].cond())
{
continue;
}
address_t &addr = callback_assignments[i].address;
m += F("<form action='" __DELETE_PATH "' method='POST'>");
m += F("<div class='row'><div class='col-auto'><div class='input-group'>");
m += F("<div class='input-group-prepend'><span class='input-group-text'>");
m += addr.ga.area;
m += F("/");
m += addr.ga.line;
m += F("/");
m += addr.ga.member;
m += F("</span>");
m += F("<span class='input-group-text'>");
m += callbacks[callback_assignments[i].callback_id].name;
m += F("</span></div>");
m += F("<input class='form-control' type='hidden' name='id' value='");
m += i;
m += F("' /><div class='input-group-append'><button type='submit' class='btn btn-danger'>Delete</button></div>");
m += F("</div></div></div>");
m += F("</form>");
}
}
if (registered_callbacks > 0)
{
m += F("<form action='" __REGISTER_PATH "' method='POST'>");
m += F("<div class='row'><div class='col-auto'><div class='input-group'>");
m += F("<input class='form-control' type='number' name='area' min='0' max='31'/>");
m += F("<div class='input-group-insert'><span class='input-group-text'>/</span></div>");
m += F("<input class='form-control' type='number' name='line' min='0' max='7'/>");
m += F("<div class='input-group-insert'><span class='input-group-text'>/</span></div>");
m += F("<input class='form-control' type='number' name='member' min='0' max='255'/>");
m += F("<div class='input-group-insert'><span class='input-group-text'>-&gt;</span></div>");
m += F("<select class='form-control' name='cb'>");
for (callback_id_t i = 0; i < registered_callbacks; ++i)
{
// Skip empty slots
if ((callbacks[i].slot_flags & SLOT_FLAGS_USED) == 0)
{
continue;
}
// Skip disabled callbacks
if (callbacks[i].cond && !callbacks[i].cond())
{
continue;
}
m += F("<option value=\"");
m += i;
m += F("\">");
m += callbacks[i].name;
m += F("</option>");
}
m += F("</select>");
m += F("<div class='input-group-append'><button type='submit' class='btn btn-primary'>Set</button></div>");
m += F("</div></div></div>");
m += F("</form>");
}
m += F("<h4>Configuration</h4>");
// Physical address
m += F("<form action='" __PHYS_PATH "' method='POST'>");
m += F("<div class='row'><div class='col-auto'><div class='input-group'>");
m += F("<div class='input-group-prepend'><span class='input-group-text'>Physical address</span></div>");
m += F("<input class='form-control' type='number' name='area' min='0' max='15' value='");
m += physaddr.pa.area;
m += F("'/>");
m += F("<div class='input-group-insert'><span class='input-group-text'>.</span></div>");
m += F("<input class='form-control' type='number' name='line' min='0' max='15' value='");
m += physaddr.pa.line;
m += F("'/>");
m += F("<div class='input-group-insert'><span class='input-group-text'>.</span></div>");
m += F("<input class='form-control' type='number' name='member' min='0' max='255' value='");
m += physaddr.pa.member;
m += F("'/>");
m += F("<div class='input-group-append'><button type='submit' class='btn btn-primary'>Set</button></div>");
m += F("</div></div></div>");
m += F("</form>");
if (registered_configs > 0)
{
for (config_id_t i = 0; i < registered_configs; ++i)
{
// Check if this config option has a enable condition and if so check that condition
if (custom_configs[i].cond && !custom_configs[i].cond())
continue;
m += F("<form action='" __CONFIG_PATH "' method='POST'>");
m += F("<div class='row'><div class='col-auto'><div class='input-group'>");
m += F("<div class='input-group-prepend'><span class='input-group-text'>");
m += custom_configs[i].name;
m += F("</span></div>");
switch (custom_configs[i].type)
{
case CONFIG_TYPE_STRING:
m += F("<input class='form-control' type='text' name='value' value='");
m += config_get_string(i);
m += F("' maxlength='");
m += custom_configs[i].len - 1; // Subtract \0 byte
m += F("'/>");
break;
case CONFIG_TYPE_INT:
m += F("<input class='form-control' type='number' name='value' value='");
m += config_get_int(i);
m += F("'/>");
break;
case CONFIG_TYPE_BOOL:
m += F("<div class='input-group-insert'><span class='input-group-text'>");
m += F("<input type='checkbox' name='value' ");
if (config_get_bool(i))
m += F("checked ");
m += F("/>");
m += F("</span></div>");
break;
case CONFIG_TYPE_OPTIONS:
{
m += F("<select class='custom-select' name='value'>");
option_entry_t *cur = custom_configs[i].data.options;
while (cur->name != nullptr)
{
if (config_get_options(i) == cur->value)
{
m += F("<option selected value='");
}
else
{
m += F("<option value='");
}
m += cur->value;
m += F("'>");
m += String(cur->name);
m += F("</option>");
cur++;
}
m += F("");
m += F("</select>");
break;
}
case CONFIG_TYPE_GA:
address_t a = config_get_ga(i);
m += F("<input class='form-control' type='number' name='area' min='0' max='31' value='");
m += a.ga.area;
m += F("'/>");
m += F("<div class='input-group-insert'><span class='input-group-text'>/</span></div>");
m += F("<input class='form-control' type='number' name='line' min='0' max='7' value='");
m += a.ga.line;
m += F("'/>");
m += F("<div class='input-group-insert'><span class='input-group-text'>/</span></div>");
m += F("<input class='form-control' type='number' name='member' min='0' max='255' value='");
m += a.ga.member;
m += F("'/>");
break;
}
m += F("<input type='hidden' name='id' value='");
m += i;
m += F("'/>");
m += F("<div class='input-group-append'><button type='submit' class='btn btn-primary'>Set</button></div>");
m += F("</div></div></div>");
m += F("</form>");
}
}
#if !(DISABLE_EEPROM_BUTTONS && DISABLE_RESTORE_BUTTON && DISABLE_REBOOT_BUTTON)
// EEPROM save and restore
m += F("<div class='row'>");
// Save to EEPROM
#if !DISABLE_EEPROM_BUTTONS
m += F("<div class='col-auto'>");
m += F("<form action='" __EEPROM_PATH "' method='POST'>");
m += F("<input type='hidden' name='mode' value='1'>");
m += F("<button type='submit' class='btn btn-success'>Save to EEPROM</button>");
m += F("</form>");
m += F("</div>");
// Restore from EEPROM
m += F("<div class='col-auto'>");
m += F("<form action='" __EEPROM_PATH "' method='POST'>");
m += F("<input type='hidden' name='mode' value='2'>");
m += F("<button type='submit' class='btn btn-info'>Restore from EEPROM</button>");
m += F("</form>");
m += F("</div>");
#endif
#if !DISABLE_RESTORE_BUTTON
// Load Defaults
m += F("<div class='col-auto'>");
m += F("<form action='" __RESTORE_PATH "' method='POST'>");
m += F("<button type='submit' class='btn btn-warning'>Restore defaults</button>");
m += F("</form>");
m += F("</div>");
#endif
#if !DISABLE_REBOOT_BUTTON
// Reboot
m += F("<div class='col-auto'>");
m += F("<form action='" __REBOOT_PATH "' method='POST'>");
m += F("<button type='submit' class='btn btn-danger'>Reboot</button>");
m += F("</form>");
m += F("</div>");
#endif
m += F("</div>"); // row
#endif
// End of page
m += F("</div></body></html>");
server->send(200, F("text/html"), m);
}
void ESPKNXIP::__handle_register()
{
DEBUG_PRINTLN(F("Register called"));
if (server->hasArg(F("area")) && server->hasArg(F("line")) && server->hasArg(F("member")) && server->hasArg(F("cb")))
{
uint8_t area = server->arg(F("area")).toInt();
uint8_t line = server->arg(F("line")).toInt();
uint8_t member = server->arg(F("member")).toInt();
callback_id_t cb = (callback_id_t)server->arg(F("cb")).toInt();
DEBUG_PRINT(F("Got args: "));
DEBUG_PRINT(area);
DEBUG_PRINT(F("/"));
DEBUG_PRINT(line);
DEBUG_PRINT(F("/"));
DEBUG_PRINT(member);
DEBUG_PRINT(F("/"));
DEBUG_PRINT(cb);
DEBUG_PRINTLN(F(""));
if (area > 31 || line > 7)
{
DEBUG_PRINTLN(F("Area or Line wrong"));
goto end;
}
if (!__callback_is_id_valid(cb))
{
DEBUG_PRINTLN(F("Invalid callback id"));
goto end;
}
address_t ga = {.ga={line, area, member}};
__callback_register_assignment(ga, cb);
}
end:
server->sendHeader(F("Location"),F(__ROOT_PATH));
server->send(302);
}
void ESPKNXIP::__handle_delete()
{
DEBUG_PRINTLN(F("Delete called"));
if (server->hasArg(F("id")))
{
callback_assignment_id_t id = (callback_assignment_id_t)server->arg(F("id")).toInt();
DEBUG_PRINT(F("Got args: "));
DEBUG_PRINT(id);
DEBUG_PRINTLN(F(""));
if (id >= registered_callback_assignments || (callback_assignments[id].slot_flags & SLOT_FLAGS_USED) == 0)
{
DEBUG_PRINTLN(F("ID wrong"));
goto end;
}
__callback_delete_assignment(id);
}
end:
server->sendHeader(F("Location"),F(__ROOT_PATH));
server->send(302);
}
void ESPKNXIP::__handle_set()
{
DEBUG_PRINTLN(F("Set called"));
if (server->hasArg(F("area")) && server->hasArg(F("line")) && server->hasArg(F("member")))
{
uint8_t area = server->arg(F("area")).toInt();
uint8_t line = server->arg(F("line")).toInt();
uint8_t member = server->arg(F("member")).toInt();
DEBUG_PRINT(F("Got args: "));
DEBUG_PRINT(area);
DEBUG_PRINT(F("."));
DEBUG_PRINT(line);
DEBUG_PRINT(F("."));
DEBUG_PRINT(member);
DEBUG_PRINTLN(F(""));
if (area > 31 || line > 7)
{
DEBUG_PRINTLN(F("Area or Line wrong"));
goto end;
}
physaddr.bytes.high = (area << 4) | line;
physaddr.bytes.low = member;
}
end:
server->sendHeader(F("Location"),F(__ROOT_PATH));
server->send(302);
}
void ESPKNXIP::__handle_config()
{
DEBUG_PRINTLN(F("Config called"));
if (server->hasArg(F("id")))
{
config_id_t id = server->arg(F("id")).toInt();
DEBUG_PRINT(F("Got args: "));
DEBUG_PRINT(id);
DEBUG_PRINTLN(F(""));
if (id < 0 || id >= registered_configs)
{
DEBUG_PRINTLN(F("ID wrong"));
goto end;
}
switch (custom_configs[id].type)
{
case CONFIG_TYPE_STRING:
{
String v = server->arg(F("value"));
if (v.length() >= custom_configs[id].len)
goto end;
__config_set_flags(id, CONFIG_FLAGS_VALUE_SET);
__config_set_string(id, v);
break;
}
case CONFIG_TYPE_INT:
{
__config_set_flags(id, CONFIG_FLAGS_VALUE_SET);
__config_set_int(id, server->arg(F("value")).toInt());
break;
}
case CONFIG_TYPE_BOOL:
{
__config_set_flags(id, CONFIG_FLAGS_VALUE_SET);
__config_set_bool(id, server->arg(F("value")).compareTo(F("on")) == 0);
break;
}
case CONFIG_TYPE_OPTIONS:
{
uint8_t val = (uint8_t)server->arg(F("value")).toInt();
DEBUG_PRINT(F("Value: "));
DEBUG_PRINTLN(val);
config_set_options(id, val);
break;
}
case CONFIG_TYPE_GA:
{
uint8_t area = server->arg(F("area")).toInt();
uint8_t line = server->arg(F("line")).toInt();
uint8_t member = server->arg(F("member")).toInt();
if (area > 31 || line > 7)
{
DEBUG_PRINTLN(F("Area or Line wrong"));
goto end;
}
address_t tmp;
tmp.bytes.high = (area << 3) | line;
tmp.bytes.low = member;
__config_set_flags(id, CONFIG_FLAGS_VALUE_SET);
__config_set_ga(id, tmp);
break;
}
}
}
end:
server->sendHeader(F("Location"),F(__ROOT_PATH));
server->send(302);
}
void ESPKNXIP::__handle_feedback()
{
DEBUG_PRINTLN(F("Feedback called"));
if (server->hasArg(F("id")))
{
config_id_t id = server->arg(F("id")).toInt();
DEBUG_PRINT(F("Got args: "));
DEBUG_PRINT(id);
DEBUG_PRINTLN(F(""));
if (id < 0 || id >= registered_feedbacks)
{
DEBUG_PRINTLN(F("ID wrong"));
goto end;
}
switch (feedbacks[id].type)
{
case FEEDBACK_TYPE_ACTION:
{
feedback_action_fptr_t func = (feedback_action_fptr_t)feedbacks[id].data;
void *arg = feedbacks[id].options.action_options.arg;
func(arg);
break;
}
default:
DEBUG_PRINTLN(F("Feedback has no action"));
break;
}
}
end:
server->sendHeader(F("Location"),F(__ROOT_PATH));
server->send(302);
}
#if !DISABLE_RESTORE_BUTTONS
void ESPKNXIP::__handle_restore()
{
DEBUG_PRINTLN(F("Restore called"));
memcpy(custom_config_data, custom_config_default_data, MAX_CONFIG_SPACE);
end:
server->sendHeader(F("Location"),F(__ROOT_PATH));
server->send(302);
}
#endif
#if !DISABLE_REBOOT_BUTTONS
void ESPKNXIP::__handle_reboot()
{
DEBUG_PRINTLN(F("Rebooting!"));
server->sendHeader(F("Location"),F(__ROOT_PATH));
server->send(302);
delay(1000);
ESP.restart();
//while(1);
}
#endif
#if !DISABLE_EEPROM_BUTTONS
void ESPKNXIP::__handle_eeprom()
{
DEBUG_PRINTLN(F("EEPROM called"));
if (server->hasArg(F("mode")))
{
uint8_t mode = server->arg(F("mode")).toInt();
DEBUG_PRINT(F("Got args: "));
DEBUG_PRINT(mode);
DEBUG_PRINTLN(F(""));
if (mode == 1)
{
// save
save_to_eeprom();
}
else if (mode == 2)
{
// restore
restore_from_eeprom();
}
}
end:
server->sendHeader(F("Location"),F(__ROOT_PATH));
server->send(302);
}
#endif

View File

@ -0,0 +1,658 @@
/**
* esp-knx-ip library for KNX/IP communication on an ESP8266
* Author: Nico Weichbrodt <envy>
* License: MIT
*/
#include "esp-knx-ip.h"
char const *string_defaults[] =
{
"Do this",
"True",
"False",
""
};
ESPKNXIP::ESPKNXIP() : server(nullptr),
registered_callback_assignments(0),
free_callback_assignment_slots(0),
registered_callbacks(0),
free_callback_slots(0),
registered_configs(0),
registered_feedbacks(0)
{
DEBUG_PRINTLN();
DEBUG_PRINTLN("ESPKNXIP starting up");
// Default physical address is 1.1.0
physaddr.bytes.high = (/*area*/1 << 4) | /*line*/1;
physaddr.bytes.low = /*member*/0;
memset(callback_assignments, 0, MAX_CALLBACK_ASSIGNMENTS * sizeof(callback_assignment_t));
memset(callbacks, 0, MAX_CALLBACKS * sizeof(callback_fptr_t));
memset(custom_config_data, 0, MAX_CONFIG_SPACE * sizeof(uint8_t));
memset(custom_config_default_data, 0, MAX_CONFIG_SPACE * sizeof(uint8_t));
memset(custom_configs, 0, MAX_CONFIGS * sizeof(config_t));
}
void ESPKNXIP::load()
{
memcpy(custom_config_default_data, custom_config_data, MAX_CONFIG_SPACE);
EEPROM.begin(EEPROM_SIZE);
restore_from_eeprom();
}
void ESPKNXIP::start(ESP8266WebServer *srv)
{
server = srv;
__start();
}
void ESPKNXIP::start()
{
server = new ESP8266WebServer(80);
__start();
}
void ESPKNXIP::__start()
{
if (server != nullptr)
{
server->on(ROOT_PREFIX, [this](){
__handle_root();
});
server->on(__ROOT_PATH, [this](){
__handle_root();
});
server->on(__REGISTER_PATH, [this](){
__handle_register();
});
server->on(__DELETE_PATH, [this](){
__handle_delete();
});
server->on(__PHYS_PATH, [this](){
__handle_set();
});
#if !DISABLE_EEPROM_BUTTONS
server->on(__EEPROM_PATH, [this](){
__handle_eeprom();
});
#endif
server->on(__CONFIG_PATH, [this](){
__handle_config();
});
server->on(__FEEDBACK_PATH, [this](){
__handle_feedback();
});
#if !DISABLE_RESTORE_BUTTON
server->on(__RESTORE_PATH, [this](){
__handle_restore();
});
#endif
#if !DISABLE_REBOOT_BUTTON
server->on(__REBOOT_PATH, [this](){
__handle_reboot();
});
#endif
server->begin();
}
udp.listenMulticast(MULTICAST_IP, MULTICAST_PORT);
udp.onPacket([this](AsyncUDPPacket &packet) {
DEBUG_PRINTLN("got packet");
__loop_knx(packet); });
}
void ESPKNXIP::save_to_eeprom()
{
uint32_t address = 0;
uint64_t magic = EEPROM_MAGIC;
EEPROM.put(address, magic);
address += sizeof(uint64_t);
EEPROM.put(address++, registered_callback_assignments);
for (uint8_t i = 0; i < MAX_CALLBACK_ASSIGNMENTS; ++i)
{
EEPROM.put(address, callback_assignments[i].address);
address += sizeof(address_t);
}
for (uint8_t i = 0; i < MAX_CALLBACK_ASSIGNMENTS; ++i)
{
EEPROM.put(address, callback_assignments[i].callback_id);
address += sizeof(callback_id_t);
}
EEPROM.put(address, physaddr);
address += sizeof(address_t);
EEPROM.put(address, custom_config_data);
address += sizeof(custom_config_data);
EEPROM.commit();
DEBUG_PRINT("Wrote to EEPROM: 0x");
DEBUG_PRINTLN(address, HEX);
}
void ESPKNXIP::restore_from_eeprom()
{
uint32_t address = 0;
uint64_t magic = 0;
EEPROM.get(address, magic);
if (magic != EEPROM_MAGIC)
{
DEBUG_PRINTLN("No valid magic in EEPROM, aborting restore.");
DEBUG_PRINT("Expected 0x");
DEBUG_PRINT((unsigned long)(EEPROM_MAGIC >> 32), HEX);
DEBUG_PRINT(" 0x");
DEBUG_PRINT((unsigned long)(EEPROM_MAGIC), HEX);
DEBUG_PRINT(" got 0x");
DEBUG_PRINT((unsigned long)(magic >> 32), HEX);
DEBUG_PRINT(" 0x");
DEBUG_PRINTLN((unsigned long)magic, HEX);
return;
}
address += sizeof(uint64_t);
EEPROM.get(address++, registered_callback_assignments);
for (uint8_t i = 0; i < MAX_CALLBACK_ASSIGNMENTS; ++i)
{
EEPROM.get(address, callback_assignments[i].address);
if (callback_assignments[i].address.value != 0)
{
// if address is not 0/0/0 then mark slot as used
callback_assignments[i].slot_flags |= SLOT_FLAGS_USED;
DEBUG_PRINTLN("used slot");
}
else
{
// if address is 0/0/0, then we found a free slot, yay!
// however, only count those slots, if we have not reached registered_callback_assignments yet
if (i < registered_callback_assignments)
{
DEBUG_PRINTLN("free slot before reaching registered_callback_assignments");
free_callback_assignment_slots++;
}
else
{
DEBUG_PRINTLN("free slot");
}
}
address += sizeof(address_t);
}
for (uint8_t i = 0; i < MAX_CALLBACK_ASSIGNMENTS; ++i)
{
EEPROM.get(address, callback_assignments[i].callback_id);
address += sizeof(callback_id_t);
}
EEPROM.get(address, physaddr);
address += sizeof(address_t);
//EEPROM.get(address, custom_config_data);
//address += sizeof(custom_config_data);
uint32_t conf_offset = address;
for (uint8_t i = 0; i < registered_configs; ++i)
{
// First byte is flags.
config_flags_t flags = CONFIG_FLAGS_NO_FLAGS;
flags = (config_flags_t)EEPROM.read(address);
DEBUG_PRINT("Flag in EEPROM @ ");
DEBUG_PRINT(address - conf_offset);
DEBUG_PRINT(": ");
DEBUG_PRINTLN(flags, BIN);
custom_config_data[custom_configs[i].offset] = flags;
if (flags & CONFIG_FLAGS_VALUE_SET)
{
DEBUG_PRINTLN("Non-default value");
for (int j = 0; j < custom_configs[i].len - sizeof(uint8_t); ++j)
{
custom_config_data[custom_configs[i].offset + sizeof(uint8_t) + j] = EEPROM.read(address + sizeof(uint8_t) + j);
}
}
address += custom_configs[i].len;
}
DEBUG_PRINT("Restored from EEPROM: 0x");
DEBUG_PRINTLN(address, HEX);
}
uint16_t ESPKNXIP::__ntohs(uint16_t n)
{
return (uint16_t)((((uint8_t*)&n)[0] << 8) | (((uint8_t*)&n)[1]));
}
callback_assignment_id_t ESPKNXIP::__callback_register_assignment(address_t address, callback_id_t id)
{
if (registered_callback_assignments >= MAX_CALLBACK_ASSIGNMENTS)
return -1;
if (free_callback_assignment_slots == 0)
{
callback_assignment_id_t aid = registered_callback_assignments;
callback_assignments[aid].slot_flags |= SLOT_FLAGS_USED;
callback_assignments[aid].address = address;
callback_assignments[aid].callback_id = id;
registered_callback_assignments++;
return aid;
}
else
{
// find the free slot
for (callback_assignment_id_t aid = 0; aid < registered_callback_assignments; ++aid)
{
if (callback_assignments[aid].slot_flags & SLOT_FLAGS_USED)
{
// found a used slot
continue;
}
// and now an empty one
callback_assignments[aid].slot_flags |= SLOT_FLAGS_USED;
callback_assignments[aid].address = address;
callback_assignments[aid].callback_id = id;
free_callback_assignment_slots--;
return id;
}
}
}
void ESPKNXIP::__callback_delete_assignment(callback_assignment_id_t id)
{
// TODO this can be optimized if we are deleting the last element
// as then we can decrement registered_callback_assignments
// clear slot and mark it as empty
callback_assignments[id].slot_flags = SLOT_FLAGS_EMPTY;
callback_assignments[id].address.value = 0;
callback_assignments[id].callback_id = 0;
if (id == registered_callback_assignments - 1)
{
DEBUG_PRINTLN("last cba deleted");
// If this is the last callback, we can delete it by decrementing registered_callbacks.
registered_callback_assignments--;
// However, if the assignment before this slot are also empty, we can decrement even further
// First check if this was also the first element
if (id == 0)
{
DEBUG_PRINTLN("really last cba");
// If this was the last, then we are done.
return;
}
id--;
while(true)
{
DEBUG_PRINT("checking ");
DEBUG_PRINTLN((int32_t)id);
if ((callback_assignments[id].slot_flags & SLOT_FLAGS_USED) == 0)
{
DEBUG_PRINTLN("merged free slot");
// Slot before is empty
free_callback_assignment_slots--;
registered_callback_assignments--;
}
else
{
DEBUG_PRINTLN("aborted on used slot");
// Slot is used, abort
return;
}
id--;
if (id == CALLBACK_ASSIGNMENT_ID_MAX)
{
DEBUG_PRINTLN("abort on wrap");
// Wrap around, abort
return;
}
}
}
else
{
DEBUG_PRINTLN("free slot created");
// there is now one more free slot
free_callback_assignment_slots++;
}
}
bool ESPKNXIP::__callback_is_id_valid(callback_id_t id)
{
if (id < registered_callbacks)
return true;
if (callbacks[id].slot_flags & SLOT_FLAGS_USED)
return true;
return false;
}
callback_id_t ESPKNXIP::callback_register(String name, callback_fptr_t cb, void *arg, enable_condition_t cond)
{
if (registered_callbacks >= MAX_CALLBACKS)
return -1;
if (free_callback_slots == 0)
{
callback_id_t id = registered_callbacks;
callbacks[id].slot_flags |= SLOT_FLAGS_USED;
callbacks[id].name = name;
callbacks[id].fkt = cb;
callbacks[id].cond = cond;
callbacks[id].arg = arg;
registered_callbacks++;
return id;
}
else
{
// find the free slot
for (callback_id_t id = 0; id < registered_callbacks; ++id)
{
if (callbacks[id].slot_flags & SLOT_FLAGS_USED)
{
// found a used slot
continue;
}
// and now an empty one
callbacks[id].slot_flags |= SLOT_FLAGS_USED;
callbacks[id].name = name;
callbacks[id].fkt = cb;
callbacks[id].cond = cond;
callbacks[id].arg = arg;
free_callback_slots--;
return id;
}
}
}
void ESPKNXIP::callback_deregister(callback_id_t id)
{
if (!__callback_is_id_valid(id))
return;
// clear slot and mark it as empty
callbacks[id].slot_flags = SLOT_FLAGS_EMPTY;
callbacks[id].fkt = nullptr;
callbacks[id].cond = nullptr;
callbacks[id].arg = nullptr;
if (id == registered_callbacks - 1)
{
// If this is the last callback, we can delete it by decrementing registered_callbacks.
registered_callbacks--;
// However, if the callbacks before this slot are also empty, we can decrement even further
// First check if this was also the first element
if (id == 0)
{
// If this was the last, then we are done.
return;
}
id--;
while(true)
{
if ((callbacks[id].slot_flags & SLOT_FLAGS_USED) == 0)
{
// Slot is empty
free_callback_slots--;
registered_callbacks--;
}
else
{
// Slot is used, abort
return;
}
id--;
if (id == CALLBACK_ASSIGNMENT_ID_MAX)
{
// Wrap around, abort
return;
}
}
}
else
{
// there is now one more free slot
free_callback_slots++;
}
}
callback_assignment_id_t ESPKNXIP::callback_assign(callback_id_t id, address_t val)
{
if (!__callback_is_id_valid(id))
return -1;
return __callback_register_assignment(val, id);
}
void ESPKNXIP::callback_unassign(callback_assignment_id_t id)
{
if (!__callback_is_id_valid(id))
return;
__callback_delete_assignment(id);
}
/**
* Feedback functions start here
*/
feedback_id_t ESPKNXIP::feedback_register_int(String name, int32_t *value, enable_condition_t cond)
{
if (registered_feedbacks >= MAX_FEEDBACKS)
return -1;
feedback_id_t id = registered_feedbacks;
feedbacks[id].type = FEEDBACK_TYPE_INT;
feedbacks[id].name = name;
feedbacks[id].cond = cond;
feedbacks[id].data = (void *)value;
registered_feedbacks++;
return id;
}
feedback_id_t ESPKNXIP::feedback_register_float(String name, float *value, uint8_t precision, char const *prefix, char const *suffix, enable_condition_t cond)
{
if (registered_feedbacks >= MAX_FEEDBACKS)
return -1;
feedback_id_t id = registered_feedbacks;
feedbacks[id].type = FEEDBACK_TYPE_FLOAT;
feedbacks[id].name = name;
feedbacks[id].cond = cond;
feedbacks[id].data = (void *)value;
feedbacks[id].options.float_options.precision = precision;
feedbacks[id].options.float_options.prefix = prefix ? strdup(prefix) : STRING_DEFAULT_EMPTY;
feedbacks[id].options.float_options.suffix = suffix ? strdup(suffix) : STRING_DEFAULT_EMPTY;
registered_feedbacks++;
return id;
}
feedback_id_t ESPKNXIP::feedback_register_bool(String name, bool *value, char const *true_text, char const *false_text, enable_condition_t cond)
{
if (registered_feedbacks >= MAX_FEEDBACKS)
return -1;
feedback_id_t id = registered_feedbacks;
feedbacks[id].type = FEEDBACK_TYPE_BOOL;
feedbacks[id].name = name;
feedbacks[id].cond = cond;
feedbacks[id].data = (void *)value;
feedbacks[id].options.bool_options.true_text = true_text ? strdup(true_text) : STRING_DEFAULT_TRUE;
feedbacks[id].options.bool_options.false_text = false_text ? strdup(false_text) : STRING_DEFAULT_FALSE;
registered_feedbacks++;
return id;
}
feedback_id_t ESPKNXIP::feedback_register_action(String name, feedback_action_fptr_t value, const char *btn_text, void *arg, enable_condition_t cond)
{
if (registered_feedbacks >= MAX_FEEDBACKS)
return -1;
feedback_id_t id = registered_feedbacks;
feedbacks[id].type = FEEDBACK_TYPE_ACTION;
feedbacks[id].name = name;
feedbacks[id].cond = cond;
feedbacks[id].data = (void *)value;
feedbacks[id].options.action_options.arg = arg;
feedbacks[id].options.action_options.btn_text = btn_text ? strdup(btn_text) : STRING_DEFAULT_DO_THIS;
registered_feedbacks++;
return id;
}
void ESPKNXIP::loop()
{
if (server != nullptr)
{
__loop_webserver();
}
}
void ESPKNXIP::__loop_webserver()
{
server->handleClient();
}
void ESPKNXIP::__loop_knx(AsyncUDPPacket &packet)
{
size_t read = packet.length();
if (!read)
{
return;
}
DEBUG_PRINTLN(F(""));
DEBUG_PRINT(F("LEN: "));
DEBUG_PRINTLN(read);
uint8_t *buf = packet.data();
DEBUG_PRINT(F("Got packet:"));
#ifdef ESP_KNX_DEBUG
for (size_t i = 0; i < read; ++i)
{
DEBUG_PRINT(F(" 0x"));
DEBUG_PRINT(buf[i], 16);
}
#endif
DEBUG_PRINTLN(F(""));
knx_ip_pkt_t *knx_pkt = (knx_ip_pkt_t *)buf;
DEBUG_PRINT(F("ST: 0x"));
DEBUG_PRINTLN(__ntohs(knx_pkt->service_type), 16);
if (knx_pkt->header_len != 0x06 && knx_pkt->protocol_version != 0x10 && knx_pkt->service_type != KNX_ST_ROUTING_INDICATION)
return;
cemi_msg_t *cemi_msg = (cemi_msg_t *)knx_pkt->pkt_data;
DEBUG_PRINT(F("MT: 0x"));
DEBUG_PRINTLN(cemi_msg->message_code, 16);
if (cemi_msg->message_code != KNX_MT_L_DATA_IND)
return;
DEBUG_PRINT(F("ADDI: 0x"));
DEBUG_PRINTLN(cemi_msg->additional_info_len, 16);
cemi_service_t *cemi_data = &cemi_msg->data.service_information;
if (cemi_msg->additional_info_len > 0)
cemi_data = (cemi_service_t *)(((uint8_t *)cemi_data) + cemi_msg->additional_info_len);
DEBUG_PRINT(F("C1: 0x"));
DEBUG_PRINTLN(cemi_data->control_1.byte, 16);
DEBUG_PRINT(F("C2: 0x"));
DEBUG_PRINTLN(cemi_data->control_2.byte, 16);
DEBUG_PRINT(F("DT: 0x"));
DEBUG_PRINTLN(cemi_data->control_2.bits.dest_addr_type, 16);
if (cemi_data->control_2.bits.dest_addr_type != 0x01)
return;
DEBUG_PRINT(F("HC: 0x"));
DEBUG_PRINTLN(cemi_data->control_2.bits.hop_count, 16);
DEBUG_PRINT(F("EFF: 0x"));
DEBUG_PRINTLN(cemi_data->control_2.bits.extended_frame_format, 16);
DEBUG_PRINT(F("Source: 0x"));
DEBUG_PRINT(cemi_data->source.bytes.high, 16);
DEBUG_PRINT(F(" 0x"));
DEBUG_PRINTLN(cemi_data->source.bytes.low, 16);
DEBUG_PRINT(F("Dest: 0x"));
DEBUG_PRINT(cemi_data->destination.bytes.high, 16);
DEBUG_PRINT(F(" 0x"));
DEBUG_PRINTLN(cemi_data->destination.bytes.low, 16);
knx_command_type_t ct = (knx_command_type_t)(((cemi_data->data[0] & 0xC0) >> 6) | ((cemi_data->pci.apci & 0x03) << 2));
DEBUG_PRINT(F("CT: 0x"));
DEBUG_PRINTLN(ct, 16);
#ifdef ESP_KNX_DEBUG
for (int i = 0; i < cemi_data->data_len; ++i)
{
DEBUG_PRINT(F(" 0x"));
DEBUG_PRINT(cemi_data->data[i], 16);
}
#endif
DEBUG_PRINTLN(F("=="));
// Call callbacks
for (int i = 0; i < registered_callback_assignments; ++i)
{
DEBUG_PRINT(F("Testing: 0x"));
DEBUG_PRINT(callback_assignments[i].address.bytes.high, 16);
DEBUG_PRINT(F(" 0x"));
DEBUG_PRINTLN(callback_assignments[i].address.bytes.low, 16);
if (cemi_data->destination.value == callback_assignments[i].address.value)
{
DEBUG_PRINTLN(F("Found match"));
if (callbacks[callback_assignments[i].callback_id].cond && !callbacks[callback_assignments[i].callback_id].cond())
{
DEBUG_PRINTLN(F("But it's disabled"));
#if ALLOW_MULTIPLE_CALLBACKS_PER_ADDRESS
continue;
#else
return;
#endif
}
uint8_t data[cemi_data->data_len];
memcpy(data, cemi_data->data, cemi_data->data_len);
data[0] = data[0] & 0x3F;
message_t msg = {};
msg.ct = ct;
msg.received_on = cemi_data->destination;
msg.data_len = cemi_data->data_len;
msg.data = data;
callbacks[callback_assignments[i].callback_id].fkt(msg, callbacks[callback_assignments[i].callback_id].arg);
#if ALLOW_MULTIPLE_CALLBACKS_PER_ADDRESS
continue;
#else
return;
#endif
}
}
return;
}
// Global "singleton" object
ESPKNXIP knx;

View File

@ -0,0 +1,571 @@
/**
* esp-knx-ip library for KNX/IP communication on an ESP8266
* Author: Nico Weichbrodt <envy>
* License: MIT
*/
#ifndef ESP_KNX_IP_H
#define ESP_KNX_IP_H
/**
* CONFIG
* All MAX_ values must not exceed 255 (1 byte, except MAC_CONFIG_SPACE which can go up to 2 bytes, so 0xffff in theory) and must not be negative!
* Config space is restriced by EEPROM_SIZE (default 1024).
* Required EEPROM size is 8 + MAX_GA_CALLBACKS * 3 + 2 + MAX_CONFIG_SPACE which is 552 by default
*/
#define EEPROM_SIZE 1024 // [Default 1024]
#define MAX_CALLBACK_ASSIGNMENTS 10 // [Default 10] Maximum number of group address callbacks that can be stored
#define MAX_CALLBACKS 10 // [Default 10] Maximum number of callbacks that can be stored
#define MAX_CONFIGS 20 // [Default 20] Maximum number of config items that can be stored
#define MAX_CONFIG_SPACE 0x0200 // [Default 0x0200] Maximum number of bytes that can be stored for custom config
#define MAX_FEEDBACKS 20 // [Default 20] Maximum number of feedbacks that can be shown
// Callbacks
#define ALLOW_MULTIPLE_CALLBACKS_PER_ADDRESS 1 // [Default 0] Set to 1 to always test all assigned callbacks. This allows for multiple callbacks being assigned to the same address. If disabled, only the first assigned will be called.
// Webserver related
#define USE_BOOTSTRAP 1 // [Default 1] Set to 1 to enable use of bootstrap CSS for nicer webconfig. CSS is loaded from bootstrapcdn.com. Set to 0 to disable
#define ROOT_PREFIX "" // [Default ""] This gets prepended to all webserver paths, default is empty string "". Set this to "/knx" if you want the config to be available on http://<ip>/knx
#define DISABLE_EEPROM_BUTTONS 1 // [Default 0] Set to 1 to disable the EEPROM buttons in the web ui.
#define DISABLE_REBOOT_BUTTON 1 // [Default 0] Set to 1 to disable the reboot button in the web ui.
#define DISABLE_RESTORE_BUTTON 1 // [Default 0] Set to 1 to disable the "restore defaults" button in the web ui.
// These values normally don't need adjustment
#define MULTICAST_PORT 3671 // [Default 3671]
#define MULTICAST_IP IPAddress(224, 0, 23, 12) // [Default IPAddress(224, 0, 23, 12)]
#define SEND_CHECKSUM 0
// Uncomment to enable printing out debug messages.
//#define ESP_KNX_DEBUG
/**
* END CONFIG
*/
#include "Arduino.h"
#include <EEPROM.h>
#include <ESP8266WiFi.h>
#include <ESPAsyncUDP.h>
#include <ESP8266WebServer.h>
#include "DPT.h"
#define EEPROM_MAGIC (0xDEADBEEF00000000 + (MAX_CONFIG_SPACE) + (MAX_CALLBACK_ASSIGNMENTS << 16) + (MAX_CALLBACKS << 8))
// Define where debug output will be printed.
#ifndef DEBUG_PRINTER
#define DEBUG_PRINTER Serial
#endif
// Setup debug printing macros.
#ifdef ESP_KNX_DEBUG
#define DEBUG_PRINT(...) { DEBUG_PRINTER.print(__VA_ARGS__); }
#define DEBUG_PRINTLN(...) { DEBUG_PRINTER.println(__VA_ARGS__); }
#else
#define DEBUG_PRINT(...) {}
#define DEBUG_PRINTLN(...) {}
#endif
#define __ROOT_PATH ROOT_PREFIX"/"
#define __REGISTER_PATH ROOT_PREFIX"/register"
#define __DELETE_PATH ROOT_PREFIX"/delete"
#define __PHYS_PATH ROOT_PREFIX"/phys"
#define __EEPROM_PATH ROOT_PREFIX"/eeprom"
#define __CONFIG_PATH ROOT_PREFIX"/config"
#define __FEEDBACK_PATH ROOT_PREFIX"/feedback"
#define __RESTORE_PATH ROOT_PREFIX"/restore"
#define __REBOOT_PATH ROOT_PREFIX"/reboot"
/**
* Different service types, we are mainly interested in KNX_ST_ROUTING_INDICATION
*/
typedef enum __knx_service_type
{
KNX_ST_SEARCH_REQUEST = 0x0201,
KNX_ST_SEARCH_RESPONSE = 0x0202,
KNX_ST_DESCRIPTION_REQUEST = 0x0203,
KNX_ST_DESCRIPTION_RESPONSE = 0x0204,
KNX_ST_CONNECT_REQUEST = 0x0205,
KNX_ST_CONNECT_RESPONSE = 0x0206,
KNX_ST_CONNECTIONSTATE_REQUEST = 0x0207,
KNX_ST_CONNECTIONSTATE_RESPONSE = 0x0208,
KNX_ST_DISCONNECT_REQUEST = 0x0209,
KNX_ST_DISCONNECT_RESPONSE = 0x020A,
KNX_ST_DEVICE_CONFIGURATION_REQUEST = 0x0310,
KNX_ST_DEVICE_CONFIGURATION_ACK = 0x0311,
KNX_ST_TUNNELING_REQUEST = 0x0420,
KNX_ST_TUNNELING_ACK = 0x0421,
KNX_ST_ROUTING_INDICATION = 0x0530,
KNX_ST_ROUTING_LOST_MESSAGE = 0x0531,
KNX_ST_ROUTING_BUSY = 0x0532,
// KNX_ST_RLOG_START = 0x0600,
// KNX_ST_RLOG_END = 0x06FF,
KNX_ST_REMOTE_DIAGNOSTIC_REQUEST = 0x0740,
KNX_ST_REMOTE_DIAGNOSTIC_RESPONSE = 0x0741,
KNX_ST_REMOTE_BASIC_CONFIGURATION_REQUEST = 0x0742,
KNX_ST_REMOTE_RESET_REQUEST = 0x0743,
// KNX_ST_OBJSRV_START = 0x0800,
// KNX_ST_OBJSRV_END = 0x08FF,
} knx_service_type_t;
/**
* Differnt command types, first three are of main interest
*/
typedef enum __knx_command_type
{
KNX_CT_READ = 0x00,
KNX_CT_ANSWER = 0x01,
KNX_CT_WRITE = 0x02,
KNX_CT_INDIVIDUAL_ADDR_WRITE = 0x03,
KNX_CT_INDIVIDUAL_ADDR_REQUEST = 0x04,
KNX_CT_INDIVIDUAL_ADDR_RESPONSE = 0x05,
KNX_CT_ADC_READ = 0x06,
KNX_CT_ADC_ANSWER = 0x07,
KNX_CT_MEM_READ = 0x08,
KNX_CT_MEM_ANSWER = 0x09,
KNX_CT_MEM_WRITE = 0x0A,
//KNX_CT_UNKNOWN = 0x0B,
KNX_CT_MASK_VERSION_READ = 0x0C,
KNX_CT_MASK_VERSION_RESPONSE = 0x0D,
KNX_CT_RESTART = 0x0E,
KNX_CT_ESCAPE = 0x0F,
} knx_command_type_t;
/**
* cEMI message types, mainly KNX_MT_L_DATA_IND is interesting
*/
typedef enum __knx_cemi_msg_type
{
KNX_MT_L_DATA_REQ = 0x11,
KNX_MT_L_DATA_IND = 0x29,
KNX_MT_L_DATA_CON = 0x2E,
} knx_cemi_msg_type_t;
/**
* TCPI communication type
*/
typedef enum __knx_communication_type {
KNX_COT_UDP = 0x00, // Unnumbered Data Packet
KNX_COT_NDP = 0x01, // Numbered Data Packet
KNX_COT_UCD = 0x02, // Unnumbered Control Data
KNX_COT_NCD = 0x03, // Numbered Control Data
} knx_communication_type_t;
/**
* KNX/IP header
*/
typedef struct __knx_ip_pkt
{
uint8_t header_len; // Should always be 0x06
uint8_t protocol_version; // Should be version 1.0, transmitted as 0x10
uint16_t service_type; // See knx_service_type_t
union
{
struct {
uint8_t first_byte;
uint8_t second_byte;
} bytes;
uint16_t len;
} total_len; // header_len + rest of pkt. This is a bit weird as the spec says this: If the total number of bytes transmitted is greater than 252 bytes, the first “Total Length” byte is set to FF (255). Only in this case the second byte includes additional length information
uint8_t pkt_data[]; // This is of type cemi_msg_t
} knx_ip_pkt_t;
typedef struct __cemi_addi
{
uint8_t type_id;
uint8_t len;
uint8_t data[];
} cemi_addi_t;
typedef union __address
{
uint16_t value;
struct
{
uint8_t high;
uint8_t low;
} bytes;
struct __attribute__((packed))
{
uint8_t line:3;
uint8_t area:5;
uint8_t member;
} ga;
struct __attribute__((packed))
{
uint8_t line:4;
uint8_t area:4;
uint8_t member;
} pa;
uint8_t array[2];
} address_t;
typedef struct __cemi_service
{
union
{
struct
{
// Struct is reversed due to bit order
uint8_t confirm:1; // 0 = no error, 1 = error
uint8_t ack:1; // 0 = no ack, 1 = ack
uint8_t priority:2; // 0 = system, 1 = high, 2 = urgent/alarm, 3 = normal
uint8_t system_broadcast:1; // 0 = system broadcast, 1 = broadcast
uint8_t repeat:1; // 0 = repeat on error, 1 = do not repeat
uint8_t reserved:1; // always zero
uint8_t frame_type:1; // 0 = extended, 1 = standard
} bits;
uint8_t byte;
} control_1;
union
{
struct
{
// Struct is reversed due to bit order
uint8_t extended_frame_format:4;
uint8_t hop_count:3;
uint8_t dest_addr_type:1; // 0 = individual, 1 = group
} bits;
uint8_t byte;
} control_2;
address_t source;
address_t destination;
uint8_t data_len; // length of data, excluding the tpci byte
struct
{
uint8_t apci:2; // If tpci.comm_type == KNX_COT_UCD or KNX_COT_NCD, then this is apparently control data?
uint8_t tpci_seq_number:4;
uint8_t tpci_comm_type:2; // See knx_communication_type_t
} pci;
uint8_t data[];
} cemi_service_t;
typedef struct __cemi_msg
{
uint8_t message_code;
uint8_t additional_info_len;
union
{
cemi_addi_t additional_info[];
cemi_service_t service_information;
} data;
} cemi_msg_t;
typedef enum __config_type
{
CONFIG_TYPE_UNKNOWN,
CONFIG_TYPE_INT,
CONFIG_TYPE_BOOL,
CONFIG_TYPE_STRING,
CONFIG_TYPE_OPTIONS,
CONFIG_TYPE_GA,
} config_type_t;
typedef enum __feedback_type
{
FEEDBACK_TYPE_UNKNOWN,
FEEDBACK_TYPE_INT,
FEEDBACK_TYPE_FLOAT,
FEEDBACK_TYPE_BOOL,
FEEDBACK_TYPE_ACTION,
} feedback_type_t;
typedef enum __config_flags
{
CONFIG_FLAGS_NO_FLAGS = 0,
CONFIG_FLAGS_VALUE_SET = 1,
} config_flags_t;
typedef enum __slot_flags
{
SLOT_FLAGS_EMPTY = 0, // Empty slots have no flags
SLOT_FLAGS_USED = 1,
} slot_flags_t;
typedef struct __message
{
knx_command_type_t ct;
address_t received_on;
uint8_t data_len;
uint8_t *data;
} message_t;
typedef bool (*enable_condition_t)(void);
typedef void (*callback_fptr_t)(message_t const &msg, void *arg);
typedef void (*feedback_action_fptr_t)(void *arg);
typedef uint8_t callback_id_t;
#define CALLBACK_ID_MAX UINT8_MAX
typedef uint8_t callback_assignment_id_t;
#define CALLBACK_ASSIGNMENT_ID_MAX UINT8_MAX
typedef uint8_t config_id_t;
typedef uint8_t feedback_id_t;
typedef struct __option_entry
{
char const *name;
uint8_t value;
} option_entry_t;
typedef struct __config
{
config_type_t type;
String name;
uint8_t offset;
uint8_t len;
enable_condition_t cond;
union {
option_entry_t *options;
} data;
} config_t;
extern char const *string_defaults[];
#define STRING_DEFAULT_DO_THIS (string_defaults[0])
#define STRING_DEFAULT_TRUE (string_defaults[1])
#define STRING_DEFAULT_FALSE (string_defaults[2])
#define STRING_DEFAULT_EMPTY (string_defaults[3])
typedef struct __feedback_float_options
{
uint8_t precision;
char const *prefix;
char const *suffix;
} feedback_float_options_t;
typedef struct __feedback_bool_options
{
char const *true_text;
char const *false_text;
} feedback_bool_options_t;
typedef struct __feedback_action_options
{
void *arg;
char const *btn_text;
} feedback_action_options_t;
typedef struct __feedback
{
feedback_type_t type;
String name;
enable_condition_t cond;
void *data;
union {
feedback_bool_options_t bool_options;
feedback_float_options_t float_options;
feedback_action_options_t action_options;
} options;
} feedback_t;
typedef struct __callback
{
uint8_t slot_flags;
callback_fptr_t fkt;
enable_condition_t cond;
void *arg;
String name;
} callback_t;
typedef struct __callback_assignment
{
uint8_t slot_flags;
address_t address;
callback_id_t callback_id;
} callback_assignment_t;
class ESPKNXIP {
public:
ESPKNXIP();
void load();
void start();
void start(ESP8266WebServer *srv);
void loop();
void save_to_eeprom();
void restore_from_eeprom();
callback_id_t callback_register(String name, callback_fptr_t cb, void *arg = nullptr, enable_condition_t cond = nullptr);
callback_assignment_id_t callback_assign(callback_id_t id, address_t val);
void callback_deregister(callback_id_t id);
void callback_unassign(callback_assignment_id_t id);
void physical_address_set(address_t const &addr);
address_t physical_address_get();
// Configuration functions
config_id_t config_register_string(String name, uint8_t len, String _default, enable_condition_t cond = nullptr);
config_id_t config_register_int(String name, int32_t _default, enable_condition_t cond = nullptr);
config_id_t config_register_bool(String name, bool _default, enable_condition_t cond = nullptr);
config_id_t config_register_options(String name, option_entry_t *options, uint8_t _default, enable_condition_t cond = nullptr);
config_id_t config_register_ga(String name, enable_condition_t cond = nullptr);
String config_get_string(config_id_t id);
int32_t config_get_int(config_id_t id);
bool config_get_bool(config_id_t id);
uint8_t config_get_options(config_id_t id);
address_t config_get_ga(config_id_t id);
void config_set_string(config_id_t id, String val);
void config_set_int(config_id_t id, int32_t val);
void config_set_bool(config_id_t, bool val);
void config_set_options(config_id_t id, uint8_t val);
void config_set_ga(config_id_t id, address_t const &val);
// Feedback functions
feedback_id_t feedback_register_int(String name, int32_t *value, enable_condition_t cond = nullptr);
feedback_id_t feedback_register_float(String name, float *value, uint8_t precision = 2, char const *prefix = nullptr, char const *suffix = nullptr, enable_condition_t cond = nullptr);
feedback_id_t feedback_register_bool(String name, bool *value, char const *true_text = nullptr, char const *false_text = nullptr, enable_condition_t cond = nullptr);
feedback_id_t feedback_register_action(String name, feedback_action_fptr_t value, char const *btn_text = nullptr, void *arg = nullptr, enable_condition_t = nullptr);
// Send functions
void send(address_t const &receiver, knx_command_type_t ct, uint8_t data_len, uint8_t *data);
void send_1bit(address_t const &receiver, knx_command_type_t ct, uint8_t bit);
void send_2bit(address_t const &receiver, knx_command_type_t ct, uint8_t twobit);
void send_4bit(address_t const &receiver, knx_command_type_t ct, uint8_t fourbit);
void send_1byte_int(address_t const &receiver, knx_command_type_t ct, int8_t val);
void send_1byte_uint(address_t const &receiver, knx_command_type_t ct, uint8_t val);
void send_2byte_int(address_t const &receiver, knx_command_type_t ct, int16_t val);
void send_2byte_uint(address_t const &receiver, knx_command_type_t ct, uint16_t val);
void send_2byte_float(address_t const &receiver, knx_command_type_t ct, float val);
void send_3byte_time(address_t const &receiver, knx_command_type_t ct, uint8_t weekday, uint8_t hours, uint8_t minutes, uint8_t seconds);
void send_3byte_time(address_t const &receiver, knx_command_type_t ct, time_of_day_t const &time) { send_3byte_time(receiver, ct, time.weekday, time.hours, time.minutes, time.seconds); }
void send_3byte_date(address_t const &receiver, knx_command_type_t ct, uint8_t day, uint8_t month, uint8_t year);
void send_3byte_date(address_t const &receiver, knx_command_type_t ct, date_t const &date) { send_3byte_date(receiver, ct, date.day, date.month, date.year); }
void send_3byte_color(address_t const &receiver, knx_command_type_t ct, uint8_t red, uint8_t green, uint8_t blue);
void send_3byte_color(address_t const &receiver, knx_command_type_t ct, color_t const &color) { send_3byte_color(receiver, ct, color.red, color.green, color.blue); }
void send_4byte_int(address_t const &receiver, knx_command_type_t ct, int32_t val);
void send_4byte_uint(address_t const &receiver, knx_command_type_t ct, uint32_t val);
void send_4byte_float(address_t const &receiver, knx_command_type_t ct, float val);
void write_1bit(address_t const &receiver, uint8_t bit) { send_1bit(receiver, KNX_CT_WRITE, bit); }
void write_2bit(address_t const &receiver, uint8_t twobit) { send_2bit(receiver, KNX_CT_WRITE, twobit); }
void write_4bit(address_t const &receiver, uint8_t fourbit) { send_4bit(receiver, KNX_CT_WRITE, fourbit); }
void write_1byte_int(address_t const &receiver, int8_t val) { send_1byte_int(receiver, KNX_CT_WRITE, val); }
void write_1byte_uint(address_t const &receiver, uint8_t val) { send_1byte_uint(receiver, KNX_CT_WRITE, val); }
void write_2byte_int(address_t const &receiver, int16_t val) { send_2byte_int(receiver, KNX_CT_WRITE, val); }
void write_2byte_uint(address_t const &receiver, uint16_t val) { send_2byte_uint(receiver, KNX_CT_WRITE, val); }
void write_2byte_float(address_t const &receiver, float val) { send_2byte_float(receiver, KNX_CT_WRITE, val); }
void write_3byte_time(address_t const &receiver, uint8_t weekday, uint8_t hours, uint8_t minutes, uint8_t seconds) { send_3byte_time(receiver, KNX_CT_WRITE, weekday, hours, minutes, seconds); }
void write_3byte_time(address_t const &receiver, time_of_day_t const &time) { send_3byte_time(receiver, KNX_CT_WRITE, time.weekday, time.hours, time.minutes, time.seconds); }
void write_3byte_date(address_t const &receiver, uint8_t day, uint8_t month, uint8_t year) { send_3byte_date(receiver, KNX_CT_WRITE, day, month, year); }
void write_3byte_date(address_t const &receiver, date_t const &date) { send_3byte_date(receiver, KNX_CT_WRITE, date.day, date.month, date.year); }
void write_3byte_color(address_t const &receiver, uint8_t red, uint8_t green, uint8_t blue) { send_3byte_color(receiver, KNX_CT_WRITE, red, green, blue); }
void write_3byte_color(address_t const &receiver, color_t const &color) { send_3byte_color(receiver, KNX_CT_WRITE, color); }
void write_4byte_int(address_t const &receiver, int32_t val) { send_4byte_int(receiver, KNX_CT_WRITE, val); }
void write_4byte_uint(address_t const &receiver, uint32_t val) { send_4byte_uint(receiver, KNX_CT_WRITE, val); }
void write_4byte_float(address_t const &receiver, float val) { send_4byte_float(receiver, KNX_CT_WRITE, val);}
void answer_1bit(address_t const &receiver, uint8_t bit) { send_1bit(receiver, KNX_CT_ANSWER, bit); }
void answer_2bit(address_t const &receiver, uint8_t twobit) { send_2bit(receiver, KNX_CT_ANSWER, twobit); }
void answer_4bit(address_t const &receiver, uint8_t fourbit) { send_4bit(receiver, KNX_CT_ANSWER, fourbit); }
void answer_1byte_int(address_t const &receiver, int8_t val) { send_1byte_int(receiver, KNX_CT_ANSWER, val); }
void answer_1byte_uint(address_t const &receiver, uint8_t val) { send_1byte_uint(receiver, KNX_CT_ANSWER, val); }
void answer_2byte_int(address_t const &receiver, int16_t val) { send_2byte_int(receiver, KNX_CT_ANSWER, val); }
void answer_2byte_uint(address_t const &receiver, uint16_t val) { send_2byte_uint(receiver, KNX_CT_ANSWER, val); }
void answer_2byte_float(address_t const &receiver, float val) { send_2byte_float(receiver, KNX_CT_ANSWER, val); }
void answer_3byte_time(address_t const &receiver, uint8_t weekday, uint8_t hours, uint8_t minutes, uint8_t seconds) { send_3byte_time(receiver, KNX_CT_ANSWER, weekday, hours, minutes, seconds); }
void answer_3byte_time(address_t const &receiver, time_of_day_t const &time) { send_3byte_time(receiver, KNX_CT_ANSWER, time.weekday, time.hours, time.minutes, time.seconds); }
void answer_3byte_date(address_t const &receiver, uint8_t day, uint8_t month, uint8_t year) { send_3byte_date(receiver, KNX_CT_ANSWER, day, month, year); }
void answer_3byte_date(address_t const &receiver, date_t const &date) { send_3byte_date(receiver, KNX_CT_ANSWER, date.day, date.month, date.year); }
void answer_3byte_color(address_t const &receiver, uint8_t red, uint8_t green, uint8_t blue) { send_3byte_color(receiver, KNX_CT_ANSWER, red, green, blue); }
void answer_3byte_color(address_t const &receiver, color_t const &color) { send_3byte_color(receiver, KNX_CT_ANSWER, color); }
void answer_4byte_int(address_t const &receiver, int32_t val) { send_4byte_int(receiver, KNX_CT_ANSWER, val); }
void answer_4byte_uint(address_t const &receiver, uint32_t val) { send_4byte_uint(receiver, KNX_CT_ANSWER, val); }
void answer_4byte_float(address_t const &receiver, float val) { send_4byte_float(receiver, KNX_CT_ANSWER, val);}
bool data_to_bool(uint8_t *data);
int8_t data_to_1byte_int(uint8_t *data);
uint8_t data_to_1byte_uint(uint8_t *data);
int16_t data_to_2byte_int(uint8_t *data);
uint16_t data_to_2byte_uint(uint8_t *data);
float data_to_2byte_float(uint8_t *data);
color_t data_to_3byte_color(uint8_t *data);
time_of_day_t data_to_3byte_time(uint8_t *data);
date_t data_to_3byte_data(uint8_t *data);
int32_t data_to_4byte_int(uint8_t *data);
uint32_t data_to_4byte_uint(uint8_t *data);
float data_to_4byte_float(uint8_t *data);
static address_t GA_to_address(uint8_t area, uint8_t line, uint8_t member)
{
// Yes, the order is correct, see the struct definition above
address_t tmp = {.ga={line, area, member}};
return tmp;
}
static address_t PA_to_address(uint8_t area, uint8_t line, uint8_t member)
{
// Yes, the order is correct, see the struct definition above
address_t tmp = {.pa={line, area, member}};
return tmp;
}
private:
void __start();
void __loop_knx(AsyncUDPPacket &packet);
// Webserver functions
void __loop_webserver();
void __handle_root();
void __handle_register();
void __handle_delete();
void __handle_set();
#if !DISABLE_EEPROM_BUTTONS
void __handle_eeprom();
#endif
void __handle_config();
void __handle_feedback();
#if !DISABLE_RESTORE_BUTTONS
void __handle_restore();
#endif
#if !DISABLE_REBOOT_BUTTONS
void __handle_reboot();
#endif
void __config_set_flags(config_id_t id, config_flags_t flags);
void __config_set_string(config_id_t id, String &val);
void __config_set_int(config_id_t id, int32_t val);
void __config_set_bool(config_id_t id, bool val);
void __config_set_options(config_id_t id, uint8_t val);
void __config_set_ga(config_id_t id, address_t const &val);
bool __callback_is_id_valid(callback_id_t id);
callback_assignment_id_t __callback_register_assignment(address_t address, callback_id_t id);
void __callback_delete_assignment(callback_assignment_id_t id);
ESP8266WebServer *server;
address_t physaddr;
AsyncUDP udp;
callback_assignment_id_t registered_callback_assignments;
callback_assignment_id_t free_callback_assignment_slots;
callback_assignment_t callback_assignments[MAX_CALLBACK_ASSIGNMENTS];
callback_id_t registered_callbacks;
callback_id_t free_callback_slots;
callback_t callbacks[MAX_CALLBACKS];
config_id_t registered_configs;
uint8_t custom_config_data[MAX_CONFIG_SPACE];
uint8_t custom_config_default_data[MAX_CONFIG_SPACE];
config_t custom_configs[MAX_CONFIGS];
feedback_id_t registered_feedbacks;
feedback_t feedbacks[MAX_FEEDBACKS];
uint16_t __ntohs(uint16_t);
};
// Global "singleton" object
extern ESPKNXIP knx;
#endif

View File

@ -0,0 +1,159 @@
/*
* This is an example showing a simple environment sensor based on a BME280 attached via I2C.
* This sketch was tested on a WeMos D1 mini
*/
#include <Adafruit_BME280.h>
#include <esp-knx-ip.h>
// WiFi config here
const char* ssid = "myssid";
const char* pass = "mypassword";
#define LED_PIN D4
#define UPDATE_INTERVAL 10000
unsigned long next_change = 0;
float last_temp = 0.0;
float last_hum = 0.0;
float last_pres = 0.0;
config_id_t temp_ga, hum_ga, pres_ga;
config_id_t hostname_id;
config_id_t update_rate_id, send_rate_id;
config_id_t enable_sending_id;
config_id_t enable_reading_id;
Adafruit_BME280 bme;
void setup() {
pinMode(LED_PIN, OUTPUT);
Serial.begin(115200);
hostname_id = knx.config_register_string("Hostname", 20, String("env"));
enable_sending_id = knx.config_register_bool("Send on update", true);
update_rate_id = knx.config_register_int("Update rate (ms)", UPDATE_INTERVAL);
temp_ga = knx.config_register_ga("Temperature", show_periodic_options);
hum_ga = knx.config_register_ga("Humidity", show_periodic_options);
pres_ga = knx.config_register_ga("Pressure", show_periodic_options);
knx.callback_register("Read Temperature", temp_cb);
knx.callback_register("Read Humidity", hum_cb);
knx.callback_register("Read Pressure", pres_cb);
knx.feedback_register_float("Temperature (°C)", &last_temp);
knx.feedback_register_float("Humidity (%)", &last_hum);
knx.feedback_register_float("Pressure (hPa)", &last_pres, 0);
// Load previous config from EEPROM
knx.load();
// Init sensor
if (!bme.begin(0x76)) {
Serial.println("Could not find a valid BME280 sensor, check wiring!");
}
// Init WiFi
WiFi.hostname(knx.config_get_string(hostname_id));
WiFi.begin(ssid, pass);
Serial.println("");
Serial.print("[Connecting]");
Serial.print(ssid);
digitalWrite(LED_PIN, LOW);
while (WiFi.status() != WL_CONNECTED) {
digitalWrite(LED_PIN, HIGH);
delay(250);
Serial.print(".");
digitalWrite(LED_PIN, LOW);
delay(250);
}
digitalWrite(LED_PIN, HIGH);
// Start knx
knx.start();
Serial.println();
Serial.println("Connected to wifi");
Serial.println(WiFi.localIP());
}
void loop() {
knx.loop();
unsigned long now = millis();
if (next_change < now)
{
next_change = now + knx.config_get_int(update_rate_id);
last_temp = bme.readTemperature();
last_hum = bme.readHumidity();
last_pres = bme.readPressure()/100.0f;
Serial.print("T: ");
Serial.print(last_temp);
Serial.print("°C H: ");
Serial.print(last_hum);
Serial.print("% P: ");
Serial.print(last_pres);
Serial.println("hPa");
if (knx.config_get_bool(enable_sending_id))
{
knx.write_2byte_float(knx.config_get_ga(temp_ga), last_temp);
knx.write_2byte_float(knx.config_get_ga(hum_ga), last_hum);
knx.write_2byte_float(knx.config_get_ga(pres_ga), last_pres);
}
}
delay(50);
}
bool show_periodic_options()
{
return knx.config_get_bool(enable_sending_id);
}
bool enable_reading_callback()
{
return knx.config_get_bool(enable_reading_id);
}
void temp_cb(message_t const &msg, void *arg)
{
switch (msg.ct)
{
case KNX_CT_READ:
{
knx.answer_2byte_float(msg.received_on, last_temp);
break;
}
}
}
void hum_cb(message_t const &msg, void *arg)
{
switch (msg.ct)
{
case KNX_CT_READ:
{
knx.answer_2byte_float(msg.received_on, last_hum);
break;
}
}
}
void pres_cb(message_t const &msg, void *arg)
{
switch (msg.ct)
{
case KNX_CT_READ:
{
knx.answer_2byte_float(msg.received_on, last_pres);
break;
}
}
}

View File

@ -0,0 +1,183 @@
#include <esp-knx-ip.h>
// WiFi config here
const char* ssid = "ssid";
const char* pass = "pass";
// Common
#define LED_PIN 13
// For Basic and S20
#define BTN1_PIN 0
#define CH1_PIN 12
// For 4CH
#define BTN2_PIN 9
#define CH2_PIN 5
#define BTN3_PIN 10
#define CH3_PIN 4
#define BTN4_PIN 14
#define CH4_PIN 15
typedef enum __type_option
{
SONOFF_TYPE_NONE = 0,
SONOFF_TYPE_BASIC = 1,
SONOFF_TYPE_S20 = 2,
SONOFF_TYPE_4CH = 3,
SONOFF_TYPE_4CH_PRO = 4,
} type_option_t;
option_entry_t type_options[] = {
{"Sonoff Basic", SONOFF_TYPE_BASIC},
{"Sonoff S20", SONOFF_TYPE_S20},
{"Sonoff 4CH", SONOFF_TYPE_4CH},
{"Sonoff 4CH Pro", SONOFF_TYPE_4CH_PRO},
{nullptr, 0}
};
config_id_t hostname_id;
config_id_t type_id;
typedef struct __sonoff_channel
{
int pin;
int btn_pin;
config_id_t status_ga_id;
bool state;
bool last_btn_state;
} sonoff_channel_t;
sonoff_channel_t channels[] = {
{CH1_PIN, BTN1_PIN, 0, false, false},
{CH2_PIN, BTN2_PIN, 0, false, false},
{CH3_PIN, BTN3_PIN, 0, false, false},
{CH4_PIN, BTN4_PIN, 0, false, false},
};
void setup()
{
pinMode(LED_PIN, OUTPUT);
pinMode(BTN1_PIN, INPUT_PULLUP);
pinMode(BTN2_PIN, INPUT_PULLUP);
pinMode(BTN3_PIN, INPUT_PULLUP);
pinMode(BTN4_PIN, INPUT_PULLUP);
pinMode(CH1_PIN, OUTPUT);
pinMode(CH2_PIN, OUTPUT);
pinMode(CH3_PIN, OUTPUT);
pinMode(CH4_PIN, OUTPUT);
Serial.begin(115200);
// Register the config options
hostname_id = knx.config_register_string("Hostname", 20, String("sonoff"));
type_id = knx.config_register_options("Type", type_options, SONOFF_TYPE_BASIC);
channels[0].status_ga_id = knx.config_register_ga("Channel 1 Status GA");
channels[1].status_ga_id = knx.config_register_ga("Channel 2 Status GA", is_4ch_or_4ch_pro);
channels[2].status_ga_id = knx.config_register_ga("Channel 3 Status GA", is_4ch_or_4ch_pro);
channels[3].status_ga_id = knx.config_register_ga("Channel 4 Status GA", is_4ch_or_4ch_pro);
knx.callback_register("Channel 1", channel_cb, &channels[0]);
knx.callback_register("Channel 2", channel_cb, &channels[1], is_4ch_or_4ch_pro);
knx.callback_register("Channel 3", channel_cb, &channels[2], is_4ch_or_4ch_pro);
knx.callback_register("Channel 4", channel_cb, &channels[3], is_4ch_or_4ch_pro);
knx.feedback_register_bool("Channel 1 is on", &(channels[0].state));
knx.feedback_register_action("Toogle channel 1", toggle_chan, &channels[0]);
knx.feedback_register_bool("Channel 2 is on", &(channels[1].state), is_4ch_or_4ch_pro);
knx.feedback_register_action("Toogle channel 2", toggle_chan, &channels[1], is_4ch_or_4ch_pro);
knx.feedback_register_bool("Channel 3 is on", &(channels[2].state), is_4ch_or_4ch_pro);
knx.feedback_register_action("Toogle channel 3", toggle_chan, &channels[2], is_4ch_or_4ch_pro);
knx.feedback_register_bool("Channel 4 is on", &(channels[3].state), is_4ch_or_4ch_pro);
knx.feedback_register_action("Toogle channel 4", toggle_chan, &channels[3], is_4ch_or_4ch_pro);
knx.load();
// Init WiFi
WiFi.hostname(knx.config_get_string(hostname_id));
WiFi.begin(ssid, pass);
Serial.println("");
Serial.print("[Connecting]");
Serial.print(ssid);
digitalWrite(LED_PIN, LOW);
while (WiFi.status() != WL_CONNECTED) {
digitalWrite(LED_PIN, HIGH);
delay(500);
Serial.print(".");
digitalWrite(LED_PIN, LOW);
}
digitalWrite(LED_PIN, HIGH);
// Start knx
knx.start();
Serial.println();
Serial.println("Connected to wifi");
Serial.println(WiFi.localIP());
}
void loop()
{
knx.loop();
// Check local buttons
check_button(&channels[0]);
if (is_4ch_or_4ch_pro())
{
check_button(&channels[1]);
check_button(&channels[2]);
check_button(&channels[3]);
}
delay(50);
}
bool is_basic_or_s20()
{
uint8_t type = knx.config_get_options(type_id);
return type == SONOFF_TYPE_BASIC || type == SONOFF_TYPE_S20;
}
bool is_4ch_or_4ch_pro()
{
uint8_t type = knx.config_get_options(type_id);
return type == SONOFF_TYPE_4CH ||type == SONOFF_TYPE_4CH_PRO;
}
void check_button(sonoff_channel_t *chan)
{
bool state_now = digitalRead(chan->btn_pin) == HIGH ? true : false;
if (state_now != chan->last_btn_state && state_now == LOW)
{
chan->state = !chan->state;
digitalWrite(chan->pin, chan->state ? HIGH : LOW);
knx.write_1bit(knx.config_get_ga(chan->status_ga_id), chan->state);
}
chan->last_btn_state = state_now;
}
void toggle_chan(void *arg)
{
sonoff_channel_t *chan = (sonoff_channel_t *)arg;
chan->state = !chan->state;
digitalWrite(chan->pin, chan->state ? HIGH : LOW);
knx.write_1bit(knx.config_get_ga(chan->status_ga_id), chan->state);
}
void channel_cb(message_t const &msg, void *arg)
{
sonoff_channel_t *chan = (sonoff_channel_t *)arg;
switch (msg.ct)
{
case KNX_CT_WRITE:
chan->state = msg.data[0];
Serial.println(chan->state ? "Toggle on" : "Toggle off");
digitalWrite(chan->pin, chan->state ? HIGH : LOW);
knx.write_1bit(knx.config_get_ga(chan->status_ga_id), chan->state);
break;
case KNX_CT_READ:
knx.answer_1bit(msg.received_on, chan->state);
}
}

View File

@ -0,0 +1,142 @@
/*
* This is an example showing a simple environment sensor based on a BME280 attached via I2C.
* It shows, how the library can used to statically configure a device without a webserver for config.
* This sketch was tested on a WeMos D1 mini
*/
#include <Adafruit_BME280.h>
#include <esp-knx-ip.h>
// WiFi config here
const char* ssid = "myssid";
const char* pass = "mypassword";
#define LED_PIN D4
#define UPDATE_INTERVAL 10000
unsigned long next_change = 0;
float last_temp = 0.0;
float last_hum = 0.0;
float last_pres = 0.0;
Adafruit_BME280 bme;
// Group addresses to send to (1/1/1, 1/1/2 and 1/1/3)
address_t temp_ga = knx.GA_to_address(1, 1, 1);
address_t hum_ga = knx.GA_to_address(1, 1, 2);
address_t pres_ga = knx.GA_to_address(1, 1, 3);
void setup() {
pinMode(LED_PIN, OUTPUT);
Serial.begin(115200);
callback_id_t temp_cb = knx.callback_register("Read Temperature", temp_cb);
callback_id_t hum_cb =knx.callback_register("Read Humidity", hum_cb);
callback_id_t pres_cb =knx.callback_register("Read Pressure", pres_cb);
// Assign callbacks to group addresses (2/1/1, 2/1/2, 2/1/3)
knx.callback_assign(temp_cb, knx.GA_to_address(2, 1, 1));
knx.callback_assign(hum_cb, knx.GA_to_address(2, 1, 2));
knx.callback_assign(pres_cb, knx.GA_to_address(2, 1, 3));
// Set physical address (1.1.1)
knx.physical_address_set(knx.PA_to_address(1, 1, 1));
// Do not call knx.load() for static config, it will try to load config from EEPROM which we don't have here
// Init sensor
if (!bme.begin(0x76)) {
Serial.println("Could not find a valid BME280 sensor, check wiring!");
}
// Init WiFi
WiFi.hostname("env");
WiFi.begin(ssid, pass);
Serial.println("");
Serial.print("[Connecting]");
Serial.print(ssid);
digitalWrite(LED_PIN, LOW);
while (WiFi.status() != WL_CONNECTED) {
digitalWrite(LED_PIN, HIGH);
delay(250);
Serial.print(".");
digitalWrite(LED_PIN, LOW);
delay(250);
}
digitalWrite(LED_PIN, HIGH);
// Start knx, disable webserver by passing nullptr
knx.start(nullptr);
Serial.println();
Serial.println("Connected to wifi");
Serial.println(WiFi.localIP());
}
void loop() {
knx.loop();
unsigned long now = millis();
if (next_change < now)
{
next_change = now + UPDATE_INTERVAL;
last_temp = bme.readTemperature();
last_hum = bme.readHumidity();
last_pres = bme.readPressure()/100.0f;
Serial.print("T: ");
Serial.print(last_temp);
Serial.print("°C H: ");
Serial.print(last_hum);
Serial.print("% P: ");
Serial.print(last_pres);
Serial.println("hPa");
knx.write_2byte_float(temp_ga, last_temp);
knx.write_2byte_float(hum_ga, last_hum);
knx.write_2byte_float(pres_ga, last_pres);
}
delay(50);
}
void temp_cb(message_t const &msg, void *arg)
{
switch (msg.ct)
{
case KNX_CT_READ:
{
knx.answer_2byte_float(msg.received_on, last_temp);
break;
}
}
}
void hum_cb(message_t const &msg, void *arg)
{
switch (msg.ct)
{
case KNX_CT_READ:
{
knx.answer_2byte_float(msg.received_on, last_hum);
break;
}
}
}
void pres_cb(message_t const &msg, void *arg)
{
switch (msg.ct)
{
case KNX_CT_READ:
{
knx.answer_2byte_float(msg.received_on, last_pres);
break;
}
}
}

View File

@ -0,0 +1,104 @@
# datatypes
address_t DATA_TYPE
message_t DATA_TYPE
callback_id_t DATA_TYPE
callback_assignment_id_t DATA_TYPE
option_entry_t DATA_TYPE
config_id_t DATA_TYPE
enable_condition_t DATA_TYPE
callback_fptr_t DATA_TYPE
feedback_action_fptr_t DATA_TYPE
knx_command_type_t DATA_TYPE
# methods
setup KEYWORD2
loop KEYWORD2
GA_to_address KEYWORD2
PA_to_address KEYWORD2
callback_register KEYWORD2
callback_assign KEYWORD2
callback_deregister KEYWORD2
callback_unassign KEYWORD2
physical_address_set KEYWORD2
physical_address_get KEYWORD2
config_register_string KEYWORD2
config_register_int KEYWORD2
config_register_bool KEYWORD2
config_register_options KEYWORD2
config_register_ga KEYWORD2
config_get_string KEYWORD2
config_get_int KEYWORD2
config_get_bool KEYWORD2
config_get_options KEYWORD2
config_get_ga KEYWORD2
config_set_string KEYWORD2
config_set_int KEYWORD2
config_set_bool KEYWORD2
config_set_options KEYWORD2
config_set_ga KEYWORD2
feedback_register_int KEYWORD2
feedback_register_float KEYWORD2
feedback_register_bool KEYWORD2
feedback_register_action KEYWORD2
send_1bit KEYWORD2
send_2bit KEYWORD2
send_4bit KEYWORD2
send_1byte_int KEYWORD2
send_1byte_uint KEYWORD2
send_2byte_int KEYWORD2
send_2byte_uint KEYWORD2
send_2byte_float KEYWORD2
send_3byte_time KEYWORD2
send_3byte_time KEYWORD2
send_3byte_date KEYWORD2
send_3byte_date KEYWORD2
send_3byte_color KEYWORD2
send_3byte_color KEYWORD2
send_4byte_int KEYWORD2
send_4byte_uint KEYWORD2
send_4byte_float KEYWORD2
write_1bit KEYWORD2
write_2bit KEYWORD2
write_4bit KEYWORD2
write_1byte_int KEYWORD2
write_1byte_uint KEYWORD2
write_2byte_int KEYWORD2
write_2byte_uint KEYWORD2
write_2byte_float KEYWORD2
write_3byte_time KEYWORD2
write_3byte_time KEYWORD2
write_3byte_date KEYWORD2
write_3byte_date KEYWORD2
write_3byte_color KEYWORD2
write_3byte_color KEYWORD2
write_4byte_int KEYWORD2
write_4byte_uint KEYWORD2
write_4byte_float KEYWORD2
answer_1bit KEYWORD2
answer_2bit KEYWORD2
answer_4bit KEYWORD2
answer_1byte_int KEYWORD2
answer_1byte_uint KEYWORD2
answer_2byte_int KEYWORD2
answer_2byte_uint KEYWORD2
answer_2byte_float KEYWORD2
answer_3byte_time KEYWORD2
answer_3byte_time KEYWORD2
answer_3byte_date KEYWORD2
answer_3byte_date KEYWORD2
answer_3byte_color KEYWORD2
answer_3byte_color KEYWORD2
answer_4byte_int KEYWORD2
answer_4byte_uint KEYWORD2
answer_4byte_float KEYWORD2
data_to_1byte_int KEYWORD 2
data_to_2byte_int KEYWORD 2
data_to_2byte_float KEYWORD 2
data_to_4byte_float KEYWORD 2
data_to_3byte_color KEYWORD 2
data_to_3byte_time KEYWORD 2
data_to_3byte_data KEYWORD 2
# constants
knx LITERAL1

View File

@ -0,0 +1,10 @@
name=ESP KNX IP Library
version=0.4
author=Nico Weichbrodt <envy>
maintainer=Nico Weichbrodt <envy>
sentence=ESP8266 library for KNX/IP communication.
paragraph=Build your own IoT devices with KNX/IP connectivity! This library depends on the ESPAsyncUDP library.
category=Communication
url=https://github.com/envy/esp-knx-ip
architectures=esp8266
includes=esp-knx-ip.h

View File

@ -2,6 +2,7 @@
* Release rules up to 511 characters
* Prepare for feature release - call on translators to update their language files
* Add timer sunrise and sunset offset (#2378)
* Fix negative Latitude and Longitude value entry (#2461)
*
* 5.12.0k
* Prepare for simple rules of up to 255 characters by enlarging Settings area to now 2048 bytes

View File

@ -50,7 +50,7 @@ typedef union { // Restricted by MISRA-C Rule 18.4 bu
uint32_t mqtt_serial : 1; // bit 22 (v5.12.0f)
uint32_t rules_enabled : 1; // bit 23 (v5.12.0j)
uint32_t rules_once : 1; // bit 24 (v5.12.0k)
uint32_t spare25 : 1;
uint32_t knx_enabled : 1; // bit 25 (v5.12.0l) KNX
uint32_t spare26 : 1;
uint32_t spare27 : 1;
uint32_t spare28 : 1;
@ -226,13 +226,9 @@ struct SYSCFG {
uint8_t light_speed; // 4A2
uint8_t light_scheme; // 4A3
uint8_t light_width; // 4A4
byte free_4A5[1]; // 4A5
byte knx_GA_registered; // 4A5 Number of Group Address to read
uint16_t light_wakeup; // 4A6
byte free_4A8[1]; // 4A8
byte knx_CB_registered; // 4A8 Number of Group Address to write
char web_password[33]; // 4A9
uint8_t switchmode[MAX_SWITCHES]; // 4CA
char ntp_server[3][33]; // 4CE
@ -256,7 +252,13 @@ struct SYSCFG {
int latitude; // 6B0
int longitude; // 6B4
byte free_6b8[72]; // 6B8
uint16_t knx_physsical_addr; // 6B8 (address_t is a uint16_t)
uint16_t knx_GA_addr[MAX_KNX_GA]; // 6BA (address_t is a uint16_t) x KNX_max_GA
uint16_t knx_CB_addr[MAX_KNX_CB]; // 6CE (address_t is a uint16_t) x KNX_max_CB
byte knx_GA_param[MAX_KNX_GA]; // 6E2 Type of Input (relay changed, button pressed, sensor read <-teleperiod)
byte knx_CB_param[MAX_KNX_CB]; // 6EC Type of Output (set relay, toggle relay, reply sensor value)
byte free_6b8[10]; // 6F6
char rules[MAX_RULE_SIZE]; // 700 uses 512 bytes in v5.12.0l

View File

@ -48,6 +48,8 @@ typedef unsigned long power_t; // Power (Relay) type
#define MAX_FRIENDLYNAMES 4 // Max number of Friendly names
#define MAX_DOMOTICZ_IDX 4 // Max number of Domoticz device, key and switch indices
#define MAX_DOMOTICZ_SNS_IDX 12 // Max number of Domoticz sensors indices
#define MAX_KNX_GA 10 // Max number of KNX Group Addresses to read that can be set
#define MAX_KNX_CB 10 // Max number of KNX Group Addresses to write that can be set
#define MAX_RULE_SIZE 512 // Max number of characters in rules
#define MODULE SONOFF_BASIC // [Module] Select default model

View File

@ -185,6 +185,7 @@ uint8_t spi_flg = 0; // SPI configured
uint8_t light_type = 0; // Light types
bool pwm_present = false; // Any PWM channel configured with SetOption15 0
boolean mdns_begun = false;
unsigned long features = 0UL;
char my_version[33]; // Composed version string
char my_hostname[33]; // Composed Wifi hostname
@ -1143,6 +1144,9 @@ boolean SendKey(byte key, byte device, byte state)
result = RulesProcess();
#endif // USE_RULES
}
#ifdef USE_KNX
KnxSendButtonPower(key, device, state);
#endif // USE_KNX
return result;
}
@ -1193,6 +1197,9 @@ void ExecuteCommandPower(byte device, byte state)
#ifdef USE_DOMOTICZ
DomoticzUpdatePowerState(device);
#endif // USE_DOMOTICZ
#ifdef USE_KNX
KnxUpdatePowerState(device, power);
#endif // USE_KNX
if (device <= MAX_PULSETIMERS) {
// pulse_timer[(device -1)] = (power & mask) ? Settings.pulse_timer[(device -1)] : 0;
pulse_timer[(device -1)] = (((POWER_ALL_OFF_PULSETIME_ON == Settings.poweronstate) ? ~power : power) & mask) ? Settings.pulse_timer[(device -1)] : 0;

View File

@ -33,6 +33,12 @@ void WifiWpsStatusCallback(wps_cb_status status);
}
#endif
#ifdef USE_KNX
#include <esp-knx-ip.h>
void KNX_CB_Action(message_t const &msg, void *arg);
#endif // USE_KNX
#define USE_DHT // Default DHT11 sensor needs no external library
#ifdef USE_ALL_SENSORS // ===================== Configure sonoff-xxl.bin =========================

View File

@ -218,7 +218,9 @@ double CharToDouble(char *str)
right *= fac;
}
}
return left + right;
double result = left + right;
if (left < 0) { result = left - right; }
return result;
}
char* dtostrfd(double number, unsigned char prec, char *s)

View File

@ -196,6 +196,9 @@ const char HTTP_BTN_MENU_MQTT[] PROGMEM =
#endif // USE_DOMOTICZ
"";
const char HTTP_BTN_MENU4[] PROGMEM =
#ifdef USE_KNX
"<br/><form action='kn' method='get'><button>" D_CONFIGURE_KNX "</button></form>"
#endif // USE_KNX
"<br/><form action='lg' method='get'><button>" D_CONFIGURE_LOGGING "</button></form>"
"<br/><form action='co' method='get'><button>" D_CONFIGURE_OTHER "</button></form>"
"<br/>"
@ -349,6 +352,9 @@ void StartWebserver(int type, IPAddress ipweb)
WebServer->on("/dm", HandleDomoticzConfiguration);
#endif // USE_DOMOTICZ
}
#ifdef USE_KNX
WebServer->on("/kn", HandleKNXConfiguration);
#endif // USE_KNX
WebServer->on("/lg", HandleLoggingConfiguration);
WebServer->on("/co", HandleOtherConfiguration);
WebServer->on("/dl", HandleBackupConfiguration);
@ -378,6 +384,9 @@ void StartWebserver(int type, IPAddress ipweb)
}
#endif // USE_EMULATION
WebServer->onNotFound(HandleNotFound);
#ifdef USE_KNX
KNXStart();
#endif // USE_KNX
}
reset_web_log_flag = 0;
WebServer->begin(); // Web server start

View File

@ -289,7 +289,7 @@ void PrepShowTimer(uint8_t index)
#ifdef USE_SUNRISE
int16_t hour = xtimer.time / 60;
if ((1 == xtimer.mode) || (2 == xtimer.mode)) { // Sunrise or Sunset
if (hour > 11) hour = (hour -12) * -1;
if (hour > 11) { hour = (hour -12) * -1; }
}
snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("%s\"" D_CMND_TIMER "%d\":{\"" D_JSON_TIMER_ARM "\":%d,\"" D_JSON_TIMER_MODE "\":%d,\"" D_JSON_TIMER_TIME "\":\"%02d:%02d\",\"" D_JSON_TIMER_DAYS "\":\"%s\",\"" D_JSON_TIMER_REPEAT "\":%d,\"" D_JSON_TIMER_OUTPUT "\":%d,\"" D_JSON_TIMER_ACTION "\":%d}"),
mqtt_data, index, xtimer.arm, xtimer.mode, hour, xtimer.time % 60, days, xtimer.repeat, xtimer.device +1, xtimer.power);
@ -349,14 +349,14 @@ boolean TimerCommand()
const char *substr = strtok(time_str, ":");
if (substr != NULL) {
value = atoi(substr);
if (value < 0) value = abs(value) +12; // Allow entering timer offset from -11:59 to -00:01 converted to 12:01 to 23:59
if (value > 23) value = 23;
if (value < 0) { value = abs(value) +12; } // Allow entering timer offset from -11:59 to -00:01 converted to 12:01 to 23:59
if (value > 23) { value = 23; }
itime = value * 60;
substr = strtok(NULL, ":");
if (substr != NULL) {
value = atoi(substr);
if (value < 0) value = 0;
if (value > 59) value = 59;
if (value < 0) { value = 0; }
if (value > 59) { value = 59; }
itime += value;
}
}
@ -371,7 +371,7 @@ boolean TimerCommand()
uint8_t i = 0;
while ((ch != '\0') && (i < 7)) {
ch = *tday++;
if (ch == '-') ch = '0';
if (ch == '-') { ch = '0'; }
uint8_t mask = 1 << i++;
Settings.timer[index].days |= (ch == '0') ? 0 : mask;
}
@ -626,7 +626,7 @@ void HandleTimerConfiguration()
page.replace(F("</style>"), FPSTR(HTTP_TIMER_STYLE));
page += FPSTR(HTTP_FORM_TIMER);
for (byte i = 0; i < MAX_TIMERS; i++) {
if (i > 0) page += F(",");
if (i > 0) { page += F(","); }
page += String(Settings.timer[i].data);
}
#ifdef USE_SUNRISE
@ -656,12 +656,7 @@ void TimerSaveSettings()
for (byte i = 0; i < MAX_TIMERS; i++) {
timer.data = strtol(p, &p, 10);
p++; // Skip comma
if (timer.time < 1440) {
#ifdef USE_SUNRISE
// if ((1 == timer.mode) || (2 == timer.mode)) timer.time = Settings.timer[i].time; // Do not save time on Sunrise or Sunset
#endif
Settings.timer[i].data = timer.data;
}
if (timer.time < 1440) { Settings.timer[i].data = timer.data; }
snprintf_P(log_data, sizeof(log_data), PSTR("%s%s0x%08X"), log_data, (i > 0)?",":"", Settings.timer[i].data);
}
AddLog(LOG_LEVEL_DEBUG);

View File

@ -141,7 +141,7 @@ bool RulesRuleMatch(String &event, String &rule)
// Step1: Analyse rule
int pos = rule.indexOf('#');
if (pos == -1) return false; // No # sign in rule
if (pos == -1) { return false; } // No # sign in rule
String rule_task = rule.substring(0, pos); // "INA219" or "SYSTEM"
String rule_name = rule.substring(pos +1); // "CURRENT>0.100" or "BOOT"
@ -173,7 +173,7 @@ bool RulesRuleMatch(String &event, String &rule)
// Step2: Search rule_task and rule_name
StaticJsonBuffer<400> jsonBuf;
JsonObject &root = jsonBuf.parseObject(event);
if (!root.success()) return false; // No valid JSON data
if (!root.success()) { return false; } // No valid JSON data
double value = 0;
const char* str_value = root[rule_task][rule_name];
@ -182,7 +182,7 @@ bool RulesRuleMatch(String &event, String &rule)
// rule_task.c_str(), rule_name.c_str(), tmp_value.c_str(), rules_trigger_count, bitRead(rules_triggers, rules_trigger_count), event.c_str(), (str_value) ? str_value : "none");
// AddLog(LOG_LEVEL_DEBUG);
if (!root[rule_task][rule_name].success()) return false;
if (!root[rule_task][rule_name].success()) { return false; }
// No value but rule_name is ok
rules_event_value = str_value; // Prepare %value%
@ -227,8 +227,8 @@ bool RulesProcess()
{
bool serviced = false;
if (!Settings.flag.rules_enabled) return serviced; // Not enabled
if (!strlen(Settings.rules)) return serviced; // No rules
if (!Settings.flag.rules_enabled) { return serviced; } // Not enabled
if (!strlen(Settings.rules)) { return serviced; } // No rules
String event_saved = mqtt_data;
event_saved.toUpperCase();
@ -237,21 +237,21 @@ bool RulesProcess()
rules_trigger_count = 0;
int plen = 0;
while (true) {
rules = rules.substring(plen); // Select relative to last rule
rules = rules.substring(plen); // Select relative to last rule
rules.trim();
if (!rules.length()) return serviced; // No more rules
if (!rules.length()) { return serviced; } // No more rules
String rule = rules;
rule.toUpperCase(); // "ON INA219#CURRENT>0.100 DO BACKLOG DIMMER 10;COLOR 100000 ENDON"
if (!rule.startsWith("ON ")) return serviced; // Bad syntax - Nothing to start on
rule.toUpperCase(); // "ON INA219#CURRENT>0.100 DO BACKLOG DIMMER 10;COLOR 100000 ENDON"
if (!rule.startsWith("ON ")) { return serviced; } // Bad syntax - Nothing to start on
int pevt = rule.indexOf(" DO ");
if (pevt == -1) return serviced; // Bad syntax - Nothing to do
String event_trigger = rule.substring(3, pevt); // "INA219#CURRENT>0.100"
if (pevt == -1) { return serviced; } // Bad syntax - Nothing to do
String event_trigger = rule.substring(3, pevt); // "INA219#CURRENT>0.100"
plen = rule.indexOf(" ENDON");
if (plen == -1) return serviced; // Bad syntax - No endon
String commands = rules.substring(pevt +4, plen); // "Backlog Dimmer 10;Color 100000"
if (plen == -1) { return serviced; } // Bad syntax - No endon
String commands = rules.substring(pevt +4, plen); // "Backlog Dimmer 10;Color 100000"
plen += 6;
// snprintf_P(log_data, sizeof(log_data), PSTR("RUL: Trigger |%s|, Commands |%s|"), event_trigger.c_str(), commands.c_str());
@ -375,7 +375,7 @@ boolean RulesCommand()
String uc_command = command;
uc_command += " "; // Distuingish from RuleTimer
uc_command.toUpperCase();
if (!uc_data.indexOf(uc_command)) strlcpy(Settings.rules, XdrvMailbox.data, sizeof(Settings.rules));
if (!uc_data.indexOf(uc_command)) { strlcpy(Settings.rules, XdrvMailbox.data, sizeof(Settings.rules)); }
*/
strlcpy(Settings.rules, XdrvMailbox.data, sizeof(Settings.rules));
}