From a76fc31b80e450f24df0be70985df747fcec0383 Mon Sep 17 00:00:00 2001 From: nanosonde <2073569+nanosonde@users.noreply.github.com> Date: Mon, 9 Dec 2019 19:53:08 +0100 Subject: [PATCH] Add cEMI Server with KNX USB HID support (#47) --- doc/knx_cemi_notes.md | 29 ++ doc/knx_rf_notes.md | 48 ++- knx-usb/.gitignore | 5 + knx-usb/custom_hwids.py | 8 + knx-usb/platformio.ini | 35 ++ knx-usb/src/main.cpp | 128 ++++++ src/knx/address_table_object.h | 2 + src/knx/application_program_object.cpp | 2 +- src/knx/application_program_object.h | 3 +- src/knx/association_table_object.h | 1 + src/knx/bau.cpp | 12 +- src/knx/bau.h | 9 + src/knx/bau07B0.cpp | 47 +++ src/knx/bau07B0.h | 16 +- src/knx/bau27B0.cpp | 73 +++- src/knx/bau27B0.h | 13 + src/knx/bau57B0.cpp | 25 ++ src/knx/bau57B0.h | 1 + src/knx/bau_systemB.cpp | 47 ++- src/knx/bau_systemB.h | 10 +- src/knx/cemi_frame.cpp | 27 +- src/knx/cemi_frame.h | 8 +- src/knx/cemi_server.cpp | 348 ++++++++++++++++ src/knx/cemi_server.h | 50 +++ src/knx/cemi_server_object.cpp | 115 ++++++ src/knx/cemi_server_object.h | 24 ++ src/knx/data_link_layer.cpp | 69 +++- src/knx/data_link_layer.h | 10 + src/knx/device_object.cpp | 57 ++- src/knx/device_object.h | 12 +- src/knx/group_object_table_object.h | 1 + src/knx/interface_object.cpp | 10 +- src/knx/interface_object.h | 14 +- src/knx/ip_parameter_object.cpp | 5 +- src/knx/ip_parameter_object.h | 3 +- src/knx/knx_types.h | 35 ++ src/knx/property_types.h | 22 + src/knx/rf_data_link_layer.cpp | 52 ++- src/knx/rf_medium_object.cpp | 6 +- src/knx/rf_medium_object.h | 3 +- src/knx/table_object.cpp | 2 +- src/knx/table_object.h | 2 +- src/knx/tpuart_data_link_layer.cpp | 3 + src/knx/usb_tunnel_interface.cpp | 543 +++++++++++++++++++++++++ src/knx/usb_tunnel_interface.h | 93 +++++ 45 files changed, 1927 insertions(+), 101 deletions(-) create mode 100644 doc/knx_cemi_notes.md create mode 100644 knx-usb/.gitignore create mode 100644 knx-usb/custom_hwids.py create mode 100644 knx-usb/platformio.ini create mode 100644 knx-usb/src/main.cpp create mode 100644 src/knx/cemi_server.cpp create mode 100644 src/knx/cemi_server.h create mode 100644 src/knx/cemi_server_object.cpp create mode 100644 src/knx/cemi_server_object.h create mode 100644 src/knx/usb_tunnel_interface.cpp create mode 100644 src/knx/usb_tunnel_interface.h diff --git a/doc/knx_cemi_notes.md b/doc/knx_cemi_notes.md new file mode 100644 index 0000000..aefc291 --- /dev/null +++ b/doc/knx_cemi_notes.md @@ -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) + diff --git a/doc/knx_rf_notes.md b/doc/knx_rf_notes.md index f20cbd0..ca90308 100644 --- a/doc/knx_rf_notes.md +++ b/doc/knx_rf_notes.md @@ -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 +``` diff --git a/knx-usb/.gitignore b/knx-usb/.gitignore new file mode 100644 index 0000000..89cc49c --- /dev/null +++ b/knx-usb/.gitignore @@ -0,0 +1,5 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/knx-usb/custom_hwids.py b/knx-usb/custom_hwids.py new file mode 100644 index 0000000..3402bff --- /dev/null +++ b/knx-usb/custom_hwids.py @@ -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. +]) diff --git a/knx-usb/platformio.ini b/knx-usb/platformio.ini new file mode 100644 index 0000000..91e03ad --- /dev/null +++ b/knx-usb/platformio.ini @@ -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 + diff --git a/knx-usb/src/main.cpp b/knx-usb/src/main.cpp new file mode 100644 index 0000000..cb48937 --- /dev/null +++ b/knx-usb/src/main.cpp @@ -0,0 +1,128 @@ +#include +#include +#include + +/* + * 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); + } + +} diff --git a/src/knx/address_table_object.h b/src/knx/address_table_object.h index 28ce2bc..1df9c38 100644 --- a/src/knx/address_table_object.h +++ b/src/knx/address_table_object.h @@ -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. */ diff --git a/src/knx/application_program_object.cpp b/src/knx/application_program_object.cpp index 7269ff4..24663a4 100644 --- a/src/knx/application_program_object.cpp +++ b/src/knx/application_program_object.cpp @@ -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) { diff --git a/src/knx/application_program_object.h b/src/knx/application_program_object.h index e7ac83f..ab101b3 100644 --- a/src/knx/application_program_object.h +++ b/src/knx/application_program_object.h @@ -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); diff --git a/src/knx/association_table_object.h b/src/knx/association_table_object.h index b56587c..32f2e89 100644 --- a/src/knx/association_table_object.h +++ b/src/knx/association_table_object.h @@ -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); diff --git a/src/knx/bau.cpp b/src/knx/bau.cpp index b60d60c..f3db808 100644 --- a/src/knx/bau.cpp +++ b/src/knx/bau.cpp @@ -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) +{ +} diff --git a/src/knx/bau.h b/src/knx/bau.h index d2d6ada..8d31879 100644 --- a/src/knx/bau.h +++ b/src/knx/bau.h @@ -1,6 +1,7 @@ #pragma once #include #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); }; diff --git a/src/knx/bau07B0.cpp b/src/knx/bau07B0.cpp index 6d5b464..9affe2f 100644 --- a/src/knx/bau07B0.cpp +++ b/src/knx/bau07B0.cpp @@ -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 +} \ No newline at end of file diff --git a/src/knx/bau07B0.h b/src/knx/bau07B0.h index 5fe3f67..41934c8 100644 --- a/src/knx/bau07B0.h +++ b/src/knx/bau07B0.h @@ -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 }; \ No newline at end of file diff --git a/src/knx/bau27B0.cpp b/src/knx/bau27B0.cpp index 3e1b548..4f2d753 100644 --- a/src/knx/bau27B0.cpp +++ b/src/knx/bau27B0.cpp @@ -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); } diff --git a/src/knx/bau27B0.h b/src/knx/bau27B0.h index b2c129e..fdfa615 100644 --- a/src/knx/bau27B0.h +++ b/src/knx/bau27B0.h @@ -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); diff --git a/src/knx/bau57B0.cpp b/src/knx/bau57B0.cpp index 648d777..a97e320 100644 --- a/src/knx/bau57B0.cpp +++ b/src/knx/bau57B0.cpp @@ -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; diff --git a/src/knx/bau57B0.h b/src/knx/bau57B0.h index 8835150..5490b37 100644 --- a/src/knx/bau57B0.h +++ b/src/knx/bau57B0.h @@ -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(); diff --git a/src/knx/bau_systemB.cpp b/src/knx/bau_systemB.cpp index 9840c0d..c508bb1 100644 --- a/src/knx/bau_systemB.cpp +++ b/src/knx/bau_systemB.cpp @@ -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; +} diff --git a/src/knx/bau_systemB.h b/src/knx/bau_systemB.h index 5ca0ff5..57c20ee 100644 --- a/src/knx/bau_systemB.h +++ b/src/knx/bau_systemB.h @@ -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(); diff --git a/src/knx/cemi_frame.cpp b/src/knx/cemi_frame.cpp index d7be5d4..289e83a 100644 --- a/src/knx/cemi_frame.cpp +++ b/src/knx/cemi_frame.cpp @@ -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); diff --git a/src/knx/cemi_frame.h b/src/knx/cemi_frame.h index d363bb3..d9db230 100644 --- a/src/knx/cemi_frame.h +++ b/src/knx/cemi_frame.h @@ -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 }; \ No newline at end of file diff --git a/src/knx/cemi_server.cpp b/src/knx/cemi_server.cpp new file mode 100644 index 0000000..627cafc --- /dev/null +++ b/src/knx/cemi_server.cpp @@ -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 + +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 diff --git a/src/knx/cemi_server.h b/src/knx/cemi_server.h new file mode 100644 index 0000000..931a4a9 --- /dev/null +++ b/src/knx/cemi_server.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#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; +}; diff --git a/src/knx/cemi_server_object.cpp b/src/knx/cemi_server_object.cpp new file mode 100644 index 0000000..5652820 --- /dev/null +++ b/src/knx/cemi_server_object.cpp @@ -0,0 +1,115 @@ +#ifdef USE_CEMI_SERVER + +#include +#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 diff --git a/src/knx/cemi_server_object.h b/src/knx/cemi_server_object.h new file mode 100644 index 0000000..971a740 --- /dev/null +++ b/src/knx/cemi_server_object.h @@ -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; + +}; diff --git a/src/knx/data_link_layer.cpp b/src/knx/data_link_layer.cpp index 92d34a6..a8421e8 100644 --- a/src/knx/data_link_layer.cpp +++ b/src/knx/data_link_layer.cpp @@ -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) diff --git a/src/knx/data_link_layer.h b/src/knx/data_link_layer.h index a08e524..770e943 100644 --- a/src/knx/data_link_layer.h +++ b/src/knx/data_link_layer.h @@ -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 }; diff --git a/src/knx/device_object.cpp b/src/knx/device_object.cpp index e9d0905..cfd7145 100644 --- a/src/knx/device_object.cpp +++ b/src/knx/device_object.cpp @@ -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 }, diff --git a/src/knx/device_object.h b/src/knx/device_object.h index 35704f1..3ecda36 100644 --- a/src/knx/device_object.h +++ b/src/knx/device_object.h @@ -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}; }; \ No newline at end of file diff --git a/src/knx/group_object_table_object.h b/src/knx/group_object_table_object.h index bd9bdd3..0561652 100644 --- a/src/knx/group_object_table_object.h +++ b/src/knx/group_object_table_object.h @@ -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); diff --git a/src/knx/interface_object.cpp b/src/knx/interface_object.cpp index 21cd541..d51ace4 100644 --- a/src/knx/interface_object.cpp +++ b/src/knx/interface_object.cpp @@ -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; -} \ No newline at end of file +} + diff --git a/src/knx/interface_object.h b/src/knx/interface_object.h index 2d2cfe8..cc70121 100644 --- a/src/knx/interface_object.h +++ b/src/knx/interface_object.h @@ -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. diff --git a/src/knx/ip_parameter_object.cpp b/src/knx/ip_parameter_object.cpp index d35883e..4139e82 100644 --- a/src/knx/ip_parameter_object.cpp +++ b/src/knx/ip_parameter_object.cpp @@ -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; } } diff --git a/src/knx/ip_parameter_object.h b/src/knx/ip_parameter_object.h index 4b23e34..fc6fc00 100644 --- a/src/knx/ip_parameter_object.h +++ b/src/knx/ip_parameter_object.h @@ -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); diff --git a/src/knx/knx_types.h b/src/knx/knx_types.h index 0421526..046a4b3 100644 --- a/src/knx/knx_types.h +++ b/src/knx/knx_types.h @@ -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 diff --git a/src/knx/property_types.h b/src/knx/property_types.h index 088c741..5df562f 100644 --- a/src/knx/property_types.h +++ b/src/knx/property_types.h @@ -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 diff --git a/src/knx/rf_data_link_layer.cpp b/src/knx/rf_data_link_layer.cpp index f4b6ff6..039dbf8 100644 --- a/src/knx/rf_data_link_layer.cpp +++ b/src/knx/rf_data_link_layer.cpp @@ -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)) { diff --git a/src/knx/rf_medium_object.cpp b/src/knx/rf_medium_object.cpp index 2e96d9d..f6d10e7 100644 --- a/src/knx/rf_medium_object.cpp +++ b/src/knx/rf_medium_object.cpp @@ -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; } } diff --git a/src/knx/rf_medium_object.h b/src/knx/rf_medium_object.h index 57c0a2e..1d4c4a6 100644 --- a/src/knx/rf_medium_object.h +++ b/src/knx/rf_medium_object.h @@ -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); diff --git a/src/knx/table_object.cpp b/src/knx/table_object.cpp index c9aaa1b..5df4c32 100644 --- a/src/knx/table_object.cpp +++ b/src/knx/table_object.cpp @@ -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) { diff --git a/src/knx/table_object.h b/src/knx/table_object.h index d30fb70..bcefd23 100644 --- a/src/knx/table_object.h +++ b/src/knx/table_object.h @@ -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. diff --git a/src/knx/tpuart_data_link_layer.cpp b/src/knx/tpuart_data_link_layer.cpp index 48fef44..1c5ba5b 100644 --- a/src/knx/tpuart_data_link_layer.cpp +++ b/src/knx/tpuart_data_link_layer.cpp @@ -364,7 +364,10 @@ void TpUartDataLinkLayer::loop() bool TpUartDataLinkLayer::sendFrame(CemiFrame& frame) { if (!_enabled) + { + dataConReceived(frame, false); return false; + } addFrameTxQueue(frame); return true; diff --git a/src/knx/usb_tunnel_interface.cpp b/src/knx/usb_tunnel_interface.cpp new file mode 100644 index 0000000..cdad135 --- /dev/null +++ b/src/knx/usb_tunnel_interface.cpp @@ -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 +#include + +#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 diff --git a/src/knx/usb_tunnel_interface.h b/src/knx/usb_tunnel_interface.h new file mode 100644 index 0000000..ef78a90 --- /dev/null +++ b/src/knx/usb_tunnel_interface.h @@ -0,0 +1,93 @@ +#pragma once + +#include + +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); +};