mirror of
https://github.com/thelsing/knx.git
synced 2024-12-18 19:08:18 +01:00
Add cEMI Server with KNX USB HID support (#47)
This commit is contained in:
parent
fd0b16b1b4
commit
a76fc31b80
29
doc/knx_cemi_notes.md
Normal file
29
doc/knx_cemi_notes.md
Normal 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)
|
||||
|
@ -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
5
knx-usb/.gitignore
vendored
Normal 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
8
knx-usb/custom_hwids.py
Normal 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
35
knx-usb/platformio.ini
Normal 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
128
knx-usb/src/main.cpp
Normal 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);
|
||||
}
|
||||
|
||||
}
|
@ -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.
|
||||
*/
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
};
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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
348
src/knx/cemi_server.cpp
Normal 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
50
src/knx/cemi_server.h
Normal 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;
|
||||
};
|
115
src/knx/cemi_server_object.cpp
Normal file
115
src/knx/cemi_server_object.cpp
Normal 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
|
24
src/knx/cemi_server_object.h
Normal file
24
src/knx/cemi_server_object.h
Normal 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;
|
||||
|
||||
};
|
@ -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)
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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 },
|
||||
|
@ -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};
|
||||
};
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
{
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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.
|
||||
|
@ -364,7 +364,10 @@ void TpUartDataLinkLayer::loop()
|
||||
bool TpUartDataLinkLayer::sendFrame(CemiFrame& frame)
|
||||
{
|
||||
if (!_enabled)
|
||||
{
|
||||
dataConReceived(frame, false);
|
||||
return false;
|
||||
}
|
||||
|
||||
addFrameTxQueue(frame);
|
||||
return true;
|
||||
|
543
src/knx/usb_tunnel_interface.cpp
Normal file
543
src/knx/usb_tunnel_interface.cpp
Normal 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
|
93
src/knx/usb_tunnel_interface.h
Normal file
93
src/knx/usb_tunnel_interface.h
Normal 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);
|
||||
};
|
Loading…
Reference in New Issue
Block a user