Add cEMI Server with KNX USB HID support (#47)

This commit is contained in:
nanosonde 2019-12-09 19:53:08 +01:00 committed by thelsing
parent fd0b16b1b4
commit a76fc31b80
45 changed files with 1927 additions and 101 deletions

29
doc/knx_cemi_notes.md Normal file
View File

@ -0,0 +1,29 @@
KNX cEMI Server
===============
Implementation Notes
--------------------
* currently only implemented for KNX USB Data Interface (Tunnel) and not KNXnet/IP for now
* basically provides a complete KNX device (TP or RF) that also has a USB interface built-in which can be used to program either the local device stack or
remote devices via the attached medium (TP or RF).
* tested with ETS 5.7.x
* tested with Net'N'Node
* **Be careful when using this with TP as your HW setup might not have a galvanic isolation between the KNX bus and USB!**
* An [USB isolator](https://www.olimex.com/Products/USB-Modules/USB-ISO/) might be the easiest option. Not tested!
* ToDo: How is this realized in commercial KNX USB Data Interfaces?
* cEMI client address (e.g. ETS) is based on KNX physical address of the local device stack +1
* Example: local device: 1.2.10 -> cEMI client address: 1.2.11
* PropertyWrite commands to change the cEMI client address are only temporary until next device restart
* requires a USB stack which properly work with USB HID and provides interrupt IN/OUT endpoints for KNX HID reports.
* normal Arduino USB device stack does NOT work as it is missing required functionality
* TinyUSB stack is used instead, therefore the Adafruit SAMD core with TinyUSB enabled is used as the Arduino core
Development environment
-----------------------
* PlatformIO
* Segger J-Link EDU
* [GY-SAMD21 Board](https://eckstein-shop.de/GY-SAMD21-Mini-Breakout-fuer-Arduino), compatible to [Adafruit Feather M0](https://www.adafruit.com/product/2772)

View File

@ -5,45 +5,53 @@ Implementation Notes
--------------------
* KNX-RF E-Mode (pushbutton method) is NOT supported!
* KNX-RF S-Mode is implemented as KNX-RF READY (KNX-RF 1.R) which means only one single channel and no fast-ack is used.
-> KNX RF Multi (KNX-RF 1.M) would be required for this. However, implementation is way more complex as frequency hopping (fast and slow channels) is used and fast-ack
* KNX RF Multi (KNX-RF 1.M) would be required for this. However, implementation is way more complex as frequency hopping (fast and slow channels) is used and fast-ack
is based on a TDMA-like access scheme which has very strict timing requirements for the time slots.
-> summary: KNX-RF 1.R does NOT acknowledge packets on the air (data link layer). So standard GROUP_VALUE_WRITE messages in multicast mode could get lost!
* summary: KNX-RF 1.R does NOT acknowledge packets on the air (data link layer). So standard GROUP_VALUE_WRITE messages in multicast mode could get lost!
Connection-oriented communication for device management is handled by the transport layer and uses T_ACK and T_NACK.
* the driver (rf_physical_layer.cpp) uses BOTH signals (GDO0, GDO2) of the RF transceiver C1101
-> GDO0 is asserted on RX/TX packet start and de-asserted on RX/TX packet end or RX/TX FIFO overflow/underflow
-> GDO2 is asserted is the FIFO needs to read out (RX) or refilled (TX)
* GDO0 is asserted on RX/TX packet start and de-asserted on RX/TX packet end or RX/TX FIFO overflow/underflow
* GDO2 is asserted is the FIFO needs to read out (RX) or refilled (TX)
* the driver (rf_physical_layer.cpp) uses both packet length modes of the CC1101: infinite length and fixed length are switched WHILE receiving or transmitting a packet.
* the edges of the signals GDO0 and GDO2 are detected by polling in the main loop and NOT by using interrupts
-> as a consequence the main loop must not be delayed too much, the transceiver receives/transmitts the data bytes itself into/from the FIFOs though (max. FIFO size 64 bytes each (TX/RX)!).
-> KNX-RF bitrate is 16384 bits/sec. -> so 40 bytes (Preamble, Syncword, Postamble) around 20ms for reception/transmission of a complete packet (to be verified!)
-> another implementation using interrupts could also be realized
* as a consequence the main loop must not be delayed too much, the transceiver receives/transmitts the data bytes itself into/from the FIFOs though (max. FIFO size 64 bytes each (TX/RX)!).
* KNX-RF bitrate is 16384 bits/sec. -> so 40 bytes (Preamble, Syncword, Postamble) around 20ms for reception/transmission of a complete packet (to be verified!)
* another implementation using interrupts could also be realized
* the driver does not use the wake-on-radio of the CC1101. The bidirectional battery powered devices are not useful at the moment.
-> wake-on-radio would also require the MCU to sleep and be woken up through interrupts.
-> BUT: UNIdirectional (battery powered) device could be realized: one the device has been configured in bidirectional mode.
* wake-on-radio would also require the MCU to sleep and be woken up through interrupts.
* BUT: UNIdirectional (battery powered) device could be realized: one the device has been configured in bidirectional mode.
The device (MCU) could sleep and only wake up if something needs to be done. Only useful for transmitters (e.g. sensors like push button, temperature sensor, etc.).
ToDo
----
* Packet duplication prevention based on the data link layer frame counter if KNX-RF retransmitters (range extension) are active (should be easy to add)
* KNX-RF 1.M (complex, may need an additional MCU, not planned for now)
-> maybe with a more capable transceiver: http://www.lapis-semi.com/en/semicon/telecom/telecom-product.php?PartNo=ML7345
* maybe with a more capable transceiver: http://www.lapis-semi.com/en/semicon/telecom/telecom-product.php?PartNo=ML7345
it could handle manchester code, CRC-16 and the whole Wireless MBUS frame structure in hardware. Also used by Tapko for KAIstack.
* KNX Data Secure with security proxy profile in line coupler (e.g. TP<>RF). See KNX AN158 (KNX Data Secure draft spec.) p.9
-> KNX-RF very much benefits from having authenticated, encrypted data exchange.
-> Security Proxy terminates the secure applicationj layer (S-AL) in the line coupler. So the existing plain TP installation without data secure feature
* KNX-RF very much benefits from having authenticated, encrypted data exchange.
* Security Proxy terminates the secure applicationj layer (S-AL) in the line coupler. So the existing plain TP installation without data secure feature
can be kept as is.
Development Setup
-----------------
Development is done on a cheap Wemos SAMD21 M0-Mini board with a standard CC1101 module (868MHz) from Ebay. Beware of defective and bad quality modules.
The SAMD21 MCU is connected via SWD (Segger J-Link) to PlatformIO (Visual Studio Code). Additionally the standard UART (Arduino) is used for serial debug messages.
The USB port of the SAMD21 is not used at all.
* Development is done on a cheap Wemos SAMD21 M0-Mini board with a standard CC1101 module (868MHz) from Ebay.
* Beware of defective and bad quality modules!
* The SAMD21 MCU is connected via SWD (Segger J-Link) to PlatformIO (Visual Studio Code). Additionally the standard UART (Arduino) is used for serial debug messages.
* The USB port of the SAMD21 is not used at all.
Connection wiring:
------------------
Signal SAMD21 CC1101
----------+-------------+---------------
Signal | SAMD21 | CC1101
----------|-------------|--------------
SPI_nCS | D10(PA18) | CSN
SPI_MOSI | D11(PA16) | SI
SPI_MISO | D12(PA19) | SO
@ -53,8 +61,11 @@ GDO2 | D9(PA07) | GDO2
Arduino MZEROUSB variant needs patching to enable SPI on SERCOM1 on D10-D13.
If you do not want to patch variant files, use a compatible board that provides SPI on D10-D13.
variant.h
---------
```
/*
* SPI Interfaces
*/
@ -66,9 +77,11 @@ variant.h
#define PERIPH_SPI sercom1
#define PAD_SPI_TX SPI_PAD_0_SCK_1
#define PAD_SPI_RX SERCOM_RX_PAD_3
```
variant.cpp
-----------
```
// 18..23 - SPI pins (ICSP:MISO,SCK,MOSI)
// ----------------------
{ PORTA, 19, PIO_SERCOM, PIN_ATTR_DIGITAL, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_12 }, // MISO: SERCOM1/PAD[3]
@ -77,3 +90,4 @@ variant.cpp
{ PORTA, 16, PIO_SERCOM, PIN_ATTR_DIGITAL, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_10 }, // MOSI: SERCOM1/PAD[0]
{ NOT_A_PORT, 0, PIO_NOT_A_PIN, PIN_ATTR_NONE, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_NONE }, // RESET
{ NOT_A_PORT, 0, PIO_NOT_A_PIN, PIN_ATTR_NONE, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_NONE }, // GND
```

5
knx-usb/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch

8
knx-usb/custom_hwids.py Normal file
View File

@ -0,0 +1,8 @@
Import("env")
board_config = env.BoardConfig()
board_config.update("build.hwids", [
# ["0x135e", "0x0024"] # Merten GmbH & Co. KG
# ["0x0E77", "0x2001"] # Weinzierl Engineering GmbH
["0x7660", "0x0002"] # KNX Assoc.
])

35
knx-usb/platformio.ini Normal file
View File

@ -0,0 +1,35 @@
;PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[env:adafruit_feather_m0]
platform = atmelsam
board = adafruit_feather_m0
framework = arduino
; VID must be changed to some known KNX Manufacturer
; so that the KNX USB interface gets recognized by ETS
extra_scripts = pre:custom_hwids.py
board_build.usb_product="KNX RF - USB Interface"
lib_deps =
Adafruit_TinyUSB_Arduino
SPI
https://github.com/thelsing/FlashStorage.git
knx
build_flags =
-DMEDIUM_TYPE=2
-DUSE_CEMI_SERVER
-DUSE_TINYUSB
-Wno-unknown-pragmas
debug_tool = jlink
#upload_protocol = jlink

128
knx-usb/src/main.cpp Normal file
View File

@ -0,0 +1,128 @@
#include <Arduino.h>
#include <Adafruit_TinyUSB.h>
#include <knx.h>
/*
* USB stuff
*/
#define STRINGIFY(s) XSTRINGIFY(s)
#define XSTRINGIFY(s) #s
#pragma message ("USB_VID=" STRINGIFY(USB_VID))
#pragma message ("USB_PID=" STRINGIFY(USB_PID))
#pragma message ("USB_MANUFACTURER=" STRINGIFY(USB_MANUFACTURER))
#pragma message ("USB_PRODUCT=" STRINGIFY(USB_PRODUCT))
Adafruit_USBD_HID usb_hid;
// Invoked when received SET_REPORT control request or
// received data on interrupt OUT endpoint
void setReportCallback(uint8_t report_id, hid_report_type_t report_type, uint8_t const* data, uint16_t bufSize)
{
// we don't use multiple report and report ID
(void) report_id;
(void) report_type;
UsbTunnelInterface::receiveHidReport(data, bufSize);
}
bool sendHidReport(uint8_t* data, uint16_t length)
{
// We do not use reportId of the TinyUSB sendReport()-API here but instead provide it in the first byte of the buffer
return usb_hid.sendReport(0, data, length);
}
bool isSendHidReportPossible()
{
return usb_hid.ready();
}
/*
* KNX stuff
*/
// create macros easy access to group objects
#define goTemperature knx.getGroupObject(1)
#define goHumidity knx.getGroupObject(2)
uint32_t cyclSend = 0;
uint8_t sendCounter = 0;
long lastsend = 0;
/******************************************************************************************/
/*
* setup()
*/
void setup(void)
{
Serial1.begin(115200);
ArduinoPlatform::SerialDebug = &Serial1;
Serial1.println("Start.");
usb_hid.enableOutEndpoint(true);
usb_hid.setPollInterval(2);
usb_hid.setReportDescriptor(UsbTunnelInterface::getKnxHidReportDescriptor(), UsbTunnelInterface::getHidReportDescriptorLength());
usb_hid.setReportCallback(NULL, setReportCallback);
usb_hid.begin();
// wait until device mounted
while( !USBDevice.mounted() ) delay(1);
println("KNX USB Interface enabled.");
// read adress table, association table, groupobject table and parameters from eeprom
knx.readMemory();
if (knx.induvidualAddress() == 0)
knx.progMode(true);
if (knx.configured())
{
cyclSend = knx.paramInt(0);
Serial1.print("Zykl. send:");
Serial1.println(cyclSend);
goTemperature.dataPointType(Dpt(9, 1));
goHumidity.dataPointType(Dpt(9, 1));
}
// start the framework.
knx.start();
}
/*
* loop()
*/
void loop(void)
{
// don't delay here to much. Otherwise you might lose packages or mess up the timing with ETS
knx.loop();
// only run the application code if the device was configured with ETS
if(!knx.configured())
return;
long now = millis();
if ((now - lastsend) < 3000)
return;
lastsend = now;
float temp = 1.2345;
float humi = 60.2;
String output = String(millis());
output += ", " + String(temp);
output += ", " + String(humi);
Serial1.println(output);
if (sendCounter++ == cyclSend)
{
sendCounter = 0;
goTemperature.value(temp);
goHumidity.value(humi);
}
}

View File

@ -21,6 +21,8 @@ class AddressTableObject : public TableObject
void readProperty(PropertyID id, uint32_t start, uint32_t& count, uint8_t* data);
uint8_t* save(uint8_t* buffer);
uint8_t* restore(uint8_t* buffer);
ObjectType objectType() { return OT_ADDR_TABLE; }
/**
* returns the number of group addresses of the object.
*/

View File

@ -25,7 +25,7 @@ void ApplicationProgramObject::readProperty(PropertyID id, uint32_t start, uint3
}
}
void ApplicationProgramObject::writeProperty(PropertyID id, uint8_t start, uint8_t* data, uint8_t count)
void ApplicationProgramObject::writeProperty(PropertyID id, uint32_t start, uint8_t* data, uint32_t& count)
{
switch (id)
{

View File

@ -7,8 +7,9 @@ class ApplicationProgramObject : public TableObject
public:
ApplicationProgramObject(Platform& platform);
void readProperty(PropertyID id, uint32_t start, uint32_t& count, uint8_t* data);
void writeProperty(PropertyID id, uint8_t start, uint8_t* data, uint8_t count);
void writeProperty(PropertyID id, uint32_t start, uint8_t* data, uint32_t& count);
uint8_t propertySize(PropertyID id);
ObjectType objectType() { return OT_APPLICATION_PROG; }
uint8_t* data(uint32_t addr);
uint8_t getByte(uint32_t addr);
uint16_t getWord(uint32_t addr);

View File

@ -7,6 +7,7 @@ class AssociationTableObject : public TableObject
public:
AssociationTableObject(Platform& platform);
void readProperty(PropertyID id, uint32_t start, uint32_t& count, uint8_t* data);
ObjectType objectType() { return OT_ASSOC_TABLE; }
uint8_t* save(uint8_t* buffer);
uint8_t* restore(uint8_t* buffer);

View File

@ -256,6 +256,16 @@ void BusAccessUnit::domainAddressSerialNumberReadIndication(Priority priority, H
{
}
void BusAccessUnit::propertyValueRead(ObjectType objectType, uint8_t objectInstance, uint8_t propertyId,
uint32_t &numberOfElements, uint16_t startIndex,
uint8_t **data, uint32_t &length)
{
}
void BusAccessUnit::propertyValueWrite(ObjectType objectType, uint8_t objectInstance, uint8_t propertyId,
uint32_t &numberOfElements, uint16_t startIndex,
uint8_t* data, uint32_t length)
{
}

View File

@ -1,6 +1,7 @@
#pragma once
#include <stdint.h>
#include "knx_types.h"
#include "interface_object.h"
class BusAccessUnit
{
@ -117,4 +118,12 @@ class BusAccessUnit
uint8_t* knxSerialNumber);
virtual void domainAddressSerialNumberReadIndication(Priority priority, HopCountType hopType, uint8_t* knxSerialNumber);
virtual void propertyValueRead(ObjectType objectType, uint8_t objectInstance, uint8_t propertyId,
uint32_t &numberOfElements, uint16_t startIndex,
uint8_t **data, uint32_t &length);
virtual void propertyValueWrite(ObjectType objectType, uint8_t objectInstance, uint8_t propertyId,
uint32_t &numberOfElements, uint16_t startIndex,
uint8_t* data, uint32_t length);
};

View File

@ -8,8 +8,16 @@ using namespace std;
Bau07B0::Bau07B0(Platform& platform)
: BauSystemB(platform),
_dlLayer(_deviceObj, _addrTable, _netLayer, _platform)
#ifdef USE_CEMI_SERVER
, _cemiServer(*this)
#endif
{
_netLayer.dataLinkLayer(_dlLayer);
#ifdef USE_CEMI_SERVER
_cemiServer.dataLinkLayer(_dlLayer);
_dlLayer.cemiServer(_cemiServer);
_memory.addSaveRestore(&_cemiServerObject);
#endif
// Set Mask Version in Device Object depending on the BAU
uint16_t maskVersion;
@ -38,6 +46,37 @@ InterfaceObject* Bau07B0::getInterfaceObject(uint8_t idx)
return &_appProgram;
case 5: // would be app_program 2
return nullptr;
#ifdef USE_CEMI_SERVER
case 6:
return &_cemiServerObject;
#endif
default:
return nullptr;
}
}
InterfaceObject* Bau07B0::getInterfaceObject(ObjectType objectType, uint8_t objectInstance)
{
// We do not use it right now.
// Required for coupler mode as there are multiple router objects for example
(void) objectInstance;
switch (objectType)
{
case OT_DEVICE:
return &_deviceObj;
case OT_ADDR_TABLE:
return &_addrTable;
case OT_ASSOC_TABLE:
return &_assocTable;
case OT_GRP_OBJ_TABLE:
return &_groupObjTable;
case OT_APPLICATION_PROG:
return &_appProgram;
#ifdef USE_CEMI_SERVER
case OT_CEMI_SERVER:
return &_cemiServerObject;
#endif
default:
return nullptr;
}
@ -52,3 +91,11 @@ DataLinkLayer& Bau07B0::dataLinkLayer()
{
return _dlLayer;
}
void Bau07B0::loop()
{
::BauSystemB::loop();
#ifdef USE_CEMI_SERVER
_cemiServer.loop();
#endif
}

View File

@ -2,20 +2,34 @@
#include "bau_systemB.h"
#include "tpuart_data_link_layer.h"
#include "cemi_server.h"
#include "cemi_server_object.h"
class Bau07B0 : public BauSystemB
{
public:
Bau07B0(Platform& platform);
void loop();
protected:
InterfaceObject* getInterfaceObject(uint8_t idx);
InterfaceObject* getInterfaceObject(ObjectType objectType, uint8_t objectInstance);
uint8_t* descriptor();
DataLinkLayer& dataLinkLayer();
private:
TpUartDataLinkLayer _dlLayer;
#ifdef USE_CEMI_SERVER
CemiServer _cemiServer;
CemiServerObject _cemiServerObject;
#endif
uint8_t _descriptor[2] = {0x07, 0xb0};
#ifdef USE_CEMI_SERVER
const uint32_t _ifObjs[7] = { 6, // length
OT_DEVICE, OT_ADDR_TABLE, OT_ASSOC_TABLE, OT_GRP_OBJ_TABLE, OT_APPLICATION_PROG, OT_CEMI_SERVER};
#else
const uint32_t _ifObjs[6] = { 5, // length
OT_DEVICE, OT_ADDR_TABLE, OT_ASSOC_TABLE, OT_GRP_OBJ_TABLE, OT_APPLICATION_PROG};
#endif
};

View File

@ -10,9 +10,17 @@ using namespace std;
Bau27B0::Bau27B0(Platform& platform)
: BauSystemB(platform),
_dlLayer(_deviceObj, _rfMediumObj, _addrTable, _netLayer, _platform)
#ifdef USE_CEMI_SERVER
, _cemiServer(*this)
#endif
{
_netLayer.dataLinkLayer(_dlLayer);
_memory.addSaveRestore(&_rfMediumObj);
#ifdef USE_CEMI_SERVER
_cemiServer.dataLinkLayer(_dlLayer);
_dlLayer.cemiServer(_cemiServer);
_memory.addSaveRestore(&_cemiServerObject);
#endif
// Set Mask Version in Device Object depending on the BAU
uint16_t maskVersion;
@ -51,6 +59,39 @@ InterfaceObject* Bau27B0::getInterfaceObject(uint8_t idx)
return nullptr;
case 6:
return &_rfMediumObj;
#ifdef USE_CEMI_SERVER
case 7:
return &_cemiServerObject;
#endif
default:
return nullptr;
}
}
InterfaceObject* Bau27B0::getInterfaceObject(ObjectType objectType, uint8_t objectInstance)
{
// We do not use it right now.
// Required for coupler mode as there are multiple router objects for example
(void) objectInstance;
switch (objectType)
{
case OT_DEVICE:
return &_deviceObj;
case OT_ADDR_TABLE:
return &_addrTable;
case OT_ASSOC_TABLE:
return &_assocTable;
case OT_GRP_OBJ_TABLE:
return &_groupObjTable;
case OT_APPLICATION_PROG:
return &_appProgram;
case OT_RF_MEDIUM:
return &_rfMediumObj;
#ifdef USE_CEMI_SERVER
case OT_CEMI_SERVER:
return &_cemiServerObject;
#endif
default:
return nullptr;
}
@ -66,53 +107,45 @@ DataLinkLayer& Bau27B0::dataLinkLayer()
return _dlLayer;
}
void Bau27B0::loop()
{
::BauSystemB::loop();
#ifdef USE_CEMI_SERVER
_cemiServer.loop();
#endif
}
void Bau27B0::domainAddressSerialNumberWriteIndication(Priority priority, HopCountType hopType, uint8_t* rfDoA,
uint8_t* knxSerialNumber)
{
uint8_t curSerialNumber[6];
pushWord(_deviceObj.manufacturerId(), &curSerialNumber[0]);
pushInt(_deviceObj.bauNumber(), &curSerialNumber[2]);
// If the received serial number matches our serial number
// then store the received RF domain address in the RF medium object
if (!memcmp(knxSerialNumber, curSerialNumber, 6))
if (!memcmp(knxSerialNumber, _deviceObj.knxSerialNumber(), 6))
_rfMediumObj.rfDomainAddress(rfDoA);
}
void Bau27B0::domainAddressSerialNumberReadIndication(Priority priority, HopCountType hopType, uint8_t* knxSerialNumber)
{
uint8_t curSerialNumber[6];
pushWord(_deviceObj.manufacturerId(), &curSerialNumber[0]);
pushInt(_deviceObj.bauNumber(), &curSerialNumber[2]);
// If the received serial number matches our serial number
// then send a response with the current RF domain address stored in the RF medium object
if (!memcmp(knxSerialNumber, curSerialNumber, 6))
if (!memcmp(knxSerialNumber, _deviceObj.knxSerialNumber(), 6))
_appLayer.domainAddressSerialNumberReadResponse(priority, hopType, _rfMediumObj.rfDomainAddress(), knxSerialNumber);
}
void Bau27B0::individualAddressSerialNumberWriteIndication(Priority priority, HopCountType hopType, uint16_t newIndividualAddress,
uint8_t* knxSerialNumber)
{
uint8_t curSerialNumber[6];
pushWord(_deviceObj.manufacturerId(), &curSerialNumber[0]);
pushInt(_deviceObj.bauNumber(), &curSerialNumber[2]);
// If the received serial number matches our serial number
// then store the received new individual address in the device object
if (!memcmp(knxSerialNumber, curSerialNumber, 6))
if (!memcmp(knxSerialNumber, _deviceObj.knxSerialNumber(), 6))
_deviceObj.induvidualAddress(newIndividualAddress);
}
void Bau27B0::individualAddressSerialNumberReadIndication(Priority priority, HopCountType hopType, uint8_t* knxSerialNumber)
{
uint8_t curSerialNumber[6];
pushWord(_deviceObj.manufacturerId(), &curSerialNumber[0]);
pushInt(_deviceObj.bauNumber(), &curSerialNumber[2]);
// If the received serial number matches our serial number
// then send a response with the current RF domain address stored in the RF medium object and the serial number
if (!memcmp(knxSerialNumber, curSerialNumber, 6))
if (!memcmp(knxSerialNumber, _deviceObj.knxSerialNumber(), 6))
_appLayer.IndividualAddressSerialNumberReadResponse(priority, hopType, _rfMediumObj.rfDomainAddress(), knxSerialNumber);
}

View File

@ -4,24 +4,37 @@
#include "rf_medium_object.h"
#include "rf_physical_layer.h"
#include "rf_data_link_layer.h"
#include "cemi_server.h"
#include "cemi_server_object.h"
class Bau27B0 : public BauSystemB
{
public:
Bau27B0(Platform& platform);
void loop();
protected:
InterfaceObject* getInterfaceObject(uint8_t idx);
InterfaceObject* getInterfaceObject(ObjectType objectType, uint8_t objectInstance);
uint8_t* descriptor();
DataLinkLayer& dataLinkLayer();
private:
RfDataLinkLayer _dlLayer;
RfMediumObject _rfMediumObj;
#ifdef USE_CEMI_SERVER
CemiServer _cemiServer;
CemiServerObject _cemiServerObject;
#endif
uint8_t _descriptor[2] = {0x27, 0xB0};
#ifdef USE_CEMI_SERVER
const uint32_t _ifObjs[8] = { 7, // length
OT_DEVICE, OT_ADDR_TABLE, OT_ASSOC_TABLE, OT_GRP_OBJ_TABLE, OT_APPLICATION_PROG, OT_RF_MEDIUM, OT_CEMI_SERVER};
#else
const uint32_t _ifObjs[7] = { 6, // length
OT_DEVICE, OT_ADDR_TABLE, OT_ASSOC_TABLE, OT_GRP_OBJ_TABLE, OT_APPLICATION_PROG, OT_RF_MEDIUM};
#endif
void domainAddressSerialNumberWriteIndication(Priority priority, HopCountType hopType, uint8_t* rfDoA,
uint8_t* knxSerialNumber);

View File

@ -47,6 +47,31 @@ InterfaceObject* Bau57B0::getInterfaceObject(uint8_t idx)
}
}
InterfaceObject* Bau57B0::getInterfaceObject(ObjectType objectType, uint8_t objectInstance)
{
// We do not use it right now.
// Required for coupler mode as there are multiple router objects for example
(void) objectInstance;
switch (objectType)
{
case OT_DEVICE:
return &_deviceObj;
case OT_ADDR_TABLE:
return &_addrTable;
case OT_ASSOC_TABLE:
return &_assocTable;
case OT_GRP_OBJ_TABLE:
return &_groupObjTable;
case OT_APPLICATION_PROG:
return &_appProgram;
case OT_IP_PARAMETER:
return &_ipParameters;
default:
return nullptr;
}
}
uint8_t* Bau57B0::descriptor()
{
return _descriptor;

View File

@ -11,6 +11,7 @@ class Bau57B0 : public BauSystemB
protected:
InterfaceObject* getInterfaceObject(uint8_t idx);
InterfaceObject* getInterfaceObject(ObjectType objectType, uint8_t objectInstance);
uint8_t* descriptor();
DataLinkLayer& dataLinkLayer();

View File

@ -209,10 +209,11 @@ void BauSystemB::propertyDescriptionReadIndication(Priority priority, HopCountTy
void BauSystemB::propertyValueWriteIndication(Priority priority, HopCountType hopType, uint16_t asap, uint8_t objectIndex,
uint8_t propertyId, uint8_t numberOfElements, uint16_t startIndex, uint8_t* data, uint8_t length)
{
uint32_t elementCount = numberOfElements;
InterfaceObject* obj = getInterfaceObject(objectIndex);
if(obj)
obj->writeProperty((PropertyID)propertyId, startIndex, data, numberOfElements);
propertyValueReadIndication(priority, hopType, asap, objectIndex, propertyId, numberOfElements, startIndex);
obj->writeProperty((PropertyID)propertyId, startIndex, data, elementCount);
propertyValueReadIndication(priority, hopType, asap, objectIndex, propertyId, elementCount, startIndex);
}
void BauSystemB::propertyValueReadIndication(Priority priority, HopCountType hopType, uint16_t asap, uint8_t objectIndex,
@ -361,7 +362,6 @@ void BauSystemB::nextRestartState()
void BauSystemB::systemNetworkParameterReadIndication(Priority priority, HopCountType hopType, uint16_t objectType,
uint16_t propertyId, uint8_t* testInfo, uint16_t testInfoLength)
{
uint8_t knxSerialNumber[6];
uint8_t operand;
popByte(operand, testInfo + 1); // First byte (+ 0) contains only 4 reserved bits (0)
@ -374,10 +374,8 @@ void BauSystemB::systemNetworkParameterReadIndication(Priority priority, HopCoun
if (_deviceObj.progMode() && (objectType == OT_DEVICE) && (propertyId == PID_SERIAL_NUMBER))
{
// Send reply. testResult data is KNX serial number
pushWord(_deviceObj.manufacturerId(), &knxSerialNumber[0]);
pushInt(_deviceObj.bauNumber(), &knxSerialNumber[2]);
_appLayer.systemNetworkParameterReadResponse(priority, hopType, objectType, propertyId,
testInfo, testInfoLength, knxSerialNumber, sizeof(knxSerialNumber));
testInfo, testInfoLength, (uint8_t*) _deviceObj.knxSerialNumber(), 6);
}
break;
@ -391,3 +389,40 @@ void BauSystemB::systemNetworkParameterReadIndication(Priority priority, HopCoun
break;
}
}
void BauSystemB::propertyValueRead(ObjectType objectType, uint8_t objectInstance, uint8_t propertyId,
uint32_t &numberOfElements, uint16_t startIndex,
uint8_t **data, uint32_t &length)
{
uint32_t size = 0;
uint32_t elementCount = numberOfElements;
InterfaceObject* obj = getInterfaceObject(objectType, objectInstance);
if (obj)
{
uint8_t elementSize = obj->propertySize((PropertyID)propertyId);
size = elementSize * numberOfElements;
*data = new uint8_t [size];
obj->readProperty((PropertyID)propertyId, startIndex, elementCount, *data);
}
else
{
elementCount = 0;
*data = nullptr;
}
numberOfElements = elementCount;
length = size;
}
void BauSystemB::propertyValueWrite(ObjectType objectType, uint8_t objectInstance, uint8_t propertyId,
uint32_t &numberOfElements, uint16_t startIndex,
uint8_t* data, uint32_t length)
{
InterfaceObject* obj = getInterfaceObject(objectType, objectInstance);
if(obj)
obj->writeProperty((PropertyID)propertyId, startIndex, data, numberOfElements);
else
numberOfElements = 0;
}

View File

@ -28,6 +28,13 @@ class BauSystemB : protected BusAccessUnit
void writeMemory();
void addSaveRestore(SaveRestore* obj);
bool restartRequest(uint16_t asap);
void propertyValueRead(ObjectType objectType, uint8_t objectInstance, uint8_t propertyId,
uint32_t &numberOfElements, uint16_t startIndex,
uint8_t **data, uint32_t &length) override;
void propertyValueWrite(ObjectType objectType, uint8_t objectInstance, uint8_t propertyId,
uint32_t &numberOfElements, uint16_t startIndex,
uint8_t* data, uint32_t length) override;
protected:
virtual DataLinkLayer& dataLinkLayer() = 0;
@ -59,10 +66,11 @@ class BauSystemB : protected BusAccessUnit
void groupValueWriteIndication(uint16_t asap, Priority priority, HopCountType hopType,
uint8_t* data, uint8_t dataLength) override;
void systemNetworkParameterReadIndication(Priority priority, HopCountType hopType, uint16_t objectType,
uint16_t propertyId, uint8_t* testInfo, uint16_t testinfoLength);
uint16_t propertyId, uint8_t* testInfo, uint16_t testinfoLength) override;
void connectConfirm(uint16_t tsap) override;
virtual InterfaceObject* getInterfaceObject(uint8_t idx) = 0;
virtual InterfaceObject* getInterfaceObject(ObjectType objectType, uint8_t objectInstance) = 0;
void sendNextGroupTelegram();
void updateGroupObject(GroupObject& go, uint8_t* data, uint8_t length);
void nextRestartState();

View File

@ -69,9 +69,9 @@ Control Field 1
*/
CemiFrame::CemiFrame(uint8_t* data, uint16_t length)
: _npdu(data + NPDU_LPDU_DIFF, *this),
_tpdu(data + TPDU_LPDU_DIFF, *this),
_apdu(data + APDU_LPDU_DIFF, *this)
: _npdu(data + data[1] + NPDU_LPDU_DIFF, *this),
_tpdu(data + data[1] + TPDU_LPDU_DIFF, *this),
_apdu(data + data[1] + APDU_LPDU_DIFF, *this)
{
_data = data;
_ctrl1 = data + data[1] + CEMI_HEADER_SIZE;
@ -184,6 +184,16 @@ void CemiFrame::fillTelegramRF(uint8_t* data)
//printHex("cEMI_fill: ", &data[0], len);
}
uint8_t* CemiFrame::data()
{
return _data;
}
uint16_t CemiFrame::dataLength()
{
return _length;
}
uint8_t CemiFrame::calcCrcTP(uint8_t * buffer, uint16_t len)
{
uint8_t crc = 0xFF;
@ -249,6 +259,17 @@ void CemiFrame::ack(AckType value)
_ctrl1[0] |= value;
}
Confirm CemiFrame::confirm() const
{
return (Confirm)(_ctrl1[0] & ConfirmError);
}
void CemiFrame::confirm(Confirm value)
{
_ctrl1[0] &= ~ConfirmError;
_ctrl1[0] |= value;
}
AddressType CemiFrame::addressType() const
{
return (AddressType)(_ctrl1[1] & GroupAddress);

View File

@ -32,6 +32,8 @@ class CemiFrame
void fillTelegramTP(uint8_t* data);
uint16_t telegramLengthtRF() const;
void fillTelegramRF(uint8_t* data);
uint8_t* data();
uint16_t dataLength();
FrameFormat frameType() const;
void frameType(FrameFormat value);
@ -43,6 +45,8 @@ class CemiFrame
void priority(Priority value);
AckType ack() const;
void ack(AckType value);
Confirm confirm() const;
void confirm(Confirm value);
AddressType addressType() const;
void addressType(AddressType value);
uint8_t hopCount() const;
@ -76,8 +80,8 @@ class CemiFrame
APDU _apdu;
uint16_t _length = 0; // only set if created from byte array
// nly for RF medium
// only for RF medium
uint8_t* _rfSerialOrDoA = 0;
uint8_t _rfInfo = 0;
uint8_t _rfLfn = 0; // RF Data Link layer frame number
uint8_t _rfLfn = 0xFF; // RF Data Link layer frame number
};

348
src/knx/cemi_server.cpp Normal file
View File

@ -0,0 +1,348 @@
#ifdef USE_CEMI_SERVER
#include "cemi_server.h"
#include "cemi_frame.h"
#include "bau_systemB.h"
#include "usb_tunnel_interface.h"
#include "data_link_layer.h"
#include "string.h"
#include "bits.h"
#include <stdio.h>
CemiServer::CemiServer(BauSystemB& bau)
: _bau(bau),
_usbTunnelInterface(*this,
_bau.deviceObject().maskVersion(),
_bau.deviceObject().manufacturerId())
{
// The cEMI server will hand out the device address + 1 to the cEMI client (e.g. ETS),
// so that the device and the cEMI client/server connection(tunnel) can operate simultaneously.
_clientAddress = _bau.deviceObject().induvidualAddress() + 1;
}
void CemiServer::dataLinkLayer(DataLinkLayer& layer)
{
_dataLinkLayer = &layer;
}
uint16_t CemiServer::clientAddress() const
{
return _clientAddress;
}
void CemiServer::clientAddress(uint16_t value)
{
_clientAddress = value;
}
void CemiServer::dataConfirmationToTunnel(CemiFrame& frame)
{
MessageCode backupMsgCode = frame.messageCode();
frame.messageCode(L_data_con);
print("L_data_con: src: ");
print(frame.sourceAddress(), HEX);
print(" dst: ");
print(frame.destinationAddress(), HEX);
printHex(" frame: ", frame.data(), frame.dataLength());
_usbTunnelInterface.sendCemiFrame(frame);
frame.messageCode(backupMsgCode);
}
void CemiServer::dataIndicationToTunnel(CemiFrame& frame)
{
#if MEDIUM_TYPE == 2
uint8_t data[frame.dataLength() + 10];
data[0] = L_data_ind; // Message Code
data[1] = 0x0A; // Total additional info length
data[2] = 0x02; // RF add. info: type
data[3] = 0x08; // RF add. info: length
data[4] = frame.rfInfo(); // RF add. info: info field (batt ok, bidir)
pushByteArray(frame.rfSerialOrDoA(), 6, &data[5]); // RF add. info:Serial or Domain Address
data[11] = frame.rfLfn(); // RF add. info: link layer frame number
memcpy(&data[12], &((frame.data())[2]), frame.dataLength() - 2);
#else
uint8_t data[frame.dataLength()];
memcpy(&data[0], frame.data(), frame.dataLength());
#endif
CemiFrame tmpFrame(data, sizeof(data));
print("L_data_ind: src: ");
print(tmpFrame.sourceAddress(), HEX);
print(" dst: ");
print(tmpFrame.destinationAddress(), HEX);
printHex(" frame: ", tmpFrame.data(), tmpFrame.dataLength());
tmpFrame.apdu().type();
_usbTunnelInterface.sendCemiFrame(tmpFrame);
}
void CemiServer::frameReceived(CemiFrame& frame)
{
switch(frame.messageCode())
{
case L_data_req:
{
// Fill in the cEMI client address if the client sets
// source address to 0.
if(frame.sourceAddress() == 0x0000)
{
frame.sourceAddress(_clientAddress);
}
#if MEDIUM_TYPE == 2
// Check if we have additional info for RF
if (((frame.data())[1] == 0x0A) && // Additional info total length: we only handle one additional info of type RF
((frame.data())[2] == 0x02) && // Additional info type: RF
((frame.data())[3] == 0x08) ) // Additional info length of type RF: 8 bytes (fixed)
{
frame.rfInfo((frame.data())[4]);
// Use the values provided in the RF additonal info
if ( ((frame.data())[5] != 0x00) || ((frame.data())[6] != 0x00) || ((frame.data())[7] != 0x00) ||
((frame.data())[8] != 0x00) || ((frame.data())[9] != 0x00) || ((frame.data())[10] != 0x00) )
{
frame.rfSerialOrDoA(&((frame.data())[5]));
} // else leave the nullptr as it is
frame.rfLfn((frame.data())[11]);
}
// If the cEMI client does not provide a link layer frame number (LFN),
// we use our own counter.
// Note: There is another link layer frame number counter inside the RF data link layer class!
// That counter is solely for the local application!
// If we set a LFN here, the data link layer counter is NOT used!
if (frame.rfLfn() == 0xFF)
{
// Set Data Link Layer Frame Number
frame.rfLfn(_frameNumber);
// Link Layer frame number counts 0..7
_frameNumber = (_frameNumber + 1) & 0x7;
}
#endif
print("L_data_req: src: ");
print(frame.sourceAddress(), HEX);
print(" dst: ");
print(frame.destinationAddress(), HEX);
printHex(" frame: ", frame.data(), frame.dataLength());
_dataLinkLayer->dataRequestFromTunnel(frame);
break;
}
case M_PropRead_req:
{
print("M_PropRead_req: ");
uint16_t objectType;
popWord(objectType, &frame.data()[1]);
uint8_t objectInstance = frame.data()[3];
uint8_t propertyId = frame.data()[4];
uint32_t numberOfElements = frame.data()[5] >> 4;
uint16_t startIndex = frame.data()[6] | ((frame.data()[5]&0x0F)<<8);
uint8_t* data = nullptr;
uint32_t dataSize = 0;
print("ObjType: ");
print(objectType, DEC);
print(" ObjInst: ");
print(objectInstance, DEC);
print(" PropId: ");
print(propertyId, DEC);
print(" NoE: ");
print(numberOfElements, DEC);
print(" startIdx: ");
print(startIndex, DEC);
// propertyValueRead() allocates memory for the data! Needs to be deleted again!
_bau.propertyValueRead((ObjectType)objectType, objectInstance, propertyId, numberOfElements, startIndex, &data, dataSize);
// Patch result for device address in device object
// The cEMI server will hand out the device address + 1 to the cEMI client (e.g. ETS),
// so that the device and the cEMI client/server connection(tunnel) can operate simultaneously.
// KNX IP Interfaces which offer multiple simultaneous tunnel connections seem to operate the same way.
// Each tunnel has its own cEMI client address which is based on the main device address.
if (((ObjectType) objectType == OT_DEVICE) &&
(propertyId == PID_DEVICE_ADDR) &&
(numberOfElements == 1))
{
data[0] = (uint8_t) (_clientAddress & 0xFF);
}
else if (((ObjectType) objectType == OT_DEVICE) &&
(propertyId == PID_SUBNET_ADDR) &&
(numberOfElements == 1))
{
data[0] = (uint8_t) ((_clientAddress >> 8) & 0xFF);
}
if (data && dataSize && numberOfElements)
{
printHex(" <- data: ", data, dataSize);
println("");
// Prepare positive response
uint8_t responseData[7 + dataSize];
memcpy(responseData, frame.data(), 7);
memcpy(&responseData[7], data, dataSize);
CemiFrame responseFrame(responseData, sizeof(responseData));
responseFrame.messageCode(M_PropRead_con);
_usbTunnelInterface.sendCemiFrame(responseFrame);
delete[] data;
}
else
{
// Prepare negative response
uint8_t responseData[7 + 1];
memcpy(responseData, frame.data(), sizeof(responseData));
responseData[7] = Void_DP; // Set cEMI error code
responseData[5] = 0; // Set Number of elements to zero
printHex(" <- error: ", &responseData[7], 1);
println("");
CemiFrame responseFrame(responseData, sizeof(responseData));
responseFrame.messageCode(M_PropRead_con);
_usbTunnelInterface.sendCemiFrame(responseFrame);
}
break;
}
case M_PropWrite_req:
{
print("M_PropWrite_req: ");
uint16_t objectType;
popWord(objectType, &frame.data()[1]);
uint8_t objectInstance = frame.data()[3];
uint8_t propertyId = frame.data()[4];
uint32_t numberOfElements = frame.data()[5] >> 4;
uint16_t startIndex = frame.data()[6] | ((frame.data()[5]&0x0F)<<8);
uint8_t* requestData = &frame.data()[7];
uint32_t requestDataSize = frame.dataLength() - 7;
print("ObjType: ");
print(objectType, DEC);
print(" ObjInst: ");
print(objectInstance, DEC);
print(" PropId: ");
print(propertyId, DEC);
print(" NoE: ");
print(numberOfElements, DEC);
print(" startIdx: ");
print(startIndex, DEC);
printHex(" -> data: ", requestData, requestDataSize);
// Patch request for device address in device object
if (((ObjectType) objectType == OT_DEVICE) &&
(propertyId == PID_DEVICE_ADDR) &&
(numberOfElements == 1))
{
// Temporarily store new cEMI client address in memory
// We also be sent back if the client requests it again
_clientAddress = (_clientAddress & 0xFF00) | requestData[0];
print("cEMI client address: ");
println(_clientAddress, HEX);
}
else if (((ObjectType) objectType == OT_DEVICE) &&
(propertyId == PID_SUBNET_ADDR) &&
(numberOfElements == 1))
{
// Temporarily store new cEMI client address in memory
// We also be sent back if the client requests it again
_clientAddress = (_clientAddress & 0x00FF) | (requestData[0] << 8);
print("cEMI client address: ");
println(_clientAddress, HEX);
}
else
{
_bau.propertyValueWrite((ObjectType)objectType, objectInstance, propertyId, numberOfElements, startIndex, requestData, requestDataSize);
}
if (numberOfElements)
{
// Prepare positive response
uint8_t responseData[7];
memcpy(responseData, frame.data(), sizeof(responseData));
println(" <- no error");
CemiFrame responseFrame(responseData, sizeof(responseData));
responseFrame.messageCode(M_PropWrite_con);
_usbTunnelInterface.sendCemiFrame(responseFrame);
}
else
{
// Prepare negative response
uint8_t responseData[7 + 1];
memcpy(responseData, frame.data(), sizeof(responseData));
responseData[7] = Illegal_Command; // Set cEMI error code
responseData[5] = 0; // Set Number of elements to zero
printHex(" <- error: ", &responseData[7], 1);
println("");
CemiFrame responseFrame(responseData, sizeof(responseData));
responseFrame.messageCode(M_PropWrite_con);
_usbTunnelInterface.sendCemiFrame(responseFrame);
}
break;
}
case M_FuncPropCommand_req:
{
println("M_FuncPropCommand_req not implemented");
break;
}
case M_FuncPropStateRead_req:
{
println("M_FuncPropStateRead_req not implemented");
break;
}
case M_Reset_req:
{
println("M_Reset_req: sending M_Reset_ind");
// A real device reset does not work for USB or KNXNET/IP.
// Thus, M_Reset_ind is NOT mandatory for USB and KNXNET/IP.
// We just save all data to the EEPROM
_bau.writeMemory();
// Prepare response
uint8_t responseData[1];
CemiFrame responseFrame(responseData, sizeof(responseData));
responseFrame.messageCode(M_Reset_ind);
_usbTunnelInterface.sendCemiFrame(responseFrame);
break;
}
// we should never receive these: server -> client
case L_data_con:
case L_data_ind:
case M_PropInfo_ind:
case M_PropRead_con:
case M_PropWrite_con:
case M_FuncPropCommand_con:
//case M_FuncPropStateRead_con: // same value as M_FuncPropCommand_con
case M_Reset_ind:
default:
break;
}
}
void CemiServer::loop()
{
_usbTunnelInterface.loop();
}
#endif

50
src/knx/cemi_server.h Normal file
View File

@ -0,0 +1,50 @@
#pragma once
#include <stdint.h>
#include "knx_types.h"
#include "usb_tunnel_interface.h"
class BauSystemB;
class DataLinkLayer;
class CemiFrame;
/**
* This is an implementation of the cEMI server as specified in @cite knx:3/6/3.
* Overview on page 57.
* It provides methods for the BusAccessUnit to do different things and translates this
* call to an cEMI frame and calls the correct method of the data link layer.
* It also takes calls from data link layer, decodes the submitted cEMI frames and calls the corresponding
* methods of the BusAccessUnit class.
*/
class CemiServer
{
public:
/**
* The constructor.
* @param bau methods are called here depending of the content of the APDU
*/
CemiServer(BauSystemB& bau);
void dataLinkLayer(DataLinkLayer& layer);
// from data link layer
// Only L_Data service
void dataIndicationToTunnel(CemiFrame& frame);
void dataConfirmationToTunnel(CemiFrame& frame);
// From tunnel interface
void frameReceived(CemiFrame& frame);
uint16_t clientAddress() const;
void clientAddress(uint16_t value);
void loop();
private:
uint16_t _clientAddress;
uint8_t _frameNumber = 0;
DataLinkLayer* _dataLinkLayer;
BauSystemB& _bau;
UsbTunnelInterface _usbTunnelInterface;
};

View File

@ -0,0 +1,115 @@
#ifdef USE_CEMI_SERVER
#include <cstring>
#include "cemi_server_object.h"
#include "bits.h"
void CemiServerObject::readProperty(PropertyID propertyId, uint32_t start, uint32_t& count, uint8_t* data)
{
switch (propertyId)
{
case PID_OBJECT_TYPE:
pushWord(OT_CEMI_SERVER, data);
break;
case PID_MEDIUM_TYPE: // PDT_BITSET16
#if MEDIUM_TYPE==0
pushWord(2, data); // TP1 supported
#elif MEDIUM_TYPE==2
pushWord(16, data); // RF supported
#elif MEDIUM_TYPE==5
pushWord(32, data); // IP supported
#endif
break;
case PID_COMM_MODE: // PDT_ENUM8
// See KNX spec. cEMI 3/6/3 p.110
data[0] = 0x00; // Only Data Link Layer mode supported and we do not allow switching (read-only)
break;
case PID_COMM_MODES_SUPPORTED:
data[0] = 0x00;
data[1] = 0x01;
break;
case PID_MEDIUM_AVAILABILITY: // PDT_BITSET16
#if MEDIUM_TYPE==0
pushWord(2, data); // TP1 active
#elif MEDIUM_TYPE==2
pushWord(16, data); // RF active
#elif MEDIUM_TYPE==5
pushWord(32, data); // IP active
#endif
break;
case PID_ADD_INFO_TYPES: // PDT_ENUM8[]
pushByteArray((uint8_t*)_addInfoTypesTable, sizeof(_addInfoTypesTable), data);
break;
// Not supported yet
break;
default:
count = 0;
}
}
void CemiServerObject::writeProperty(PropertyID id, uint32_t start, uint8_t* data, uint32_t& count)
{
switch (id)
{
case PID_COMM_MODE:
//_commMode = data[0]; // TODO: only Data Link Layer supported for now
// Property is also marked as read-only, normally it is read/write.
break;
default:
count = 0;
break;
}
}
uint8_t CemiServerObject::propertySize(PropertyID id)
{
switch (id)
{
case PID_COMM_MODE:
return 1;
case PID_OBJECT_TYPE:
case PID_MEDIUM_TYPE:
case PID_MEDIUM_AVAILABILITY:
case PID_COMM_MODES_SUPPORTED:
return 2;
case PID_ADD_INFO_TYPES:
return sizeof(_addInfoTypesTable);
default:
break;
}
return 0;
}
uint8_t* CemiServerObject::save(uint8_t* buffer)
{
return buffer;
}
uint8_t* CemiServerObject::restore(uint8_t* buffer)
{
return buffer;
}
static PropertyDescription _propertyDescriptions[] =
{
{ PID_OBJECT_TYPE, false, PDT_UNSIGNED_INT, 1, ReadLv3 | WriteLv0 },
{ PID_MEDIUM_TYPE, false, PDT_BITSET16, 1, ReadLv3 | WriteLv0 },
{ PID_COMM_MODE, false, PDT_ENUM8, 1, ReadLv3 | WriteLv0 },
{ PID_COMM_MODES_SUPPORTED, false, PDT_BITSET16, 1, ReadLv3 | WriteLv0 },
{ PID_MEDIUM_AVAILABILITY, false, PDT_BITSET16, 1, ReadLv3 | WriteLv0 },
{ PID_ADD_INFO_TYPES, false, PDT_ENUM8, 1, ReadLv3 | WriteLv0 }
};
static uint8_t _propertyCount = sizeof(_propertyDescriptions) / sizeof(PropertyDescription);
uint8_t CemiServerObject::propertyCount()
{
return _propertyCount;
}
PropertyDescription* CemiServerObject::propertyDescriptions()
{
return _propertyDescriptions;
}
#endif

View File

@ -0,0 +1,24 @@
#pragma once
#include "interface_object.h"
class CemiServerObject: public InterfaceObject
{
public:
void readProperty(PropertyID id, uint32_t start, uint32_t& count, uint8_t* data) override;
void writeProperty(PropertyID id, uint32_t start, uint8_t* data, uint32_t& count) override;
uint8_t propertySize(PropertyID id) override;
uint8_t* save(uint8_t* buffer);
uint8_t* restore(uint8_t* buffer);
void readPropertyDescription(uint8_t propertyId, uint8_t& propertyIndex, bool& writeEnable, uint8_t& type, uint16_t& numberOfElements, uint8_t& access);
ObjectType objectType() { return OT_CEMI_SERVER; }
protected:
uint8_t propertyCount();
PropertyDescription* propertyDescriptions();
private:
// cEMI additional info types supported by this cEMI server: only 0x02 (RF Control Octet and Serial Number or DoA)
uint8_t _addInfoTypesTable[1] = { 0x02 };
uint8_t _commMode = 0x00;
};

View File

@ -4,7 +4,7 @@
#include "platform.h"
#include "device_object.h"
#include "address_table_object.h"
#include "cemi_server.h"
DataLinkLayer::DataLinkLayer(DeviceObject& devObj, AddressTableObject& addrTab,
NetworkLayer& layer, Platform& platform) :
@ -12,6 +12,27 @@ DataLinkLayer::DataLinkLayer(DeviceObject& devObj, AddressTableObject& addrTab,
{
}
#ifdef USE_CEMI_SERVER
void DataLinkLayer::cemiServer(CemiServer& cemiServer)
{
_cemiServer = &cemiServer;
}
void DataLinkLayer::dataRequestFromTunnel(CemiFrame& frame)
{
_cemiServer->dataConfirmationToTunnel(frame);
frame.messageCode(L_data_ind);
// Send to local stack
frameRecieved(frame);
// Send to KNX medium
sendFrame(frame);
}
#endif
void DataLinkLayer::dataRequest(AckType ack, AddressType addrType, uint16_t destinationAddr, FrameFormat format, Priority priority, NPDU& npdu)
{
// Normal data requests and broadcasts will always be transmitted as (domain) broadcast with domain address for open media (e.g. RF medium)
@ -29,6 +50,9 @@ void DataLinkLayer::systemBroadcastRequest(AckType ack, FrameFormat format, Prio
void DataLinkLayer::dataConReceived(CemiFrame& frame, bool success)
{
MessageCode backupMsgCode = frame.messageCode();
frame.messageCode(L_data_con);
frame.confirm(success ? ConfirmNoError : ConfirmError);
AckType ack = frame.ack();
AddressType addrType = frame.addressType();
uint16_t destination = frame.destinationAddress();
@ -38,6 +62,16 @@ void DataLinkLayer::dataConReceived(CemiFrame& frame, bool success)
NPDU& npdu = frame.npdu();
SystemBroadcast systemBroadcast = frame.systemBroadcast();
#ifdef USE_CEMI_SERVER
// if the confirmation was caused by a tunnel request then
// do not send it to the local stack
if (frame.sourceAddress() == _cemiServer->clientAddress())
{
// Stop processing here and do NOT send it the local network layer
return;
}
#endif
if (addrType == GroupAddress && destination == 0)
if (systemBroadcast == SysBroadcast)
_networkLayer.systemBroadcastConfirm(ack, type, priority, source, npdu, success);
@ -46,8 +80,9 @@ void DataLinkLayer::dataConReceived(CemiFrame& frame, bool success)
else
_networkLayer.dataConfirm(ack, addrType, destination, type, priority, source, npdu, success);
frame.messageCode(backupMsgCode);
}
void DataLinkLayer::frameRecieved(CemiFrame& frame)
{
AckType ack = frame.ack();
@ -59,7 +94,15 @@ void DataLinkLayer::frameRecieved(CemiFrame& frame)
NPDU& npdu = frame.npdu();
uint16_t ownAddr = _deviceObject.induvidualAddress();
SystemBroadcast systemBroadcast = frame.systemBroadcast();
#ifdef USE_CEMI_SERVER
// Do not send our own message back to the tunnel
if (frame.sourceAddress() != _cemiServer->clientAddress())
{
_cemiServer->dataIndicationToTunnel(frame);
}
#endif
if (source == ownAddr)
_deviceObject.induvidualAddressDuplication(true);
@ -117,7 +160,25 @@ bool DataLinkLayer::sendTelegram(NPDU & npdu, AckType ack, uint16_t destinationA
// frame.apdu().printPDU();
// }
return sendFrame(frame);
// The data link layer might be an open media link layer
// and will setup rfSerialOrDoA, rfInfo and rfLfn that we also
// have to send through the cEMI server tunnel
// Thus, reuse the modified cEMI frame as "frame" is only passed by reference here!
bool success = sendFrame(frame);
#ifdef USE_CEMI_SERVER
CemiFrame tmpFrame(frame.data(), frame.totalLenght());
// We can just copy the pointer for rfSerialOrDoA as sendFrame() sets
// a pointer to const uint8_t data in either device object (serial) or
// RF medium object (domain address)
tmpFrame.rfSerialOrDoA(frame.rfSerialOrDoA());
tmpFrame.rfInfo(frame.rfInfo());
tmpFrame.rfLfn(frame.rfLfn());
tmpFrame.confirm(ConfirmNoError);
_cemiServer->dataIndicationToTunnel(tmpFrame);
#endif
return success;
}
uint8_t* DataLinkLayer::frameData(CemiFrame& frame)

View File

@ -5,6 +5,7 @@
#include "address_table_object.h"
#include "knx_types.h"
#include "network_layer.h"
#include "cemi_server.h"
class DataLinkLayer
{
@ -12,6 +13,12 @@ class DataLinkLayer
DataLinkLayer(DeviceObject& devObj, AddressTableObject& addrTab, NetworkLayer& layer,
Platform& platform);
#ifdef USE_CEMI_SERVER
// from tunnel
void cemiServer(CemiServer& cemiServer);
void dataRequestFromTunnel(CemiFrame& frame);
#endif
// from network layer
void dataRequest(AckType ack, AddressType addrType, uint16_t destinationAddr, FrameFormat format,
Priority priority, NPDU& npdu);
@ -30,4 +37,7 @@ class DataLinkLayer
AddressTableObject& _groupAddressTable;
NetworkLayer& _networkLayer;
Platform& _platform;
#ifdef USE_CEMI_SERVER
CemiServer* _cemiServer;
#endif
};

View File

@ -10,11 +10,10 @@ void DeviceObject::readProperty(PropertyID propertyId, uint32_t start, uint32_t&
pushWord(OT_DEVICE, data);
break;
case PID_SERIAL_NUMBER:
pushWord(_manufacturerId, data);
pushInt(_bauNumber, data + 2);
pushByteArray((uint8_t*)_knxSerialNumber, 6, data);
break;
case PID_MANUFACTURER_ID:
pushWord(_manufacturerId, data);
pushByteArray(&_knxSerialNumber[0], 2, data);
break;
case PID_DEVICE_CONTROL:
*data = _deviceControl;
@ -52,12 +51,15 @@ void DeviceObject::readProperty(PropertyID propertyId, uint32_t start, uint32_t&
case PID_DEVICE_DESCRIPTOR:
pushWord(_maskVersion, data);
break;
case PID_RF_DOMAIN_ADDRESS_CEMI_SERVER:
pushByteArray((uint8_t*)_rfDomainAddress, 6, data);
break;
default:
count = 0;
}
}
void DeviceObject::writeProperty(PropertyID id, uint8_t start, uint8_t* data, uint8_t count)
void DeviceObject::writeProperty(PropertyID id, uint32_t start, uint8_t* data, uint32_t& count)
{
switch (id)
{
@ -70,6 +72,18 @@ void DeviceObject::writeProperty(PropertyID id, uint8_t start, uint8_t* data, ui
case PID_PROG_MODE:
_prgMode = data[0];
break;
case PID_RF_DOMAIN_ADDRESS_CEMI_SERVER:
memcpy(_rfDomainAddress, data, propertySize(PID_RF_DOMAIN_ADDRESS_CEMI_SERVER));
break;
case PID_SUBNET_ADDR:
_ownAddress = (data[0] << 8) | (_ownAddress & 0xff);
break;
case PID_DEVICE_ADDR:
_ownAddress = data[0] | (_ownAddress & 0xff00);
break;
default:
count = 0;
break;
}
}
@ -93,6 +107,7 @@ uint8_t DeviceObject::propertySize(PropertyID id)
return 4;
case PID_SERIAL_NUMBER:
case PID_HARDWARE_TYPE:
case PID_RF_DOMAIN_ADDRESS_CEMI_SERVER:
return 6;
case PID_ORDER_INFO:
return 10;
@ -105,6 +120,7 @@ uint8_t* DeviceObject::save(uint8_t* buffer)
buffer = pushByte(_deviceControl, buffer);
buffer = pushByte(_routingCount, buffer);
buffer = pushWord(_ownAddress, buffer);
buffer = pushByteArray((uint8_t*)_rfDomainAddress, 6, buffer);
return buffer;
}
@ -113,6 +129,7 @@ uint8_t* DeviceObject::restore(uint8_t* buffer)
buffer = popByte(_deviceControl, buffer);
buffer = popByte(_routingCount, buffer);
buffer = popWord(_ownAddress, buffer);
buffer = popByteArray((uint8_t*)_rfDomainAddress, 6, buffer);
_prgMode = 0;
return buffer;
}
@ -200,22 +217,36 @@ void DeviceObject::progMode(bool value)
uint16_t DeviceObject::manufacturerId()
{
return _manufacturerId;
uint16_t manufacturerId;
popWord(manufacturerId, &_knxSerialNumber[0]);
return manufacturerId;
}
void DeviceObject::manufacturerId(uint16_t value)
{
_manufacturerId = value;
pushWord(value, &_knxSerialNumber[0]);
}
uint32_t DeviceObject::bauNumber()
{
return _bauNumber;
uint32_t bauNumber;
popInt(bauNumber, &_knxSerialNumber[2]);
return bauNumber;
}
void DeviceObject::bauNumber(uint32_t value)
{
_bauNumber = value;
pushInt(value, &_knxSerialNumber[2]);
}
const uint8_t* DeviceObject::knxSerialNumber()
{
return _knxSerialNumber;
}
void DeviceObject::knxSerialNumber(const uint8_t* value)
{
pushByteArray(value, 6, _knxSerialNumber);
}
const char* DeviceObject::orderNumber()
@ -278,6 +309,16 @@ void DeviceObject::ifObj(const uint32_t* value)
_ifObjs = value;
}
uint8_t* DeviceObject::rfDomainAddress()
{
return _rfDomainAddress;
}
void DeviceObject::rfDomainAddress(uint8_t* value)
{
pushByteArray(value, 6, _rfDomainAddress);
}
static PropertyDescription _propertyDescriptions[] =
{
{ PID_OBJECT_TYPE, false, PDT_UNSIGNED_INT, 1, ReadLv3 | WriteLv0 },

View File

@ -6,12 +6,12 @@ class DeviceObject: public InterfaceObject
{
public:
void readProperty(PropertyID id, uint32_t start, uint32_t& count, uint8_t* data);
void writeProperty(PropertyID id, uint8_t start, uint8_t* data, uint8_t count);
void writeProperty(PropertyID id, uint32_t start, uint8_t* data, uint32_t& count);
uint8_t propertySize(PropertyID id);
uint8_t* save(uint8_t* buffer);
uint8_t* restore(uint8_t* buffer);
void readPropertyDescription(uint8_t propertyId, uint8_t& propertyIndex, bool& writeEnable, uint8_t& type, uint16_t& numberOfElements, uint8_t& access);
ObjectType objectType() { return OT_DEVICE; }
uint16_t induvidualAddress();
void induvidualAddress(uint16_t value);
@ -29,6 +29,8 @@ public:
void manufacturerId(uint16_t value);
uint32_t bauNumber();
void bauNumber(uint32_t value);
const uint8_t* knxSerialNumber();
void knxSerialNumber(const uint8_t* value);
const char* orderNumber();
void orderNumber(const char* value);
const uint8_t* hardwareType();
@ -41,6 +43,8 @@ public:
void maxApduLength(uint16_t value);
const uint32_t* ifObj();
void ifObj(const uint32_t* value);
uint8_t* rfDomainAddress();
void rfDomainAddress(uint8_t* value);
protected:
uint8_t propertyCount();
PropertyDescription* propertyDescriptions();
@ -49,12 +53,12 @@ private:
uint8_t _routingCount = 0;
uint8_t _prgMode = 0;
uint16_t _ownAddress = 0;
uint16_t _manufacturerId = 0xfa; //Default to KNXA
uint32_t _bauNumber = 0;
uint8_t _knxSerialNumber[6] = { 0x00, 0xFA, 0x00, 0x00, 0x00, 0x00 }; //Default to KNXA (0xFA)
char _orderNumber[10] = "";
uint8_t _hardwareType[6] = { 0, 0, 0, 0, 0, 0};
uint16_t _version = 0;
uint16_t _maskVersion = 0x0000;
uint16_t _maxApduLength = 254;
const uint32_t* _ifObjs;
uint8_t _rfDomainAddress[6] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
};

View File

@ -11,6 +11,7 @@ class GroupObjectTableObject : public TableObject
GroupObjectTableObject(Platform& platform);
virtual ~GroupObjectTableObject();
void readProperty(PropertyID id, uint32_t start, uint32_t& count, uint8_t* data);
ObjectType objectType() { return OT_GRP_OBJ_TABLE; }
uint16_t entryCount();
GroupObject& get(uint16_t asap);
GroupObject& nextUpdatedObject(bool& valid);

View File

@ -48,11 +48,16 @@ void InterfaceObject::readPropertyDescription(uint8_t& propertyId, uint8_t& prop
void InterfaceObject::readProperty(PropertyID id, uint32_t start, uint32_t &count, uint8_t *data)
{
// Set number of elements to zero as we are in the end of the call chain
// Nobody processed the property before.
count = 0;
}
void InterfaceObject::writeProperty(PropertyID id, uint8_t start, uint8_t *data, uint8_t count)
void InterfaceObject::writeProperty(PropertyID id, uint32_t start, uint8_t *data, uint32_t &count)
{
// Set number of elements to zero as we are in the end of the call chain
// Nobody processed the property before.
count = 0;
}
uint8_t InterfaceObject::propertySize(PropertyID id)
@ -68,4 +73,5 @@ uint8_t InterfaceObject::propertyCount()
PropertyDescription* InterfaceObject::propertyDescriptions()
{
return nullptr;
}
}

View File

@ -84,11 +84,12 @@ class InterfaceObject : public SaveRestore
*
* @param start (for properties with multiple values) at which element should we start
*
* @param count how many values should be written.
* @param[in, out] count how many values should be written. If there is a problem (e.g. property does not exist)
* this value is set to 0.
*
* @param data The data that should be written.
* @param[in] data The data that should be written.
*/
virtual void writeProperty(PropertyID id, uint8_t start, uint8_t* data, uint8_t count);
virtual void writeProperty(PropertyID id, uint32_t start, uint8_t *data, uint32_t &count);
/**
* Gets the size of of property in bytes.
*
@ -118,6 +119,13 @@ class InterfaceObject : public SaveRestore
void readPropertyDescription(uint8_t& propertyId, uint8_t& propertyIndex, bool& writeEnable, uint8_t& type, uint16_t& numberOfElements, uint8_t& access);
/**
* Gets object type.
*
* @returns object type
*/
virtual ObjectType objectType() = 0;
protected:
/**
* Returns the number of properties the interface object has.

View File

@ -79,7 +79,7 @@ void IpParameterObject::readProperty(PropertyID propertyId, uint32_t start, uint
}
}
void IpParameterObject::writeProperty(PropertyID id, uint8_t start, uint8_t* data, uint8_t count)
void IpParameterObject::writeProperty(PropertyID id, uint32_t start, uint8_t* data, uint32_t& count)
{
switch (id)
{
@ -114,6 +114,9 @@ void IpParameterObject::writeProperty(PropertyID id, uint8_t start, uint8_t* dat
for (uint8_t i = start; i < start + count; i++)
_friendlyName[i-1] = data[i - start];
break;
default:
count = 0;
break;
}
}

View File

@ -9,8 +9,9 @@ class IpParameterObject : public InterfaceObject
public:
IpParameterObject(DeviceObject& deviceObject, Platform& platform);
void readProperty(PropertyID id, uint32_t start, uint32_t& count, uint8_t* data);
void writeProperty(PropertyID id, uint8_t start, uint8_t* data, uint8_t count);
void writeProperty(PropertyID id, uint32_t start, uint8_t* data, uint32_t& count);
uint8_t propertySize(PropertyID id);
ObjectType objectType() { return OT_IP_PARAMETER; }
uint8_t* save(uint8_t* buffer);
uint8_t* restore(uint8_t* buffer);

View File

@ -28,7 +28,42 @@ enum AddressType
enum MessageCode
{
// L_Data services
L_data_req = 0x11,
L_data_con = 0x2E,
L_data_ind = 0x29,
// Data Properties
M_PropRead_req = 0xFC,
M_PropRead_con = 0xFB,
M_PropWrite_req = 0xF6,
M_PropWrite_con = 0xF5,
M_PropInfo_ind = 0xF7,
// Function Properties
M_FuncPropCommand_req = 0xF8,
M_FuncPropCommand_con = 0xFA,
M_FuncPropStateRead_req = 0xF9,
M_FuncPropStateRead_con = 0xFA, // same as M_FuncPropStateRead_con (see 3/6/3 p.105)
// Further cEMI servies
M_Reset_req = 0xF1,
M_Reset_ind = 0xF0,
};
enum cEmiErrorCode
{
Unspecified_Error = 0x00, // unknown error (R/W)
Out_Of_Range = 0x01, // write value not allowed (general, if not error 2 or 3) (W)
Out_Of_Max_Range = 0x02, // write value to high (W)
Out_Of_Min_Range = 0x03, // write value to low (W)
Memory_Error = 0x04, // memory can not be written or only with fault(s) (W)
Read_Only = 0x05, // write access to a read only or a write protected Property (W)
Illegal_Command = 0x06, // COMMAND not valid or not supported (W)
Void_DP = 0x07, // read or write access to an non existing Property (R/W)
Type_Conflict = 0x08, // write access with a wrong data type (Datapoint length) (W)
Prop_Index_Range_Error = 0x09, // read or write access to a non existing Property array index (R/W)
Value_temp_not_writeable = 0x0A, // The Property exists but can at this moment not be written with a new value (W)
};
enum Repetition

View File

@ -94,6 +94,7 @@ enum PropertyID
PID_DEVICE_ADDR = 58,
PID_IO_LIST = 71,
PID_HARDWARE_TYPE = 78,
PID_RF_DOMAIN_ADDRESS_CEMI_SERVER = 82,
PID_DEVICE_DESCRIPTOR = 83,
/** Properties in the RF Medium Object */
@ -135,6 +136,27 @@ enum PropertyID
PID_MSG_TRANSMIT_TO_KNX = 75,
PID_FRIENDLY_NAME = 76,
PID_ROUTING_BUSY_WAIT_TIME = 78,
/** cEMI Server Object */
PID_MEDIUM_TYPE = 51,
PID_COMM_MODE = 52,
PID_MEDIUM_AVAILABILITY = 53,
PID_ADD_INFO_TYPES = 54,
PID_TIME_BASE = 55,
PID_TRANSP_ENABLE = 56,
PID_CLIENT_SNA = 57,
PID_CLIENT_DEVICE_ADDRESS = 58,
PID_BIBAT_NEXTBLOCK = 59,
PID_RF_MODE_SELECT = 60,
PID_RF_MODE_SUPPORT = 61,
PID_RF_FILTERING_MODE_SELECT_CEMI_SERVER = 62,
PID_RF_FILTERING_MODE_SUPPORT_CEMI_SERVER = 63,
PID_COMM_MODES_SUPPORTED = 64,
PID_FILTERING_MODE_SUPPORT = 65,
PID_FILTERING_MODE_SELECT = 66,
PID_MAX_INTERFACE_APDU_LENGTH = 68,
PID_MAX_LOCAL_APDU_LENGTH = 69,
};
enum LoadState

View File

@ -23,30 +23,40 @@ void RfDataLinkLayer::loop()
bool RfDataLinkLayer::sendFrame(CemiFrame& frame)
{
if (!_enabled)
return false;
// Depending on this flag, use either KNX Serial Number
// or the RF domain address that was programmed by ETS
if (frame.systemBroadcast() == SysBroadcast)
// If no serial number of domain address was set,
// use our own SN/DoA
if (frame.rfSerialOrDoA() == nullptr)
{
uint8_t knxSerialNumber[6];
pushWord(_deviceObject.manufacturerId(), &knxSerialNumber[0]);
pushInt(_deviceObject.bauNumber(), &knxSerialNumber[2]);
frame.rfSerialOrDoA(&knxSerialNumber[0]);
}
else
{
frame.rfSerialOrDoA(_rfMediumObj.rfDomainAddress());
// Depending on this flag, use either KNX Serial Number
// or the RF domain address that was programmed by ETS
if (frame.systemBroadcast() == SysBroadcast)
{
frame.rfSerialOrDoA((uint8_t*)_deviceObject.knxSerialNumber());
}
else
{
frame.rfSerialOrDoA(_rfMediumObj.rfDomainAddress());
}
}
// Set Data Link Layer Frame Number
frame.rfLfn(_frameNumber);
// Link Layer frame number counts 0..7
_frameNumber = (_frameNumber + 1) & 0x7;
// If Link Layer frame is set to 0xFF,
// use our own counter
if (frame.rfLfn() == 0xFF)
{
// Set Data Link Layer Frame Number
frame.rfLfn(_frameNumber);
// Link Layer frame number counts 0..7
_frameNumber = (_frameNumber + 1) & 0x7;
}
// bidirectional device, battery is ok, signal strength indication is void (no measurement)
frame.rfInfo(0x02);
frame.rfInfo(0x02);
if (!_enabled)
{
dataConReceived(frame, false);
return false;
}
// TODO: Is queueing really required?
// According to the spec. the upper layer may only send a new L_Data.req if it received
@ -166,7 +176,7 @@ void RfDataLinkLayer::frameBytesReceived(uint8_t* rfPacketBuf, uint16_t length)
// Prepare CEMI by writing/overwriting certain fields in the buffer (contiguous frame without CRC checksums)
// See 3.6.3 p.79: L_Data services for KNX RF asynchronous frames
// For now we do not use additional info, but use normal method arguments for CEMI
_buffer[0] = 0x29; // L_data.ind
_buffer[0] = (uint8_t) L_data_ind; // L_data.ind
_buffer[1] = 0; // Additional info length (spec. says that local dev management is not required to use AddInfo internally)
_buffer[2] = 0; // CTRL1 field (will be set later, this is the field we reserved space for)
_buffer[3] &= 0x0F; // CTRL2 field (take only RFCtrl.b3..0, b7..4 shall always be 0 for asynchronous KNX RF)
@ -190,7 +200,7 @@ void RfDataLinkLayer::frameBytesReceived(uint8_t* rfPacketBuf, uint16_t length)
// then we received the domain address and not the KNX serial number
if (systemBroadcast == Broadcast)
{
// Check if the received RF domain address matches the one stored in the RF medium object
// Check if the received RF domain address matches the one stored in the RF medium object
// If it does not match then skip the remaining processing
if (memcmp(_rfMediumObj.rfDomainAddress(), &rfPacketBuf[4], 6))
{

View File

@ -37,10 +37,13 @@ void RfMediumObject::readProperty(PropertyID propertyId, uint32_t start, uint32_
}
}
void RfMediumObject::writeProperty(PropertyID id, uint8_t start, uint8_t* data, uint8_t count)
void RfMediumObject::writeProperty(PropertyID id, uint32_t start, uint8_t* data, uint32_t& count)
{
switch (id)
{
case PID_RF_MULTI_TYPE:
// We only support RF ready and not RF multi, ignore write request
break;
case PID_RF_DOMAIN_ADDRESS:
for (uint8_t i = start; i < start + count; i++)
_rfDomainAddress[i-1] = data[i - start];
@ -60,6 +63,7 @@ void RfMediumObject::writeProperty(PropertyID id, uint8_t start, uint8_t* data,
// Not supported yet
break;
default:
count = 0;
break;
}
}

View File

@ -6,11 +6,12 @@ class RfMediumObject: public InterfaceObject
{
public:
void readProperty(PropertyID id, uint32_t start, uint32_t& count, uint8_t* data);
void writeProperty(PropertyID id, uint8_t start, uint8_t* data, uint8_t count);
void writeProperty(PropertyID id, uint32_t start, uint8_t* data, uint32_t& count);
uint8_t propertySize(PropertyID id);
uint8_t* save(uint8_t* buffer);
uint8_t* restore(uint8_t* buffer);
void readPropertyDescription(uint8_t propertyId, uint8_t& propertyIndex, bool& writeEnable, uint8_t& type, uint16_t& numberOfElements, uint8_t& access);
ObjectType objectType() { return OT_RF_MEDIUM; }
uint8_t* rfDomainAddress();
void rfDomainAddress(uint8_t* value);

View File

@ -29,7 +29,7 @@ void TableObject::readProperty(PropertyID id, uint32_t start, uint32_t& count, u
}
}
void TableObject::writeProperty(PropertyID id, uint8_t start, uint8_t* data, uint8_t count)
void TableObject::writeProperty(PropertyID id, uint32_t start, uint8_t* data, uint32_t &count)
{
switch (id)
{

View File

@ -15,7 +15,7 @@ public:
*/
TableObject(Platform& platform);
virtual void readProperty(PropertyID id, uint32_t start, uint32_t& count, uint8_t* data);
virtual void writeProperty(PropertyID id, uint8_t start, uint8_t* data, uint8_t count);
virtual void writeProperty(PropertyID id, uint32_t start, uint8_t* data, uint32_t& count);
virtual uint8_t propertySize(PropertyID id);
/**
* The destructor.

View File

@ -364,7 +364,10 @@ void TpUartDataLinkLayer::loop()
bool TpUartDataLinkLayer::sendFrame(CemiFrame& frame)
{
if (!_enabled)
{
dataConReceived(frame, false);
return false;
}
addFrameTxQueue(frame);
return true;

View File

@ -0,0 +1,543 @@
#ifdef USE_CEMI_SERVER
#include "bits.h"
#include "usb_tunnel_interface.h"
#include "cemi_server.h"
#include "cemi_frame.h"
#include <stdio.h>
#include <string.h>
#define MIN(a, b) ((a < b) ? (a) : (b))
#define MAX_EP_SIZE 64
#define HID_HEADER_SIZE 3
#define MAX_KNX_TELEGRAM_SIZE 263
#define KNX_HID_REPORT_ID 0x01
#define PROTOCOL_VERSION 0x00
#define PROTOCOL_HEADER_LENGTH 0x08
// Maximum possible payload data bytes in a transfer protocol body
#define MAX_DATASIZE_START_PACKET 52
#define MAX_DATASIZE_PARTIAL_PACKET 61
#define PACKET_TYPE_START 1
#define PACKET_TYPE_END 2
#define PACKET_TYPE_PARTIAL 4
//#define DEBUG_TX_HID_REPORT
//#define DEBUG_RX_HID_REPORT
extern bool sendHidReport(uint8_t* data, uint16_t length);
extern bool isSendHidReportPossible();
// class UsbTunnelInterface
UsbTunnelInterface::UsbTunnelInterface(CemiServer& cemiServer,
uint16_t mId,
uint16_t mV)
: _cemiServer(cemiServer),
_manufacturerId(mId),
_maskVersion(mV)
{
}
void UsbTunnelInterface::loop()
{
// Make sure that the USB HW is also ready to send another report
if (!isTxQueueEmpty() && isSendHidReportPossible())
{
uint8_t* buffer;
uint16_t length;
loadNextTxFrame(&buffer, &length);
sendHidReport(buffer, length);
delete buffer;
}
// Check if we already a COMPLETE transport protocol packet
// A transport protocol packet might be split into multiple HID reports and
// need to be assembled again
if (rxHaveCompletePacket)
{
handleHidReportRxQueue();
rxHaveCompletePacket = false;
}
}
/* USB TX */
void UsbTunnelInterface::sendCemiFrame(CemiFrame& frame)
{
sendKnxHidReport(KnxTunneling, ServiceIdNotUsed, frame.data(), frame.dataLength());
}
void UsbTunnelInterface::addBufferTxQueue(uint8_t* data, uint16_t length)
{
_queue_buffer_t* tx_buffer = new _queue_buffer_t;
tx_buffer->length = MAX_EP_SIZE;
tx_buffer->data = new uint8_t[MAX_EP_SIZE]; // We always have to send full max. USB endpoint size of 64 bytes
tx_buffer->next = nullptr;
memcpy(tx_buffer->data, data, tx_buffer->length);
memset(&tx_buffer->data[length], 0x00, MAX_EP_SIZE - length); // Set unused bytes to zero
if (_tx_queue.back == nullptr)
{
_tx_queue.front = _tx_queue.back = tx_buffer;
}
else
{
_tx_queue.back->next = tx_buffer;
_tx_queue.back = tx_buffer;
}
}
bool UsbTunnelInterface::isTxQueueEmpty()
{
if (_tx_queue.front == nullptr)
{
return true;
}
return false;
}
void UsbTunnelInterface::loadNextTxFrame(uint8_t** sendBuffer, uint16_t* sendBufferLength)
{
if (_tx_queue.front == nullptr)
{
return;
}
_queue_buffer_t* tx_buffer = _tx_queue.front;
*sendBuffer = tx_buffer->data;
*sendBufferLength = tx_buffer->length;
_tx_queue.front = tx_buffer->next;
if (_tx_queue.front == nullptr)
{
_tx_queue.back = nullptr;
}
delete tx_buffer;
#ifdef DEBUG_TX_HID_REPORT
print("TX HID report: len: ");
// We do not print the padded zeros
uint8_t len = (*sendBuffer)[2];
println(len, DEC);
for (int i = 0; i < len; i++)
{
if ((*sendBuffer)[i] < 16)
print("0");
print((*sendBuffer)[i], HEX);
print(" ");
}
println("");
#endif
}
void UsbTunnelInterface::sendKnxHidReport(ProtocolIdType protId, ServiceIdType servId, uint8_t* data, uint16_t length)
{
uint16_t maxData = MAX_DATASIZE_START_PACKET;
uint8_t packetType = PACKET_TYPE_START;
if (length > maxData)
{
packetType |= PACKET_TYPE_PARTIAL;
}
uint16_t offset = 0;
uint8_t* buffer = nullptr;
// In theory we can only have sequence numbers from 1..5
// First packet: 51 bytes max
// Other packets: 62 bytes max.
// -> 51 + 4*62 = 296 bytes -> enough for a KNX cEMI extended frame APDU + Transport Protocol Header length
for(uint8_t seqNum = 1; seqNum < 6; seqNum++)
{
uint16_t copyLen = MIN(length, maxData);
// If this is the first packet we include the transport protocol header
if (packetType & PACKET_TYPE_START)
{
buffer = new uint8_t[copyLen + 8 + HID_HEADER_SIZE]; // length of transport protocol header: 11 bytes
buffer[2] = 8 + copyLen; // KNX USB Transfer Protocol Body length
buffer[3] = PROTOCOL_VERSION; // Protocol version (fixed 0x00)
buffer[4] = PROTOCOL_HEADER_LENGTH; // USB KNX Transfer Protocol Header Length (fixed 0x08)
pushWord(copyLen, &buffer[5]); // KNX USB Transfer Protocol Body length (e.g. cEMI length)
buffer[7] = (uint8_t) protId; // KNX Tunneling (0x01) or KNX Bus Access Server (0x0f)
buffer[8] = (protId == KnxTunneling) ? (uint8_t)CEMI : (uint8_t)servId; // either EMI ID or Service Id
buffer[9] = 0x00; // Manufacturer (fixed 0x00) see KNX Spec 9/3 p.23 3.4.1.3.5
buffer[10] = 0x00; // Manufacturer (fixed 0x00) see KNX Spec 9/3 p.23 3.4.1.3.5
memcpy(&buffer[11], &data[offset], copyLen); // Copy payload for KNX USB Transfer Protocol Body
}
else
{
buffer = new uint8_t[copyLen]; // no transport protocol header in partial packets
buffer[2] = copyLen; // KNX USB Transfer Protocol Body length
memcpy(&buffer[0], &data[offset], copyLen); // Copy payload for KNX USB Transfer Protocol Body
}
offset += copyLen;
if (offset >= length)
{
packetType |= PACKET_TYPE_END;
}
buffer[0] = KNX_HID_REPORT_ID; // ReportID (fixed 0x01)
buffer[1] = ((seqNum << 4) & 0xF0) | (packetType & 0x07); // PacketInfo (SeqNo and Type)
addBufferTxQueue(buffer, (buffer[2] + HID_HEADER_SIZE));
delete[] buffer;
if (offset >= length)
{
break;
}
packetType &= ~PACKET_TYPE_START;
maxData = MAX_DATASIZE_PARTIAL_PACKET;
}
}
/* USB RX */
// Invoked when received SET_REPORT control request or via interrupt out pipe
void UsbTunnelInterface::receiveHidReport(uint8_t const* data, uint16_t bufSize)
{
// Check KNX ReportID (fixed 0x01)
if (data[0] == KNX_HID_REPORT_ID)
{
// We just store only the used space of the HID report buffer
// which is normally padded with 0 to fill the complete USB EP size (e.g. 64 bytes)
uint8_t packetLength = data[2] + HID_HEADER_SIZE;
UsbTunnelInterface::addBufferRxQueue(data, packetLength);
// Check if packet type indicates last packet
if ((data[1] & PACKET_TYPE_END) == PACKET_TYPE_END)
{
// Signal main loop that we have a complete KNX USB packet
rxHaveCompletePacket = true;
}
}
}
UsbTunnelInterface::_queue_t UsbTunnelInterface::_rx_queue;
bool UsbTunnelInterface::rxHaveCompletePacket = false;
void UsbTunnelInterface::addBufferRxQueue(const uint8_t* data, uint16_t length)
{
_queue_buffer_t* rx_buffer = new _queue_buffer_t;
rx_buffer->length = length;
rx_buffer->data = new uint8_t[rx_buffer->length];
rx_buffer->next = nullptr;
memcpy(rx_buffer->data, data, rx_buffer->length);
if (_rx_queue.back == nullptr)
{
_rx_queue.front =_rx_queue.back = rx_buffer;
}
else
{
_rx_queue.back->next = rx_buffer;
_rx_queue.back = rx_buffer;
}
}
bool UsbTunnelInterface::isRxQueueEmpty()
{
if (_rx_queue.front == nullptr)
{
return true;
}
return false;
}
void UsbTunnelInterface::loadNextRxBuffer(uint8_t** receiveBuffer, uint16_t* receiveBufferLength)
{
if (_rx_queue.front == nullptr)
{
return;
}
_queue_buffer_t* rx_buffer = _rx_queue.front;
*receiveBuffer = rx_buffer->data;
*receiveBufferLength = rx_buffer->length;
_rx_queue.front = rx_buffer->next;
if (_rx_queue.front == nullptr)
{
_rx_queue.back = nullptr;
}
delete rx_buffer;
#ifdef DEBUG_RX_HID_REPORT
print("RX HID report: len: ");
println(*receiveBufferLength, DEC);
for (int i = 0; i < (*receiveBufferLength); i++)
{
if ((*receiveBuffer)[i] < 16)
print("0");
print((*receiveBuffer)[i], HEX);
print(" ");
}
println("");
#endif
}
void UsbTunnelInterface::handleTransferProtocolPacket(uint8_t* data, uint16_t length)
{
if (data[0] == PROTOCOL_VERSION && // Protocol version (fixed 0x00)
data[1] == PROTOCOL_HEADER_LENGTH) // USB KNX Transfer Protocol Header Length (fixed 0x08)
{
uint16_t bodyLength;
popWord(bodyLength, (uint8_t*)&data[2]); // KNX USB Transfer Protocol Body length
if (data[4] == (uint8_t) BusAccessServer) // Bus Access Server Feature (0x0F)
{
handleBusAccessServerProtocol((ServiceIdType)data[5], &data[8], bodyLength);
}
else if (data[4] == (uint8_t) KnxTunneling) // KNX Tunneling (0x01)
{
if (data[5] == (uint8_t) CEMI) // EMI type: only cEMI supported (0x03))
{
// Prepare the cEMI frame
CemiFrame frame((uint8_t*)&data[8], bodyLength);
/*
print("cEMI USB RX len: ");
print(length);
print(" data: ");
printHex(" data: ", buffer, length);
*/
_cemiServer.frameReceived(frame);
}
else
{
println("Error: Only cEMI is supported!");
}
}
}
}
void UsbTunnelInterface::handleHidReportRxQueue()
{
if (isRxQueueEmpty())
{
println("Error: RX HID report queue was empty!");
return;
}
uint8_t tpPacket[MAX_KNX_TELEGRAM_SIZE + PROTOCOL_HEADER_LENGTH]; // Transport Protocol Header + Body
uint16_t offset = 0;
bool success = false;
// Now we have to reassemble the whole transport protocol packet which might be distributed over multiple HID reports
// In theory we can only have sequence numbers from 1..5
// First packet: 51 bytes max
// Other packets: 62 bytes max.
// -> 51 + 4*62 = 296 bytes -> enough for a KNX cEMI extended frame APDU + Transport Protocol Header length
for(int expSeqNum = 1; expSeqNum < 6; expSeqNum++)
{
// We should have at least one packet: either single packet (START and END set) or
// start packet (START and PARTIAL set) -> thus load first part
uint8_t* data;
uint16_t bufSize;
loadNextRxBuffer(&data, &bufSize); // bufSize contains the complete HID report length incl. HID header
// Get KNX HID report header details
uint8_t seqNum = data[1] >> 4;
uint8_t packetType = data[1] & 0x07;
uint8_t packetLength = MIN(data[2], bufSize - HID_HEADER_SIZE); // Do not try to read more than we actually have!
// Does the received sequence number match the expected one?
if (expSeqNum != seqNum)
{
println("Error: Wrong sequence number!");
delete data;
continue;
}
// first RX buffer from queue should contain the first part of the transfer protocol packet
if ((expSeqNum == 1) && ((packetType & PACKET_TYPE_START) != PACKET_TYPE_START))
{
println("Error: Sequence number 1 does not contain a START packet!");
delete data;
continue;
}
// Make sure we only have one START packet
if ((expSeqNum != 1) && ((packetType & PACKET_TYPE_START) == PACKET_TYPE_START))
{
println("Error: Sequence number (!=1) contains a START packet!");
delete data;
continue;
}
// Make sure other packets are marked correctly as PARTIAL packet
if ((expSeqNum != 1) && ((packetType & PACKET_TYPE_PARTIAL) != PACKET_TYPE_PARTIAL))
{
println("Error: Sequence number (!=1) must be a PARTIAL packet!");
delete data;
continue;
}
// Not really necessary, but we reset the offset here to zero
if ((packetType & PACKET_TYPE_START) == PACKET_TYPE_START)
{
offset = 0;
}
// Copy KNX HID Report Body to final buffer for concatenating
memcpy(&tpPacket[offset], &data[3], packetLength);
// Remove the source HID report buffer
delete data;
// Move offset
offset += packetLength;
// If we reached the end of the transport protocol packet, leave the loop
if ((packetType & PACKET_TYPE_END) == PACKET_TYPE_END)
{
success = true;
break;
}
}
// Make sure that we really saw the end of the transport protocol packet
if (success)
{
handleTransferProtocolPacket(tpPacket, offset);
}
else
{
println("Error: Did not find END packet!");
}
}
void UsbTunnelInterface::handleBusAccessServerProtocol(ServiceIdType servId, const uint8_t* requestData, uint16_t packetLength)
{
uint8_t respData[3]; // max. 3 bytes are required for a response
switch (servId)
{
case DeviceFeatureGet: // Device Feature Get
{
FeatureIdType featureId = (FeatureIdType)requestData[0];
respData[0] = (uint8_t) featureId; // first byte in repsonse is the featureId itself again
switch (featureId)
{
case SupportedEmiType: // Supported EMI types
println("Device Feature Get: Supported EMI types");
respData[1] = 0x00; // USB KNX Transfer Protocol Body: Feature Data
respData[2] = 0x04; // USB KNX Transfer Protocol Body: Feature Data -> only cEMI supported
sendKnxHidReport(BusAccessServer, DeviceFeatureResponse, respData, 3);
break;
case HostDeviceDescriptorType0: // Host Device Descriptor Type 0
println("Device Feature Get: Host Device Descriptor Type 0");
pushWord(_maskVersion, &respData[1]); // USB KNX Transfer Protocol Body: Feature Data -> Mask version
sendKnxHidReport(BusAccessServer, DeviceFeatureResponse, respData, 3);
break;
case BusConnectionStatus: // Bus connection status
println("Device Feature Get: Bus connection status");
respData[1] = 1; // USB KNX Transfer Protocol Body: Feature Data -> bus connection status
sendKnxHidReport(BusAccessServer, DeviceFeatureResponse, respData, 2);
break;
case KnxManufacturerCode: // KNX manufacturer code
println("Device Feature Get: KNX manufacturer code");
pushWord(_manufacturerId, &respData[1]); // USB KNX Transfer Protocol Body: Feature Data -> Manufacturer Code
sendKnxHidReport(BusAccessServer, DeviceFeatureResponse, respData, 3);
break;
case ActiveEmiType: // Active EMI type
println("Device Feature Get: Active EMI type");
respData[1] = (uint8_t) CEMI; // USB KNX Transfer Protocol Body: Feature Data -> cEMI type ID
sendKnxHidReport(BusAccessServer, DeviceFeatureResponse, respData, 2);
break;
default:
break;
}
break;
}
case DeviceFeatureSet: // Device Feature Set
{
FeatureIdType featureId = (FeatureIdType)requestData[0];
switch (featureId)
{
case ActiveEmiType: // Active EMI type
print("Device Feature Set: Active EMI type: ");
if (requestData[1] < 16)
print("0");
println(requestData[1], HEX); // USB KNX Transfer Protocol Body: Feature Data -> EMI TYPE ID
break;
// All other featureIds must not be set
case SupportedEmiType: // Supported EMI types
case HostDeviceDescriptorType0: // Host Device Descriptor Type 0
case BusConnectionStatus: // Bus connection status
case KnxManufacturerCode: // KNX manufacturer code
default:
break;
}
break;
}
// These are only sent from the device to the host
case DeviceFeatureResponse: // Device Feature Response
case DeviceFeatureInfo: // Device Feature Info
case DeviceFeatureEscape: // reserved (ESCAPE for future extensions)
default:
break;
}
}
/* USB HID report descriptor for KNX HID */
const uint8_t UsbTunnelInterface::descHidReport[] =
{
//TUD_HID_REPORT_DESC_KNXHID_INOUT(64)
0x06, 0xA0, 0xFF, // Usage Page (Vendor Defined 0xFFA0)
0x09, 0x01, // Usage (0x01)
0xA1, 0x01, // Collection (Application)
0x09, 0x01, // Usage (0x01)
0xA1, 0x00, // Collection (Physical)
0x06, 0xA1, 0xFF, // Usage Page (Vendor Defined 0xFFA1)
0x09, 0x03, // Usage (0x03)
0x09, 0x04, // Usage (0x04)
0x15, 0x80, // Logical Minimum (-128)
0x25, 0x7F, // Logical Maximum (127)
0x35, 0x00, // Physical Minimum (0)
0x45, 0xFF, // Physical Maximum (-1)
0x75, 0x08, // Report Size (8)
0x85, 0x01, // Report ID (1)
0x95, 0x3F, // Report Count (63)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x09, 0x05, // Usage (0x05)
0x09, 0x06, // Usage (0x06)
0x15, 0x80, // Logical Minimum (-128)
0x25, 0x7F, // Logical Maximum (127)
0x35, 0x00, // Physical Minimum (0)
0x45, 0xFF, // Physical Maximum (-1)
0x75, 0x08, // Report Size (8)
0x85, 0x01, // Report ID (1)
0x95, 0x3F, // Report Count (63)
0x91, 0x02, // Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0xC0, // End Collection
0xC0 // End Collection
};
const uint8_t* UsbTunnelInterface::getKnxHidReportDescriptor()
{
return &descHidReport[0];
}
uint16_t UsbTunnelInterface::getHidReportDescriptorLength()
{
return sizeof(descHidReport);
}
#endif

View File

@ -0,0 +1,93 @@
#pragma once
#include <stdint.h>
class CemiServer;
class CemiFrame;
enum ProtocolIdType
{
KnxTunneling = 0x01,
BusAccessServer = 0x0f
};
enum EmiIdType
{
EmiIdNotUsed = 0x00,
EMI1 = 0x01,
EMI2 = 0x02,
CEMI = 0x03
};
enum ServiceIdType
{
ServiceIdNotUsed = 0x00,
DeviceFeatureGet = 0x01,
DeviceFeatureResponse = 0x02,
DeviceFeatureSet = 0x03,
DeviceFeatureInfo = 0x04,
DeviceFeatureEscape = 0xFF
};
enum FeatureIdType
{
SupportedEmiType = 0x01,
HostDeviceDescriptorType0 = 0x02,
BusConnectionStatus = 0x03,
KnxManufacturerCode = 0x04,
ActiveEmiType = 0x05
};
class UsbTunnelInterface
{
public:
UsbTunnelInterface(CemiServer& cemiServer, uint16_t manufacturerId, uint16_t maskVersion);
void loop();
// from cEMI server
void sendCemiFrame(CemiFrame& frame);
static const uint8_t* getKnxHidReportDescriptor();
static uint16_t getHidReportDescriptorLength();
static void receiveHidReport(uint8_t const* data, uint16_t bufSize);
private:
struct _queue_buffer_t
{
uint8_t* data;
uint16_t length;
_queue_buffer_t* next;
};
struct _queue_t
{
_queue_buffer_t* front = nullptr;
_queue_buffer_t* back = nullptr;
};
static const uint8_t descHidReport[];
CemiServer& _cemiServer;
uint16_t _manufacturerId;
uint16_t _maskVersion;
// USB TX queue
_queue_t _tx_queue;
void addBufferTxQueue(uint8_t* data, uint16_t length);
bool isTxQueueEmpty();
void loadNextTxFrame(uint8_t** sendBuffer, uint16_t* sendBufferLength);
// USB RX queue
static _queue_t _rx_queue;
static void addBufferRxQueue(const uint8_t* data, uint16_t length);
bool isRxQueueEmpty();
void loadNextRxBuffer(uint8_t** receiveBuffer, uint16_t* receiveBufferLength);
static bool rxHaveCompletePacket;
void handleTransferProtocolPacket(uint8_t* data, uint16_t length);
void handleHidReportRxQueue();
void handleBusAccessServerProtocol(ServiceIdType servId, const uint8_t* requestData, uint16_t packetLength);
void sendKnxHidReport(ProtocolIdType protId, ServiceIdType servId, uint8_t* data, uint16_t length);
};