From 70f387a7b964365e5441d33a81bd2a3758f5fc16 Mon Sep 17 00:00:00 2001 From: mumpf Date: Tue, 15 Oct 2019 09:49:50 +0200 Subject: [PATCH 01/21] "Restart device" implementation (#38) * corrected float with DPT9 * Switch Programming-LED also via Bus/ETS * Again: Prog-LED switchable from bus/ETS * DPT16 (to bus) implemented * - Allows ProgButton interrupts on FALLING signal * restart device command * - removed magic numbers - added enum for restart states --- src/knx/application_layer.cpp | 24 +++++++++++++-- src/knx/application_layer.h | 7 +++-- src/knx/bau.cpp | 4 +++ src/knx/bau.h | 1 + src/knx/bau_systemB.cpp | 58 +++++++++++++++++++++++++++++++++-- src/knx/bau_systemB.h | 14 ++++++++- 6 files changed, 99 insertions(+), 9 deletions(-) diff --git a/src/knx/application_layer.cpp b/src/knx/application_layer.cpp index 321942a..0252c0f 100644 --- a/src/knx/application_layer.cpp +++ b/src/knx/application_layer.cpp @@ -176,7 +176,10 @@ void ApplicationLayer::connectIndication(uint16_t tsap) void ApplicationLayer::connectConfirm(uint16_t destination, uint16_t tsap, bool status) { if (status) + { _connectedTsap = tsap; + _bau.connectConfirm(tsap); + } else _connectedTsap = -1; } @@ -188,7 +191,7 @@ void ApplicationLayer::disconnectIndication(uint16_t tsap) void ApplicationLayer::disconnectConfirm(Priority priority, uint16_t tsap, bool status) { - + _connectedTsap = -1; } void ApplicationLayer::dataConnectedIndication(Priority priority, uint16_t tsap, APDU& apdu) @@ -334,13 +337,23 @@ void ApplicationLayer::deviceDescriptorReadResponse(AckType ack, Priority priori individualSend(ack, hopType, priority, asap, apdu); } -void ApplicationLayer::restartRequest(AckType ack, Priority priority, HopCountType hopType, uint16_t asap) +void ApplicationLayer::connectRequest(uint16_t destination, Priority priority) +{ + _transportLayer->connectRequest(destination, priority); +} + +void ApplicationLayer::disconnectRequest(Priority priority) +{ + _transportLayer->disconnectRequest(_connectedTsap, priority); +} + +void ApplicationLayer::restartRequest(AckType ack, Priority priority, HopCountType hopType) { CemiFrame frame(1); APDU& apdu = frame.apdu(); apdu.type(Restart); - individualSend(ack, hopType, priority, asap, apdu); + individualSend(ack, hopType, priority, _connectedTsap, apdu); } void ApplicationLayer::propertyValueReadRequest(AckType ack, Priority priority, HopCountType hopType, uint16_t asap, @@ -788,3 +801,8 @@ void ApplicationLayer::individualSend(AckType ack, HopCountType hopType, Priorit else _transportLayer->dataIndividualRequest(ack, hopType, priority, asap, apdu); } + +bool ApplicationLayer::isConnected() +{ + return (_connectedTsap >= 0); +} \ No newline at end of file diff --git a/src/knx/application_layer.h b/src/knx/application_layer.h index 7af7262..81a80b6 100644 --- a/src/knx/application_layer.h +++ b/src/knx/application_layer.h @@ -95,7 +95,10 @@ class ApplicationLayer uint8_t descriptorType); void deviceDescriptorReadResponse(AckType ack, Priority priority, HopCountType hopType, uint16_t asap, uint8_t descriptorType, uint8_t* deviceDescriptor); - void restartRequest(AckType ack, Priority priority, HopCountType hopType, uint16_t asap); + void connectRequest(uint16_t destination, Priority priority); + void disconnectRequest(Priority priority); + bool isConnected(); + void restartRequest(AckType ack, Priority priority, HopCountType hopType); void propertyValueReadRequest(AckType ack, Priority priority, HopCountType hopType, uint16_t asap, uint8_t objectIndex, uint8_t propertyId, uint8_t numberOfElements, uint16_t startIndex); void propertyValueReadResponse(AckType ack, Priority priority, HopCountType hopType, uint16_t asap, uint8_t objectIndex, @@ -147,5 +150,5 @@ class ApplicationLayer AssociationTableObject& _assocTable; BusAccessUnit& _bau; TransportLayer* _transportLayer = 0; - int32_t _connectedTsap; + int32_t _connectedTsap = -1; }; diff --git a/src/knx/bau.cpp b/src/knx/bau.cpp index 105b380..4bed0e5 100644 --- a/src/knx/bau.cpp +++ b/src/knx/bau.cpp @@ -236,3 +236,7 @@ void BusAccessUnit::keyWriteResponseConfirm(AckType ack, Priority priority, HopC void BusAccessUnit::keyWriteAppLayerConfirm(Priority priority, HopCountType hopType, uint16_t asap, uint8_t level) { } + +void BusAccessUnit::connectConfirm(uint16_t destination) +{ +} \ No newline at end of file diff --git a/src/knx/bau.h b/src/knx/bau.h index 14ca5b5..b979185 100644 --- a/src/knx/bau.h +++ b/src/knx/bau.h @@ -108,4 +108,5 @@ class BusAccessUnit virtual void keyWriteResponseConfirm(AckType ack, Priority priority, HopCountType hopType, uint16_t asap, uint8_t level, bool status); virtual void keyWriteAppLayerConfirm(Priority priority, HopCountType hopType, uint16_t asap, uint8_t level); + virtual void connectConfirm(uint16_t destination); }; diff --git a/src/knx/bau_systemB.cpp b/src/knx/bau_systemB.cpp index 4e66daa..53548e6 100644 --- a/src/knx/bau_systemB.cpp +++ b/src/knx/bau_systemB.cpp @@ -1,4 +1,5 @@ #include "bau_systemB.h" +#include "bits.h" #include #include @@ -21,6 +22,7 @@ void BauSystemB::loop() dataLinkLayer().loop(); _transLayer.loop(); sendNextGroupTelegram(); + nextRestartState(); } bool BauSystemB::enabled() @@ -293,8 +295,58 @@ void BauSystemB::addSaveRestore(SaveRestore* obj) _memory.addSaveRestore(obj); } - -void BauSystemB::restartRequest(uint16_t asap) +bool BauSystemB::restartRequest(uint16_t asap) { - _appLayer.restartRequest(AckRequested, LowPriority, NetworkLayerParameter, asap); + if (_appLayer.isConnected()) + return false; + _restartState = Connecting; // order important, has to be set BEFORE connectRequest + _appLayer.connectRequest(asap, SystemPriority); + _appLayer.deviceDescriptorReadRequest(AckRequested, SystemPriority, NetworkLayerParameter, asap, 0); + return true; } + +void BauSystemB::connectConfirm(uint16_t tsap) +{ + if (_restartState == Connecting && tsap >= 0) + { + /* restart connection is confirmed, go to the next state */ + _restartState = Connected; + _restartDelay = millis(); + } + else + { + _restartState = Idle; + } +} + +void BauSystemB::nextRestartState() +{ + switch (_restartState) + { + case Idle: + /* inactive state, do nothing */ + break; + case Connecting: + /* wait for connection, we do nothing here */ + break; + case Connected: + /* connection confirmed, we send restartRequest, but we wait a moment (sending ACK etc)... */ + if (millis() - _restartDelay > 30) + { + _appLayer.restartRequest(AckRequested, SystemPriority, NetworkLayerParameter); + _restartState = Restarted; + _restartDelay = millis(); + } + break; + case Restarted: + /* restart is finished, we send a discommect */ + if (millis() - _restartDelay > 30) + { + _appLayer.disconnectRequest(SystemPriority); + _restartState = Idle; + } + default: + break; + } +} + diff --git a/src/knx/bau_systemB.h b/src/knx/bau_systemB.h index 91a0741..6ad0300 100644 --- a/src/knx/bau_systemB.h +++ b/src/knx/bau_systemB.h @@ -27,7 +27,7 @@ class BauSystemB : protected BusAccessUnit void readMemory(); void writeMemory(); void addSaveRestore(SaveRestore* obj); - void restartRequest(uint16_t asap); + bool restartRequest(uint16_t asap); protected: virtual DataLinkLayer& dataLinkLayer() = 0; @@ -58,10 +58,20 @@ class BauSystemB : protected BusAccessUnit uint8_t* data, uint8_t dataLength) override; void groupValueWriteIndication(uint16_t asap, Priority priority, HopCountType hopType, uint8_t* data, uint8_t dataLength) override; + void connectConfirm(uint16_t tsap) override; virtual InterfaceObject* getInterfaceObject(uint8_t idx) = 0; void sendNextGroupTelegram(); void updateGroupObject(GroupObject& go, uint8_t* data, uint8_t length); + void nextRestartState(); + + enum RestartState + { + Idle, + Connecting, + Connected, + Restarted + }; DeviceObject _deviceObj; Memory _memory; @@ -74,4 +84,6 @@ class BauSystemB : protected BusAccessUnit TransportLayer _transLayer; NetworkLayer _netLayer; bool _configured = true; + RestartState _restartState = Idle; + uint32_t _restartDelay = 0; }; \ No newline at end of file From 3063bf8195e8491f4955e4a6e89c1533074084c1 Mon Sep 17 00:00:00 2001 From: thelsing Date: Tue, 15 Oct 2019 09:50:46 +0200 Subject: [PATCH 02/21] Update .travis.yml --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0b8bcb6..852763e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,8 +34,8 @@ before_install: - if [ "$MODE" = "ARDUINO" ]; then sed -i 's#compiler.warning_level=all#compiler.warning_level=default#' ~/.arduino15/preferences.txt; fi # changes for bsec lib # samd - - if [ "$MODE" = "ARDUINO" ]; then sed -ri 's#(recipe.c.combine.pattern=[^$]*\{archive_file\}")( -Wl,--end-group)#\1 {compiler.libraries.ldflags}\2#' ~/.arduino15/packages/arduino/hardware/samd/1.8.3/platform.txt; fi - - if [ "$MODE" = "ARDUINO" ]; then sed -i 's#compiler.elf2hex.extra_flags=#compiler.elf2hex.extra_flags=\ncompiler.libraries.ldflags=#' ~/.arduino15/packages/arduino/hardware/samd/1.8.3/platform.txt; fi + - if [ "$MODE" = "ARDUINO" ]; then sed -ri 's#(recipe.c.combine.pattern=[^$]*\{archive_file\}")( -Wl,--end-group)#\1 {compiler.libraries.ldflags}\2#' ~/.arduino15/packages/arduino/hardware/samd/1.8.4/platform.txt; fi + - if [ "$MODE" = "ARDUINO" ]; then sed -i 's#compiler.elf2hex.extra_flags=#compiler.elf2hex.extra_flags=\ncompiler.libraries.ldflags=#' ~/.arduino15/packages/arduino/hardware/samd/1.8.4/platform.txt; fi # esp8266 - if [ "$MODE" = "ARDUINO" ]; then sed -ri 's#(recipe.c.combine.pattern=[^$]*\{compiler.c.elf.libs\})( -Wl,--end-group "-L\{build.path\}")#\1 {compiler.libraries.ldflags}\2#' ~/.arduino15/packages/esp8266/hardware/esp8266/2.5.2/platform.txt; fi - if [ "$MODE" = "ARDUINO" ]; then sed -i 's#compiler.elf2hex.extra_flags=#compiler.elf2hex.extra_flags=\ncompiler.libraries.ldflags=#' ~/.arduino15/packages/esp8266/hardware/esp8266/2.5.2/platform.txt; fi From eaab7d754819ba085e95cfce4215ef679003dd4a Mon Sep 17 00:00:00 2001 From: thelsing Date: Wed, 23 Oct 2019 22:16:48 +0200 Subject: [PATCH 03/21] Update apdu.cpp fix short acpi detection --- src/knx/apdu.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/knx/apdu.cpp b/src/knx/apdu.cpp index 8809d10..9867215 100644 --- a/src/knx/apdu.cpp +++ b/src/knx/apdu.cpp @@ -12,7 +12,7 @@ ApduType APDU::type() apci = getWord(_data); popWord(apci, _data); apci &= 0x3ff; - if ((apci >> 6) < 11) //short apci + if ((apci >> 6) < 11 && (apci >> 6) != 7) //short apci apci &= 0x3c0; return (ApduType)apci; } From fc0153e52a60a375896ae82e69610f6deaa9fb5b Mon Sep 17 00:00:00 2001 From: nanosonde <2073569+nanosonde@users.noreply.github.com> Date: Fri, 25 Oct 2019 16:41:29 +0200 Subject: [PATCH 04/21] initial KNX RF S-Mode support --- knx-linux/CMakeLists.txt | 45 +- knx-linux/main.cpp | 4 +- src/arduino_platform.cpp | 40 ++ src/arduino_platform.h | 10 + src/knx/application_layer.cpp | 106 ++++- src/knx/application_layer.h | 8 + src/knx/bau.cpp | 25 +- src/knx/bau.h | 14 +- src/knx/bau07B0.cpp | 6 + src/knx/bau27B0.cpp | 103 +++++ src/knx/bau27B0.h | 30 ++ src/knx/bau57B0.cpp | 6 + src/knx/bau_systemB.cpp | 33 ++ src/knx/bau_systemB.h | 2 + src/knx/cemi_frame.cpp | 151 ++++++- src/knx/cemi_frame.h | 20 +- src/knx/data_link_layer.cpp | 22 +- src/knx/data_link_layer.h | 2 +- src/knx/device_object.cpp | 15 +- src/knx/device_object.h | 5 +- src/knx/interface_object.h | 5 +- src/knx/knx_types.h | 31 +- src/knx/network_layer.cpp | 2 +- src/knx/platform.h | 9 + src/knx/property_types.h | 11 + src/knx/rf_data_link_layer.cpp | 365 +++++++++++++++ src/knx/rf_data_link_layer.h | 58 +++ src/knx/rf_medium_object.cpp | 128 ++++++ src/knx/rf_medium_object.h | 27 ++ src/knx/rf_physical_layer.cpp | 799 +++++++++++++++++++++++++++++++++ src/knx/rf_physical_layer.h | 257 +++++++++++ src/knx_facade.cpp | 25 +- src/knx_facade.h | 45 +- src/linux_platform.cpp | 397 +++++++++++++++- src/linux_platform.h | 19 + 35 files changed, 2761 insertions(+), 64 deletions(-) create mode 100644 src/knx/bau27B0.cpp create mode 100644 src/knx/bau27B0.h create mode 100644 src/knx/rf_data_link_layer.cpp create mode 100644 src/knx/rf_data_link_layer.h create mode 100644 src/knx/rf_medium_object.cpp create mode 100644 src/knx/rf_medium_object.h create mode 100644 src/knx/rf_physical_layer.cpp create mode 100644 src/knx/rf_physical_layer.h diff --git a/knx-linux/CMakeLists.txt b/knx-linux/CMakeLists.txt index 5a9ba65..d29ed91 100644 --- a/knx-linux/CMakeLists.txt +++ b/knx-linux/CMakeLists.txt @@ -9,7 +9,8 @@ add_executable(knx-linux ../src/knx/association_table_object.cpp ../src/knx/bau.cpp ../src/knx/bau07B0.cpp - ../src/knx/bau57B0.cpp + ../src/knx/bau27B0.cpp + ../src/knx/bau57B0.cpp ../src/knx/bau_systemB.cpp ../src/knx/bits.cpp ../src/knx/cemi_frame.cpp @@ -23,12 +24,50 @@ add_executable(knx-linux ../src/knx/memory.cpp ../src/knx/network_layer.cpp ../src/knx/npdu.cpp - ../src/knx/table_object.cpp + ../src/knx/rf_physical_layer.cpp + ../src/knx/rf_data_link_layer.cpp + ../src/knx/rf_medium_object.cpp + ../src/knx/table_object.cpp ../src/knx/tpdu.cpp ../src/knx/tpuart_data_link_layer.cpp ../src/knx/transport_layer.cpp ../src/knx/platform.cpp + ../src/knx/address_table_object.h + ../src/knx/apdu.h + ../src/knx/application_layer.h + ../src/knx/application_program_object.h + ../src/knx/association_table_object.h + ../src/knx/bau.h + ../src/knx/bau07B0.h + ../src/knx/bau27B0.h + ../src/knx/bau57B0.h + ../src/knx/bau_systemB.h + ../src/knx/bits.h + ../src/knx/cemi_frame.h + ../src/knx/data_link_layer.h + ../src/knx/device_object.h + ../src/knx/group_object.h + ../src/knx/group_object_table_object.h + ../src/knx/interface_object.h + ../src/knx/ip_data_link_layer.h + ../src/knx/ip_parameter_object.h + ../src/knx/memory.h + ../src/knx/network_layer.h + ../src/knx/npdu.h + ../src/knx/rf_physical_layer.h + ../src/knx/rf_data_link_layer.h + ../src/knx/rf_medium_object.h + ../src/knx/table_object.h + ../src/knx/tpdu.h + ../src/knx/tpuart_data_link_layer.h + ../src/knx/transport_layer.h + ../src/knx/platform.h main.cpp + ../src/linux_platform.h + ../src/knx_facade.h + ../src/knx/dptconvert.h + ../src/knx/knx_value.h + ../src/knx/dpt.h ../src/linux_platform.cpp ../src/knx_facade.cpp ../src/knx/dptconvert.cpp @@ -38,3 +77,5 @@ target_link_libraries(knx-linux "${LIBRARIES_FROM_REFERENCES}") include_directories(../src) set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -Wall -Wno-unknown-pragmas -Wno-switch -g -O0") set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Wall -Wno-unknown-pragmas -Wno-switch -g -O0") +set_property(TARGET knx-linux PROPERTY CXX_STANDARD 11) +install(TARGETS knx-linux RUNTIME DESTINATION /tmp) diff --git a/knx-linux/main.cpp b/knx-linux/main.cpp index 3a38d4a..21e7abc 100644 --- a/knx-linux/main.cpp +++ b/knx-linux/main.cpp @@ -1,5 +1,6 @@ #include "knx_facade.h" #include "knx/bau57B0.h" +//#include "knx/bau2705.h" #include "knx/group_object_table_object.h" #include "knx/bits.h" #include @@ -7,6 +8,7 @@ #include KnxFacade knx; +//KnxFacade knx; long lastsend = 0; @@ -94,6 +96,6 @@ int main(int argc, char **argv) knx.loop(); if(knx.configured()) appLoop(); - delay(100); + delayMicroseconds(1000); } } \ No newline at end of file diff --git a/src/arduino_platform.cpp b/src/arduino_platform.cpp index 979745d..874f3b1 100644 --- a/src/arduino_platform.cpp +++ b/src/arduino_platform.cpp @@ -2,6 +2,7 @@ #include #include +#include Stream* ArduinoPlatform::SerialDebug = &Serial; @@ -60,6 +61,7 @@ void ArduinoPlatform::closeMultiCast() bool ArduinoPlatform::sendBytes(uint8_t * buffer, uint16_t len) { //not needed + return false; } int ArduinoPlatform::readBytes(uint8_t * buffer, uint16_t maxLen) @@ -137,6 +139,44 @@ size_t ArduinoPlatform::readBytesUart(uint8_t *buffer, size_t length) return length; } +void ArduinoPlatform::setupSpi() +{ + SPI.begin(); + SPI.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0)); +} + +void ArduinoPlatform::closeSpi() +{ + SPI.endTransaction(); + SPI.end(); +} + +int ArduinoPlatform::readWriteSpi(uint8_t *data, size_t len) +{ + SPI.transfer(data, len); + return 0; +} + +void ArduinoPlatform::setupGpio(uint32_t dwPin, uint32_t dwMode) +{ + pinMode(dwPin, dwMode); +} + +void ArduinoPlatform::closeGpio(uint32_t dwPin) +{ + // not used +} + +void ArduinoPlatform::writeGpio(uint32_t dwPin, uint32_t dwVal) +{ + digitalWrite(dwPin, dwVal); +} + +uint32_t ArduinoPlatform::readGpio(uint32_t dwPin) +{ + return digitalRead(dwPin); +} + void print(const char* s) { ArduinoPlatform::SerialDebug->print(s); diff --git a/src/arduino_platform.h b/src/arduino_platform.h index 60acba8..53a32a8 100644 --- a/src/arduino_platform.h +++ b/src/arduino_platform.h @@ -35,6 +35,16 @@ class ArduinoPlatform : public Platform virtual int readUart(); virtual size_t readBytesUart(uint8_t* buffer, size_t length); + //spi + void setupSpi() override; + void closeSpi() override; + int readWriteSpi (uint8_t *data, size_t len) override; + + virtual void setupGpio(uint32_t dwPin, uint32_t dwMode) override; + virtual void closeGpio(uint32_t dwPin) override; + virtual void writeGpio(uint32_t dwPin, uint32_t dwVal) override; + virtual uint32_t readGpio(uint32_t dwPin) override; + static Stream* SerialDebug; protected: diff --git a/src/knx/application_layer.cpp b/src/knx/application_layer.cpp index 0252c0f..669bb44 100644 --- a/src/knx/application_layer.cpp +++ b/src/knx/application_layer.cpp @@ -90,8 +90,11 @@ void ApplicationLayer::dataBroadcastIndication(HopCountType hopType, Priority pr _bau.individualAddressReadAppLayerConfirm(hopType, apdu.frame().sourceAddress()); break; case IndividualAddressSerialNumberRead: - _bau.individualAddressSerialNumberReadIndication(hopType, data + 1); + { + uint8_t* knxSerialNumber = &data[1]; + _bau.individualAddressSerialNumberReadIndication(priority, hopType, knxSerialNumber); break; + } case IndividualAddressSerialNumberResponse: { uint16_t domainAddress; @@ -102,9 +105,10 @@ void ApplicationLayer::dataBroadcastIndication(HopCountType hopType, Priority pr } case IndividualAddressSerialNumberWrite: { - uint16_t newAddress; - popWord(newAddress, data + 7); - _bau.individualAddressSerialNumberWriteIndication(hopType, data + 1, newAddress); + uint8_t* knxSerialNumber = &data[1]; + uint16_t newIndividualAddress; + popWord(newIndividualAddress, &data[7]); + _bau.individualAddressSerialNumberWriteIndication(priority, hopType, newIndividualAddress, knxSerialNumber); break; } } @@ -150,7 +154,40 @@ void ApplicationLayer::dataBroadcastConfirm(AckType ack, HopCountType hopType, P void ApplicationLayer::dataSystemBroadcastIndication(HopCountType hopType, Priority priority, uint16_t source, APDU& apdu) { - + uint8_t* data = apdu.data(); + switch (apdu.type()) + { + // TODO: testInfo could be of any length + case SystemNetworkParameterRead: + { + uint16_t objectType; + uint16_t propertyId; + uint8_t testInfo[2]; + popWord(objectType, data + 1); + popWord(propertyId, data + 3); + popByte(testInfo[0], data + 4); + popByte(testInfo[1], data + 5); + propertyId = (propertyId >> 4) & 0x0FFF;; + testInfo[0] &= 0x0F; + _bau.systemNetworkParameterReadIndication(priority, hopType, objectType, propertyId, testInfo, sizeof(testInfo)); + break; + } + case DomainAddressSerialNumberWrite: + { + uint8_t* knxSerialNumber = &data[1]; + uint8_t* domainAddress = &data[7]; + _bau.domainAddressSerialNumberWriteIndication(priority, hopType, domainAddress, knxSerialNumber); + break; + } + case DomainAddressSerialNumberRead: + { + uint8_t* knxSerialNumber = &data[1]; + _bau.domainAddressSerialNumberReadIndication(priority, hopType, knxSerialNumber); + break; + } + default: + break; + } } void ApplicationLayer::dataSystemBroadcastConfirm(HopCountType hopType, Priority priority, APDU& apdu, bool status) @@ -356,6 +393,65 @@ void ApplicationLayer::restartRequest(AckType ack, Priority priority, HopCountTy individualSend(ack, hopType, priority, _connectedTsap, apdu); } +//TODO: ApplicationLayer::systemNetworkParameterReadRequest() +void ApplicationLayer::systemNetworkParameterReadResponse(Priority priority, HopCountType hopType, + uint16_t objectType, uint16_t propertyId, + uint8_t* testInfo, uint16_t testInfoLength, + uint8_t* testResult, uint16_t testResultLength) +{ + CemiFrame frame(testInfoLength + testResultLength + 3 + 1); // PID and testInfo share an octet (+3) and +1 for APCI byte(?) + APDU& apdu = frame.apdu(); + apdu.type(SystemNetworkParameterResponse); + uint8_t* data = apdu.data() + 1; + + pushWord(objectType, data); + pushWord((propertyId << 4) & 0xFFF0, data + 2); // Reserved bits for test_info are always 0 + uint8_t* pData = pushByteArray(&testInfo[1], testInfoLength - 1, data + 4); // TODO: upper reserved bits (testInfo + 0) have to put into the lower bits of data + 3 + memcpy(pData, testResult, testResultLength); + + //apdu.printPDU(); + + _transportLayer->dataSystemBroadcastRequest(AckDontCare, hopType, SystemPriority, apdu); +} + +//TODO: ApplicationLayer::domainAddressSerialNumberWriteRequest() +//TODO: ApplicationLayer::domainAddressSerialNumberReadRequest() +void ApplicationLayer::domainAddressSerialNumberReadResponse(Priority priority, HopCountType hopType, uint8_t* rfDoA, + uint8_t* knxSerialNumber) +{ + CemiFrame frame(13); + APDU& apdu = frame.apdu(); + apdu.type(DomainAddressSerialNumberResponse); + + uint8_t* data = apdu.data() + 1; + + memcpy(data, knxSerialNumber, 6); + memcpy(data + 6, rfDoA, 6); + + //apdu.printPDU(); + + _transportLayer->dataSystemBroadcastRequest(AckDontCare, hopType, SystemPriority, apdu); +} + +//TODO: ApplicationLayer::IndividualAddressSerialNumberWriteRequest() +//TODO: ApplicationLayer::IndividualAddressSerialNumberReadRequest() +void ApplicationLayer::IndividualAddressSerialNumberReadResponse(Priority priority, HopCountType hopType, uint8_t* rfDoA, + uint8_t* knxSerialNumber) +{ + CemiFrame frame(13); + APDU& apdu = frame.apdu(); + apdu.type(IndividualAddressSerialNumberResponse); + + uint8_t* data = apdu.data() + 1; + + memcpy(data, knxSerialNumber, 6); + memcpy(data + 6, rfDoA, 6); + + //apdu.printPDU(); + + _transportLayer->dataBroadcastRequest(AckDontCare, hopType, SystemPriority, apdu); +} + void ApplicationLayer::propertyValueReadRequest(AckType ack, Priority priority, HopCountType hopType, uint16_t asap, uint8_t objectIndex, uint8_t propertyId, uint8_t numberOfElements, uint16_t startIndex) { diff --git a/src/knx/application_layer.h b/src/knx/application_layer.h index 81a80b6..9d7223f 100644 --- a/src/knx/application_layer.h +++ b/src/knx/application_layer.h @@ -129,6 +129,14 @@ class ApplicationLayer void authorizeResponse(AckType ack, Priority priority, HopCountType hopType, uint16_t asap, uint8_t level); void keyWriteRequest(AckType ack, Priority priority, HopCountType hopType, uint16_t asap, uint8_t level, uint32_t key); void keyWriteResponse(AckType ack, Priority priority, HopCountType hopType, uint16_t asap, uint8_t level); + + void systemNetworkParameterReadResponse(Priority priority, HopCountType hopType, uint16_t objectType, + uint16_t propertyId, uint8_t* testInfo, uint16_t testInfoLength, + uint8_t* testResult, uint16_t testResultLength); + void domainAddressSerialNumberReadResponse(Priority priority, HopCountType hopType, uint8_t* rfDoA, + uint8_t* knxSerialNumber); + void IndividualAddressSerialNumberReadResponse(Priority priority, HopCountType hopType, uint8_t* rfDoA, + uint8_t* knxSerialNumber); #pragma endregion private: diff --git a/src/knx/bau.cpp b/src/knx/bau.cpp index 4bed0e5..b60d60c 100644 --- a/src/knx/bau.cpp +++ b/src/knx/bau.cpp @@ -52,7 +52,7 @@ void BusAccessUnit::individualAddressSerialNumberReadLocalConfirm(AckType ack, H { } -void BusAccessUnit::individualAddressSerialNumberReadIndication(HopCountType hopType, uint8_t * serialNumber) +void BusAccessUnit::individualAddressSerialNumberReadIndication(Priority priority, HopCountType hopType, uint8_t* knxSerialNumber) { } @@ -68,7 +68,8 @@ void BusAccessUnit::individualAddressSerialNumberWriteLocalConfirm(AckType ack, { } -void BusAccessUnit::individualAddressSerialNumberWriteIndication(HopCountType hopType, uint8_t * serialNumber, uint16_t newaddress) +void BusAccessUnit::individualAddressSerialNumberWriteIndication(Priority priority, HopCountType hopType, uint16_t newIndividualAddress, + uint8_t* knxSerialNumber) { } @@ -239,4 +240,22 @@ void BusAccessUnit::keyWriteAppLayerConfirm(Priority priority, HopCountType hopT void BusAccessUnit::connectConfirm(uint16_t destination) { -} \ No newline at end of file +} + +void BusAccessUnit::systemNetworkParameterReadIndication(Priority priority, HopCountType hopType, uint16_t objectType, + uint16_t propertyId, uint8_t* testInfo, uint16_t testInfoLength) +{ +} + +void BusAccessUnit::domainAddressSerialNumberWriteIndication(Priority priority, HopCountType hopType, uint8_t* rfDoA, + uint8_t* knxSerialNumber) +{ +} + +void BusAccessUnit::domainAddressSerialNumberReadIndication(Priority priority, HopCountType hopType, uint8_t* knxSerialNumber) +{ +} + + + + diff --git a/src/knx/bau.h b/src/knx/bau.h index b979185..e13af25 100644 --- a/src/knx/bau.h +++ b/src/knx/bau.h @@ -25,14 +25,15 @@ class BusAccessUnit virtual void individualAddressReadAppLayerConfirm(HopCountType hopType, uint16_t individualAddress); virtual void individualAddressSerialNumberReadLocalConfirm(AckType ack, HopCountType hopType, uint8_t* serialNumber, bool status); - virtual void individualAddressSerialNumberReadIndication(HopCountType hopType, uint8_t* serialNumber); + virtual void individualAddressSerialNumberReadIndication(Priority priority, HopCountType hopType, uint8_t* knxSerialNumber); virtual void individualAddressSerialNumberReadResponseConfirm(AckType ack, HopCountType hopType, uint8_t* serialNumber, uint16_t domainAddress, bool status); virtual void individualAddressSerialNumberReadAppLayerConfirm(HopCountType hopType, uint8_t* serialNumber, uint16_t individualAddress, uint16_t domainAddress); virtual void individualAddressSerialNumberWriteLocalConfirm(AckType ack, HopCountType hopType, uint8_t* serialNumber, uint16_t newaddress, bool status); - virtual void individualAddressSerialNumberWriteIndication(HopCountType hopType, uint8_t* serialNumber, uint16_t newaddress); + virtual void individualAddressSerialNumberWriteIndication(Priority priority, HopCountType hopType, uint16_t newIndividualAddress, + uint8_t* knxSerialNumber); virtual void deviceDescriptorReadLocalConfirm(AckType ack, Priority priority, HopCountType hopType, uint16_t asap, uint8_t descriptorType, bool status); virtual void deviceDescriptorReadIndication(Priority priority, HopCountType hopType, uint16_t asap, uint8_t descriptorType); @@ -108,5 +109,12 @@ class BusAccessUnit virtual void keyWriteResponseConfirm(AckType ack, Priority priority, HopCountType hopType, uint16_t asap, uint8_t level, bool status); virtual void keyWriteAppLayerConfirm(Priority priority, HopCountType hopType, uint16_t asap, uint8_t level); - virtual void connectConfirm(uint16_t destination); + virtual bool connectConfirm(uint16_t destination); + virtual void systemNetworkParameterReadIndication(Priority priority, HopCountType hopType, uint16_t objectType, + uint16_t propertyId, uint8_t* testInfo, uint16_t testInfoLength); + + virtual void domainAddressSerialNumberWriteIndication(Priority priority, HopCountType hopType, uint8_t* rfDoA, + uint8_t* knxSerialNumber); + + virtual void domainAddressSerialNumberReadIndication(Priority priority, HopCountType hopType, uint8_t* knxSerialNumber); }; diff --git a/src/knx/bau07B0.cpp b/src/knx/bau07B0.cpp index 435ba5e..87db8ce 100644 --- a/src/knx/bau07B0.cpp +++ b/src/knx/bau07B0.cpp @@ -1,4 +1,5 @@ #include "bau07B0.h" +#include "bits.h" #include #include @@ -9,6 +10,11 @@ Bau07B0::Bau07B0(Platform& platform) _dlLayer(_deviceObj, _addrTable, _netLayer, _platform) { _netLayer.dataLinkLayer(_dlLayer); + + // Set Mask Version in Device Object depending on the BAU + uint16_t maskVersion; + popWord(maskVersion, _descriptor); + _deviceObj.maskVersion(maskVersion); } InterfaceObject* Bau07B0::getInterfaceObject(uint8_t idx) diff --git a/src/knx/bau27B0.cpp b/src/knx/bau27B0.cpp new file mode 100644 index 0000000..2b5365d --- /dev/null +++ b/src/knx/bau27B0.cpp @@ -0,0 +1,103 @@ +#include "bau27B0.h" +#include "bits.h" +#include +#include + +using namespace std; + +Bau27B0::Bau27B0(Platform& platform) + : BauSystemB(platform), + _dlLayer(_deviceObj, _rfMediumObj, _addrTable, _netLayer, _platform) +{ + _netLayer.dataLinkLayer(_dlLayer); + _memory.addSaveRestore(&_rfMediumObj); + + // Set Mask Version in Device Object depending on the BAU + uint16_t maskVersion; + popWord(maskVersion, _descriptor); + _deviceObj.maskVersion(maskVersion); +} + +// see KNX AN160 p.74 for mask 27B0 +InterfaceObject* Bau27B0::getInterfaceObject(uint8_t idx) +{ + switch (idx) + { + case 0: + return &_deviceObj; + case 1: + return &_addrTable; + case 2: + return &_assocTable; + case 3: + return &_groupObjTable; + case 4: + return &_appProgram; + case 5: // would be app_program 2 + return nullptr; + case 6: + return &_rfMediumObj; + default: + return nullptr; + } +} + +uint8_t* Bau27B0::descriptor() +{ + return _descriptor; +} + +DataLinkLayer& Bau27B0::dataLinkLayer() +{ + return _dlLayer; +} + +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)) + _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)) + _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)) + _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)) + _appLayer.IndividualAddressSerialNumberReadResponse(priority, hopType, _rfMediumObj.rfDomainAddress(), knxSerialNumber); +} diff --git a/src/knx/bau27B0.h b/src/knx/bau27B0.h new file mode 100644 index 0000000..f1b30e6 --- /dev/null +++ b/src/knx/bau27B0.h @@ -0,0 +1,30 @@ +#pragma once + +#include "bau_systemB.h" +#include "rf_medium_object.h" +#include "rf_physical_layer.h" +#include "rf_data_link_layer.h" + +class Bau27B0 : public BauSystemB +{ + public: + Bau27B0(Platform& platform); + + protected: + InterfaceObject* getInterfaceObject(uint8_t idx); + uint8_t* descriptor(); + DataLinkLayer& dataLinkLayer(); + + private: + RfDataLinkLayer _dlLayer; + RfMediumObject _rfMediumObj; + + uint8_t _descriptor[2] = {0x27, 0xB0}; + + void domainAddressSerialNumberWriteIndication(Priority priority, HopCountType hopType, uint8_t* rfDoA, + uint8_t* knxSerialNumber); + void domainAddressSerialNumberReadIndication(Priority priority, HopCountType hopType, uint8_t* knxSerialNumber); + void individualAddressSerialNumberWriteIndication(Priority priority, HopCountType hopType, uint16_t newIndividualAddress, + uint8_t* knxSerialNumber); + void individualAddressSerialNumberReadIndication(Priority priority, HopCountType hopType, uint8_t* knxSerialNumber); +}; \ No newline at end of file diff --git a/src/knx/bau57B0.cpp b/src/knx/bau57B0.cpp index c255934..74373c9 100644 --- a/src/knx/bau57B0.cpp +++ b/src/knx/bau57B0.cpp @@ -1,4 +1,5 @@ #include "bau57B0.h" +#include "bits.h" #include #include @@ -11,6 +12,11 @@ Bau57B0::Bau57B0(Platform& platform) { _netLayer.dataLinkLayer(_dlLayer); _memory.addSaveRestore(&_ipParameters); + + // Set Mask Version in Device Object depending on the BAU + uint16_t maskVersion; + popWord(maskVersion, _descriptor); + _deviceObj.maskVersion(maskVersion); } InterfaceObject* Bau57B0::getInterfaceObject(uint8_t idx) diff --git a/src/knx/bau_systemB.cpp b/src/knx/bau_systemB.cpp index 53548e6..807b5ef 100644 --- a/src/knx/bau_systemB.cpp +++ b/src/knx/bau_systemB.cpp @@ -350,3 +350,36 @@ 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) + + // See KNX spec. 3.5.2 p.33 (Management Procedures: Procedures with A_SystemNetworkParameter_Read) + switch(operand) + { + case 0x01: // NM_Read_SerialNumber_By_ProgrammingMode + // Only send a reply if programming mode is on + 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)); + } + break; + + case 0x02: // NM_Read_SerialNumber_By_ExFactoryState + break; + + case 0x03: // NM_Read_SerialNumber_By_PowerReset + break; + + case 0xFE: // Manufacturer specific use of A_SystemNetworkParameter_Read + break; + } +} diff --git a/src/knx/bau_systemB.h b/src/knx/bau_systemB.h index 6ad0300..5ca0ff5 100644 --- a/src/knx/bau_systemB.h +++ b/src/knx/bau_systemB.h @@ -58,6 +58,8 @@ class BauSystemB : protected BusAccessUnit uint8_t* data, uint8_t dataLength) override; 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); void connectConfirm(uint16_t tsap) override; virtual InterfaceObject* getInterfaceObject(uint8_t idx) = 0; diff --git a/src/knx/cemi_frame.cpp b/src/knx/cemi_frame.cpp index 005cd2e..d7be5d4 100644 --- a/src/knx/cemi_frame.cpp +++ b/src/knx/cemi_frame.cpp @@ -3,18 +3,88 @@ #include "string.h" #include -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) +/* +cEMI Frame Format + ++---------+--------+--------+--------+--------+---------+---------+--------+---------+ + | Header | Msg |Add.Info| Ctrl 1 | Ctrl 2 | Source | Dest. | Data | APDU | + | | Code | Length | | | Address | Address | Length | | + +---------+--------+--------+--------+--------+---------+---------+--------+---------+ + 6 bytes 1 byte 1 byte 1 byte 1 byte 2 bytes 2 bytes 1 byte 2 bytes + + Header = See below the structure of a cEMI header + Message Code = See below. On Appendix A is the list of all existing EMI and cEMI codes + Add.Info Length = 0x00 - no additional info + Control Field 1 = + Control Field 2 = + Source Address = 0x0000 - filled in by router/gateway with its source address which is + part of the KNX subnet + Dest. Address = KNX group or individual address (2 byte) + Data Length = Number of bytes of data in the APDU excluding the TPCI/APCI bits + APDU = Application Protocol Data Unit - the actual payload including transport + protocol control information (TPCI), application protocol control + information (APCI) and data passed as an argument from higher layers of + the KNX communication stack + +Control Field 1 + + Bit | + ------+--------------------------------------------------------------- + 7 | Frame Type - 0x0 for extended frame + | 0x1 for standard frame + ------+--------------------------------------------------------------- + 6 | Reserved + | + ------+--------------------------------------------------------------- + 5 | Repeat Flag - 0x0 repeat frame on medium in case of an error + | 0x1 do not repeat + ------+--------------------------------------------------------------- + 4 | System Broadcast - 0x0 system broadcast + | 0x1 broadcast + ------+--------------------------------------------------------------- + 3 | Priority - 0x0 system + | 0x1 normal + ------+ 0x2 urgent + 2 | 0x3 low + | + ------+--------------------------------------------------------------- + 1 | Acknowledge Request - 0x0 no ACK requested + | (L_Data.req) 0x1 ACK requested + ------+--------------------------------------------------------------- + 0 | Confirm - 0x0 no error + | (L_Data.con) - 0x1 error + ------+--------------------------------------------------------------- + + Control Field 2 + + Bit | + ------+--------------------------------------------------------------- + 7 | Destination Address Type - 0x0 individual address + | - 0x1 group address + ------+--------------------------------------------------------------- + 6-4 | Hop Count (0-7) + ------+--------------------------------------------------------------- + 3-0 | Extended Frame Format - 0x0 standard frame + ------+--------------------------------------------------------------- +*/ + +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) { _data = data; - _ctrl1 = data + data[1] + 2; + _ctrl1 = data + data[1] + CEMI_HEADER_SIZE; _length = length; } -CemiFrame::CemiFrame(uint8_t apduLength): _data(buffer), - _npdu(_data + NPDU_LPDU_DIFF, *this), _tpdu(_data + TPDU_LPDU_DIFF, *this), _apdu(_data + APDU_LPDU_DIFF, *this) +CemiFrame::CemiFrame(uint8_t apduLength) + : _data(buffer), + _npdu(_data + NPDU_LPDU_DIFF, *this), + _tpdu(_data + TPDU_LPDU_DIFF, *this), + _apdu(_data + APDU_LPDU_DIFF, *this) { - _ctrl1 = _data + 2; + _ctrl1 = _data + CEMI_HEADER_SIZE; _length = 0; memset(_data, 0, apduLength + APDU_LPDU_DIFF); @@ -22,10 +92,13 @@ CemiFrame::CemiFrame(uint8_t apduLength): _data(buffer), _npdu.octetCount(apduLength); } -CemiFrame::CemiFrame(const CemiFrame & other): _data(buffer), - _npdu(_data + NPDU_LPDU_DIFF, *this), _tpdu(_data + TPDU_LPDU_DIFF, *this), _apdu(_data + APDU_LPDU_DIFF, *this) +CemiFrame::CemiFrame(const CemiFrame & other) + : _data(buffer), + _npdu(_data + NPDU_LPDU_DIFF, *this), + _tpdu(_data + TPDU_LPDU_DIFF, *this), + _apdu(_data + APDU_LPDU_DIFF, *this) { - _ctrl1 = _data + 2; + _ctrl1 = _data + CEMI_HEADER_SIZE; _length = other._length; memcpy(_data, other._data, other.totalLenght()); @@ -35,7 +108,7 @@ CemiFrame& CemiFrame::operator=(CemiFrame other) { _length = other._length; _data = buffer; - _ctrl1 = _data + 2; + _ctrl1 = _data + CEMI_HEADER_SIZE; memcpy(_data, other._data, other.totalLenght()); _npdu._data = _data + NPDU_LPDU_DIFF; _tpdu._data = _data + TPDU_LPDU_DIFF; @@ -84,10 +157,34 @@ void CemiFrame::fillTelegramTP(uint8_t* data) { memcpy(data, _ctrl1, len - 1); } - data[len - 1] = calcCRC(data, len - 1); + data[len - 1] = calcCrcTP(data, len - 1); } -uint8_t CemiFrame::calcCRC(uint8_t * buffer, uint16_t len) +uint16_t CemiFrame::telegramLengthtRF() const +{ + return totalLenght() - 3; +} + +void CemiFrame::fillTelegramRF(uint8_t* data) +{ + uint16_t len = telegramLengthtRF(); + + // We prepare the actual KNX telegram for RF here only. + // The packaging into blocks with CRC16 (Format based on FT3 Data Link Layer (IEC 870-5)) + // is done in the RF Data Link Layer code. + // RF always uses the Extended Frame Format. However, the length field is missing (right before the APDU) + // as there is already a length field at the beginning of the raw RF frame which is also used by the + // physical layer to control the HW packet engine of the transceiver. + + data[0] = _ctrl1[1] & 0x0F; // KNX CTRL field for RF (bits 3..0 EFF only), bits 7..4 are set to 0 for asynchronous RF frames + memcpy(data + 1, _ctrl1 + 2, 4); // SA, DA + data[5] = (_ctrl1[1] & 0xF0) | ((_rfLfn & 0x7) << 1) | ((_ctrl1[0] & 0x10) >> 4); // L/NPCI field: AT, Hopcount, LFN, AET + memcpy(data + 6, _ctrl1 + 7, len - 6); // APDU + + //printHex("cEMI_fill: ", &data[0], len); +} + +uint8_t CemiFrame::calcCrcTP(uint8_t * buffer, uint16_t len) { uint8_t crc = 0xFF; @@ -198,6 +295,36 @@ void CemiFrame::destinationAddress(uint16_t value) pushWord(value, _ctrl1 + 4); } +uint8_t* CemiFrame::rfSerialOrDoA() const +{ + return _rfSerialOrDoA; +} + +void CemiFrame::rfSerialOrDoA(uint8_t* rfSerialOrDoA) +{ + _rfSerialOrDoA = rfSerialOrDoA; +} + +uint8_t CemiFrame::rfInfo() const +{ + return _rfInfo; +} + +void CemiFrame::rfInfo(uint8_t rfInfo) +{ + _rfInfo = rfInfo; +} + +uint8_t CemiFrame::rfLfn() const +{ + return _rfLfn; +} + +void CemiFrame::rfLfn(uint8_t rfLfn) +{ + _rfLfn = rfLfn; +} + NPDU& CemiFrame::npdu() { return _npdu; diff --git a/src/knx/cemi_frame.h b/src/knx/cemi_frame.h index f8f69e6..d363bb3 100644 --- a/src/knx/cemi_frame.h +++ b/src/knx/cemi_frame.h @@ -12,6 +12,9 @@ #define TPDU_LPDU_DIFF (TPDU_NPDU_DIFF + NPDU_LPDU_DIFF) #define APDU_LPDU_DIFF (APDU_TPDU_DIFF + TPDU_NPDU_DIFF + NPDU_LPDU_DIFF) +// Mesg Code and additional info length +#define CEMI_HEADER_SIZE 2 + class CemiFrame { friend class DataLinkLayer; @@ -27,6 +30,8 @@ class CemiFrame uint16_t totalLenght() const; uint16_t telegramLengthtTP() const; void fillTelegramTP(uint8_t* data); + uint16_t telegramLengthtRF() const; + void fillTelegramRF(uint8_t* data); FrameFormat frameType() const; void frameType(FrameFormat value); @@ -47,11 +52,19 @@ class CemiFrame uint16_t destinationAddress() const; void destinationAddress(uint16_t value); + // only for RF medium + uint8_t* rfSerialOrDoA() const; + void rfSerialOrDoA(uint8_t* rfSerialOrDoA); + uint8_t rfInfo() const; + void rfInfo(uint8_t rfInfo); + uint8_t rfLfn() const; + void rfLfn(uint8_t rfInfo); + NPDU& npdu(); TPDU& tpdu(); APDU& apdu(); - uint8_t calcCRC(uint8_t* buffer, uint16_t len); + uint8_t calcCrcTP(uint8_t* buffer, uint16_t len); bool valid() const; private: @@ -62,4 +75,9 @@ class CemiFrame TPDU _tpdu; APDU _apdu; uint16_t _length = 0; // only set if created from byte array + + // nly for RF medium + uint8_t* _rfSerialOrDoA = 0; + uint8_t _rfInfo = 0; + uint8_t _rfLfn = 0; // RF Data Link layer frame number }; \ No newline at end of file diff --git a/src/knx/data_link_layer.cpp b/src/knx/data_link_layer.cpp index 1690eb4..ca3a107 100644 --- a/src/knx/data_link_layer.cpp +++ b/src/knx/data_link_layer.cpp @@ -14,15 +14,20 @@ DataLinkLayer::DataLinkLayer(DeviceObject& devObj, AddressTableObject& addrTab, void DataLinkLayer::dataRequest(AckType ack, AddressType addrType, uint16_t destinationAddr, FrameFormat format, Priority priority, NPDU& npdu) { - sendTelegram(npdu, ack, destinationAddr, addrType, format, priority); + // Normal data requests and broadcasts will always be transmitted as (domain) broadcast with domain address for open media (e.g. RF medium) + // The domain address "simulates" a closed medium (such as TP) on an open medium (such as RF or PL) + // See 3.2.5 p.22 + sendTelegram(npdu, ack, destinationAddr, addrType, format, priority, Broadcast); } void DataLinkLayer::systemBroadcastRequest(AckType ack, FrameFormat format, Priority priority, NPDU& npdu) { - sendTelegram(npdu, ack, 0, GroupAddress, format, priority); + // System Broadcast requests will always be transmitted as broadcast with KNX serial number for open media (e.g. RF medium) + // See 3.2.5 p.22 + sendTelegram(npdu, ack, 0, GroupAddress, format, priority, SysBroadcast); } -void DataLinkLayer::dataConReceived(CemiFrame& frame,bool success) +void DataLinkLayer::dataConReceived(CemiFrame& frame, bool success) { AckType ack = frame.ack(); AddressType addrType = frame.addressType(); @@ -49,12 +54,18 @@ void DataLinkLayer::frameRecieved(CemiFrame& frame) Priority priority = frame.priority(); NPDU& npdu = frame.npdu(); uint16_t ownAddr = _deviceObject.induvidualAddress(); + SystemBroadcast systemBroadcast = frame.systemBroadcast(); if (source == ownAddr) _deviceObject.induvidualAddressDuplication(true); if (addrType == GroupAddress && destination == 0) - _networkLayer.systemBroadcastIndication(ack, type, npdu, priority, source); + { + if (systemBroadcast == SysBroadcast) + _networkLayer.systemBroadcastIndication(ack, type, npdu, priority, source); + else + _networkLayer.dataIndication(ack, addrType, destination, type, npdu, priority, source); + } else { if (addrType == InduvidualAddress && destination != _deviceObject.induvidualAddress()) @@ -73,7 +84,7 @@ void DataLinkLayer::frameRecieved(CemiFrame& frame) } } -bool DataLinkLayer::sendTelegram(NPDU & npdu, AckType ack, uint16_t destinationAddr, AddressType addrType, FrameFormat format, Priority priority) +bool DataLinkLayer::sendTelegram(NPDU & npdu, AckType ack, uint16_t destinationAddr, AddressType addrType, FrameFormat format, Priority priority, SystemBroadcast systemBroadcast) { CemiFrame& frame = npdu.frame(); frame.messageCode(L_data_ind); @@ -82,6 +93,7 @@ bool DataLinkLayer::sendTelegram(NPDU & npdu, AckType ack, uint16_t destinationA frame.addressType(addrType); frame.priority(priority); frame.repetition(RepititionAllowed); + frame.systemBroadcast(systemBroadcast); if (npdu.octetCount() <= 15) frame.frameType(StandardFrame); diff --git a/src/knx/data_link_layer.h b/src/knx/data_link_layer.h index c1bd908..a08e524 100644 --- a/src/knx/data_link_layer.h +++ b/src/knx/data_link_layer.h @@ -23,7 +23,7 @@ class DataLinkLayer protected: void frameRecieved(CemiFrame& frame); void dataConReceived(CemiFrame& frame, bool success); - bool sendTelegram(NPDU& npdu, AckType ack, uint16_t destinationAddr, AddressType addrType, FrameFormat format, Priority priority); + bool sendTelegram(NPDU& npdu, AckType ack, uint16_t destinationAddr, AddressType addrType, FrameFormat format, Priority priority, SystemBroadcast systemBroadcast); virtual bool sendFrame(CemiFrame& frame) = 0; uint8_t* frameData(CemiFrame& frame); DeviceObject& _deviceObject; diff --git a/src/knx/device_object.cpp b/src/knx/device_object.cpp index 1784f62..f7a5f85 100644 --- a/src/knx/device_object.cpp +++ b/src/knx/device_object.cpp @@ -11,7 +11,7 @@ void DeviceObject::readProperty(PropertyID propertyId, uint32_t start, uint32_t& break; case PID_SERIAL_NUMBER: pushWord(_manufacturerId, data); - pushInt(_bauNumber, data); + pushInt(_bauNumber, data + 2); break; case PID_MANUFACTURER_ID: pushWord(_manufacturerId, data); @@ -55,8 +55,7 @@ void DeviceObject::readProperty(PropertyID propertyId, uint32_t start, uint32_t& break; } case PID_DEVICE_DESCRIPTOR: - data[0] = 0x57; - data[1] = 0xB0; + pushWord(_maskVersion, data); break; default: count = 0; @@ -254,6 +253,16 @@ void DeviceObject::version(uint16_t value) _version = value; } +uint16_t DeviceObject::maskVersion() +{ + return _maskVersion; +} + +void DeviceObject::maskVersion(uint16_t value) +{ + _maskVersion = value; +} + 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 6d66133..de86bb5 100644 --- a/src/knx/device_object.h +++ b/src/knx/device_object.h @@ -35,6 +35,8 @@ public: void hardwareType(const uint8_t* value); uint16_t version(); void version(uint16_t value); + uint16_t maskVersion(); + void maskVersion(uint16_t value); protected: uint8_t propertyCount(); PropertyDescription* propertyDescriptions(); @@ -44,8 +46,9 @@ private: uint8_t _prgMode = 0; uint16_t _ownAddress = 0; uint16_t _manufacturerId = 0xfa; //Default to KNXA - uint32_t _bauNumber = 0; + uint32_t _bauNumber = 0xaabbccdd; char _orderNumber[10] = ""; uint8_t _hardwareType[6] = { 0, 0, 0, 0, 0, 0}; uint16_t _version = 0; + uint16_t _maskVersion = 0x0000; }; \ No newline at end of file diff --git a/src/knx/interface_object.h b/src/knx/interface_object.h index 1452eca..2d2cfe8 100644 --- a/src/knx/interface_object.h +++ b/src/knx/interface_object.h @@ -47,7 +47,10 @@ enum ObjectType OT_RESERVED = 12, /** File Server Object */ - OT_FILE_SERVER = 13 + OT_FILE_SERVER = 13, + + /** RF Medium Object */ + OT_RF_MEDIUM = 19 }; /** diff --git a/src/knx/knx_types.h b/src/knx/knx_types.h index e4cd2c5..25afe8a 100644 --- a/src/knx/knx_types.h +++ b/src/knx/knx_types.h @@ -71,12 +71,30 @@ enum TpduType enum ApduType { + // Application Layer services on Multicast Communication Mode GroupValueRead = 0x000, GroupValueResponse = 0x040, GroupValueWrite = 0x080, + + // Application Layer services on Broadcast Communication Mode IndividualAddressWrite = 0x0c0, IndividualAddressRead = 0x100, IndividualAddressResponse = 0x140, + IndividualAddressSerialNumberRead = 0x3dc, + IndividualAddressSerialNumberResponse = 0x3dd, + IndividualAddressSerialNumberWrite = 0x3de, + + // Application Layer Services on System Broadcast communication mode + SystemNetworkParameterRead = 0x1c8, + SystemNetworkParameterResponse = 0x1c9, + SystemNetworkParameterWrite = 0x1ca, + // Open media specific Application Layer Services on System Broadcast communication mode + DomainAddressSerialNumberRead = 0x3ec, + DomainAddressSerialNumberResponse = 0x3ed, + DomainAddressSerialNumberWrite = 0x3ee, + + // Application Layer Services on Point-to-point Connection-Oriented Communication Mode (mandatory) + // Application Layer Services on Point-to-point Connectionless Communication Mode (either optional or mandatory) MemoryRead = 0x200, MemoryResponse = 0x240, MemoryWrite = 0x280, @@ -97,7 +115,14 @@ enum ApduType PropertyValueWrite = 0x3d7, PropertyDescriptionRead = 0x3d8, PropertyDescriptionResponse = 0x3d9, - IndividualAddressSerialNumberRead = 0x3dc, - IndividualAddressSerialNumberResponse = 0x3dd, - IndividualAddressSerialNumberWrite = 0x3de, +}; + +enum KnxMediumType +{ + KnxMediumType_TP1 = 0, + KnxMediumType_PL = 1, + KnxMediumType_RF = 2, + KnxMediumType_TP0 = 3, // not supported anymore + KnxMediumType_PL132 = 4, // not supported anymore + KnxMediumType_IP = 5, }; \ No newline at end of file diff --git a/src/knx/network_layer.cpp b/src/knx/network_layer.cpp index ceab6b6..4089c2e 100644 --- a/src/knx/network_layer.cpp +++ b/src/knx/network_layer.cpp @@ -69,7 +69,7 @@ void NetworkLayer::dataConfirm(AckType ack, AddressType addressType, uint16_t de void NetworkLayer::systemBroadcastIndication(AckType ack, FrameFormat format, NPDU& npdu, Priority priority, uint16_t source) { HopCountType hopType = npdu.hopCount() == 7 ? UnlimitedRouting : NetworkLayerParameter; - _transportLayer.dataBroadcastIndication(hopType, priority, source, npdu.tpdu()); + _transportLayer.dataSystemBroadcastIndication(hopType, priority, source, npdu.tpdu()); } void NetworkLayer::systemBroadcastConfirm(AckType ack, FrameFormat format, Priority priority, uint16_t source, NPDU& npdu, bool status) diff --git a/src/knx/platform.h b/src/knx/platform.h index 497d98e..903ad32 100644 --- a/src/knx/platform.h +++ b/src/knx/platform.h @@ -29,6 +29,15 @@ class Platform virtual int readUart() = 0; virtual size_t readBytesUart(uint8_t* buffer, size_t length) = 0; + virtual void setupSpi() = 0; + virtual void closeSpi() = 0; + virtual int readWriteSpi (uint8_t *data, size_t len) = 0; + + virtual void setupGpio(uint32_t dwPin, uint32_t dwMode) = 0; + virtual void closeGpio(uint32_t dwPin) = 0; + virtual void writeGpio(uint32_t dwPin, uint32_t dwVal) = 0; + virtual uint32_t readGpio(uint32_t dwPin) = 0; + virtual uint8_t* getEepromBuffer(uint16_t size) = 0; virtual void commitToEeprom() = 0; diff --git a/src/knx/property_types.h b/src/knx/property_types.h index 5f392e8..088c741 100644 --- a/src/knx/property_types.h +++ b/src/knx/property_types.h @@ -96,6 +96,17 @@ enum PropertyID PID_HARDWARE_TYPE = 78, PID_DEVICE_DESCRIPTOR = 83, + /** Properties in the RF Medium Object */ + PID_RF_MULTI_TYPE = 51, + PID_RF_DOMAIN_ADDRESS = 56, + PID_RF_RETRANSMITTER = 57, + PID_RF_FILTERING_MODE_SUPPORT = 58, + PID_RF_FILTERING_MODE_SELECT = 59, + PID_RF_BIDIR_TIMEOUT = 60, + PID_RF_DIAG_SA_FILTER_TABLE = 61, + PID_RF_DIAG_BUDGET_TABLE = 62, + PID_RF_DIAG_PROBE = 63, + /** KNXnet/IP Parameter Object */ PID_PROJECT_INSTALLATION_ID = 51, PID_KNX_INDIVIDUAL_ADDRESS = 52, diff --git a/src/knx/rf_data_link_layer.cpp b/src/knx/rf_data_link_layer.cpp new file mode 100644 index 0000000..253083e --- /dev/null +++ b/src/knx/rf_data_link_layer.cpp @@ -0,0 +1,365 @@ +#include "rf_physical_layer.h" +#include "rf_data_link_layer.h" + +#include "bits.h" +#include "platform.h" +#include "device_object.h" +#include "address_table_object.h" +#include "rf_medium_object.h" +#include "cemi_frame.h" + +#include +#include + +void RfDataLinkLayer::loop() +{ + if (!_enabled) + return; + + _rfPhy.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) + { + uint8_t knxSerialNumber[6]; + pushWord(_deviceObject.manufacturerId(), &knxSerialNumber[0]); + pushInt(_deviceObject.bauNumber(), &knxSerialNumber[2]); + frame.rfSerialOrDoA(&knxSerialNumber[0]); + } + 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; + + // bidirectional device, battery is ok, signal strength indication is void (no measurement) + frame.rfInfo(0x02); + + // TODO: Is queueing really required? + // According to the spec. the upper layer may only send a new L_Data.req if it received + // the L_Data.con for the previous L_Data.req. + addFrameTxQueue(frame); + + // TODO: For now L_data.req is confirmed immediately (L_Data.con) + // see 3.6.3 p.80: L_Data.con shall be generated AFTER transmission of the corresponsing frame + // RF sender will never generate L_Data.con with C=1 (Error), but only if the TX buffer overflows + // The RF sender cannot detect if the RF frame was transmitted successfully or not according to the spec. + dataConReceived(frame, true); + + return true; +} + +RfDataLinkLayer::RfDataLinkLayer(DeviceObject& devObj, RfMediumObject& rfMediumObj, AddressTableObject& addrTab, + NetworkLayer& layer, Platform& platform) + : DataLinkLayer(devObj, addrTab, layer, platform), + _rfMediumObj(rfMediumObj), + _rfPhy(*this, platform) +{ +} + +uint16_t RfDataLinkLayer::calcCrcRF(uint8_t* buffer, uint32_t offset, uint32_t len) +{ + // CRC-16-DNP + // generator polynomial = 2^16 + 2^13 + 2^12 + 2^11 + 2^10 + 2^8 + 2^6 + 2^5 + 2^2 + 2^0 + uint32_t pn = 0x13d65; // 1 0011 1101 0110 0101 + + // for much data, using a lookup table would be a way faster CRC calculation + uint32_t crc = 0; + for (uint32_t i = offset; i < offset + len; i++) { + uint8_t bite = buffer[i] & 0xff; + for (uint8_t b = 8; b --> 0;) { + bool bit = ((bite >> b) & 1) == 1; + bool one = (crc >> 15 & 1) == 1; + crc <<= 1; + if (one ^ bit) + crc ^= pn; + } + } + return (~crc) & 0xffff; +} + +void RfDataLinkLayer::frameBytesReceived(uint8_t* rfPacketBuf, uint16_t length) +{ + // RF data link layer frame format + // See 3.2.5 p.22 + + // First block + smallest KNX telegram will give a minimum size of 22 bytes with checksum bytes + if (length < 21) + { + print("Received packet is too small. length: "); + println(length); + return; + } + + // CRC16-DNP of first block is always located here + uint16_t block1Crc = rfPacketBuf[10] << 8 | rfPacketBuf[11]; + + // If the checksum was ok and the other + // two constant header bytes match the KNX-RF spec. (C-field: 0x44 and ESC-field: 0xFF)... + // then we seem to have a valid first block of an KNX RF frame. + // The first block basically contains the RF-info field and the KNX SN/Domain address. + if ((rfPacketBuf[1] == 0x44) && + (rfPacketBuf[2] == 0xFF) && + (calcCrcRF(rfPacketBuf, 0, 10) == block1Crc)) + { + // bytes left from the remaining block(s) + uint16_t bytesLeft = length - 12; + // we use two pointers to move over the two buffers + uint8_t* pRfPacketBuf = &rfPacketBuf[12]; // pointer to start of RF frame block 2 (with CTRL field) + // Reserve 1 byte (+1) for the second ctrl field + // cEMI frame has two CTRL fields, but RF frame has only one, but uses ALWAYS extended frames + // Information for BOTH cEMI CTRL fields is distributed in a RF frame (RF CTRL field and RF L/NPCI field) + // So we cannot just copy an RF frame with CTRL fields as is + // KNX RF frame will be placed starting at cEMI CTRL2 field (so RF CTRL field is CTRL2 field cEMI) + uint8_t* pBuffer = &_buffer[CEMI_HEADER_SIZE + 1]; + // New length of the packet with CRC bytes removed, add space for CEMI header and the second CTRL field + uint16_t newLength = CEMI_HEADER_SIZE + 1; + + // Now check each block checksum and copy the payload of the block + // into a new buffer without checksum + uint16_t blockCrc; + bool crcOk = true; + while (bytesLeft > 18) + { + // Get CRC16 from end of the block + blockCrc = pRfPacketBuf[16] << 8 | pRfPacketBuf[17]; + if (calcCrcRF(pRfPacketBuf, 0, 16) == blockCrc) + { + // Copy only the payload without the checksums + memcpy(pBuffer, pRfPacketBuf, 16); + } + else + { + crcOk = false; + break; + } + pBuffer += 16; + pRfPacketBuf += 18; + newLength += 16; + bytesLeft -= 18; + } + + // Now process the last block + blockCrc = pRfPacketBuf[bytesLeft - 2] << 8 | pRfPacketBuf[bytesLeft - 1]; + crcOk = crcOk && (calcCrcRF(&pRfPacketBuf[0], 0, bytesLeft -2) == blockCrc); + + // If all checksums were ok, then... + if (crcOk) + { + // Copy rest of the received packet without checksum + memcpy(pBuffer, pRfPacketBuf, bytesLeft -2); + newLength += bytesLeft -2; + + // 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[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) + + // Now get all control bits from the L/NPCI field of the RF frame + // so that we can overwrite it afterwards with the correct NPDU length + // Get data link layer frame number (LFN field) from L/NPCI.LFN (bit 3..1) + uint8_t lfn = (_buffer[8] & 0x0E) >> 1; + // Get address type from L/NPCI.LFN (bit 7) + AddressType addressType = (_buffer[8] & 0x80) ? GroupAddress:InduvidualAddress; + // Get routing counter from L/NPCI.LFN (bit 6..4) and map to hop count in Ctrl2.b6-4 + uint8_t hopCount = (_buffer[8] & 0x70) >> 4; + // Get AddrExtensionType from L/NPCI.LFN (bit 7) and map to system broadcast flag in Ctrl1.b4 + SystemBroadcast systemBroadcast = (_buffer[8] & 0x01) ? Broadcast:SysBroadcast; + + // Setup L field of the cEMI frame with the NPDU length + // newLength -8 bytes (NPDU_LPDU_DIFF, no AddInfo) -1 byte length field -1 byte TPCI/APCI bits + _buffer[8] = newLength - NPDU_LPDU_DIFF - 1 - 1; + + // If we have a broadcast message (within the domain), + // 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 + // If it does not match then skip the remaining processing + if (memcmp(_rfMediumObj.rfDomainAddress(), &rfPacketBuf[4], 6)) + { + println("RX domain address does not match. Skipping..."); + return; + } + } + + // TODO + // Frame duplication prevention based on LFN (see KKNX RF spec. 3.2.5 p.28) + + // Prepare the cEMI frame + CemiFrame frame(_buffer, newLength); + frame.frameType(ExtendedFrame); // KNX RF uses only extended frame format + frame.priority(SystemPriority); // Not used in KNX RF + frame.ack(AckDontCare); // Not used in KNX RF + frame.systemBroadcast(systemBroadcast); // Mapped from flag AddrExtensionType (KNX serial(0) or Domain Address(1)) + frame.hopCount(hopCount); // Hop count from routing counter + frame.addressType(addressType); // Group address or individual address + frame.rfSerialOrDoA(&rfPacketBuf[4]); // Copy pointer to field Serial or Domain Address (check broadcast flag what it is exactly) + frame.rfInfo(rfPacketBuf[3]); // RF-info field (1 byte) + frame.rfLfn(lfn); // Data link layer frame number (LFN field) +/* + print("RX LFN: "); + print(lfn); + print(" len: "); + print(newLength); + + print(" data: "); + printHex(" data: ", _buffer, newLength); +*/ + frameRecieved(frame); + } + } +} + +void RfDataLinkLayer::enabled(bool value) +{ + if (value && !_enabled) + { + if (_rfPhy.InitChip()) + { + _enabled = true; + print("ownaddr "); + println(_deviceObject.induvidualAddress(), HEX); + } + else + { + _enabled = false; + println("ERROR, RF transceiver not responding"); + } + return; + } + + if (!value && _enabled) + { + _rfPhy.stopChip(); + _enabled = false; + return; + } +} + +bool RfDataLinkLayer::enabled() const +{ + return _enabled; +} + +void RfDataLinkLayer::fillRfFrame(CemiFrame& frame, uint8_t* data) +{ + uint16_t crc; + uint16_t length = frame.telegramLengthtRF(); + + data[0] = 9 + length; // Length block1 (always 9 bytes, without length itself) + Length of KNX telegram without CRCs + data[1] = 0x44; // C field: According to IEC870-5. KNX only uses SEND/NO REPLY (C = 44h) + data[2] = 0xFF; // ESC field: This field shall have the fixed value FFh. + data[3] = frame.rfInfo(); // RF-info field + + // Generate CRC16-DNP over the first block of data + pushByteArray(frame.rfSerialOrDoA(), 6, &data[4]); + crc = calcCrcRF(&data[0], 0, 10); + pushWord(crc, &data[10]); + + // Put the complete KNX telegram into a temporary buffer + // as we have to add CRC16 checksums after each block of 16 bytes + frame.fillTelegramRF(_buffer); + + // Create a checksum for each block of full 16 bytes + uint16_t bytesLeft = length; + uint8_t *pBuffer = &_buffer[0]; + uint8_t *pData = &data[12]; + while (bytesLeft > 16) + { + memcpy(pData, pBuffer, 16); + crc = calcCrcRF(pData, 0, 16); + pushWord(crc, &pData[16]); + + pBuffer += 16; + pData += 18; + bytesLeft -= 16; + } + + // Copy remaining bytes of last block. Could be less than 16 bytes + memcpy(pData, pBuffer, bytesLeft); + // And add last CRC + crc = calcCrcRF(pData, 0, bytesLeft); + pushWord(crc, &pData[bytesLeft]); +} + +void RfDataLinkLayer::addFrameTxQueue(CemiFrame& frame) +{ + _tx_queue_frame_t* tx_frame = new _tx_queue_frame_t; + + uint16_t length = frame.telegramLengthtRF(); // Just the pure KNX telegram from CTRL field until end of APDU + uint8_t nrFullBlocks = length / 16; // Number of full (16 bytes) RF blocks required + uint8_t bytesLeft = length % 16; // Remaining bytes of the last packet + + // Calculate total number of bytes required to store the complete raw RF frame + // Block1 always requires 12 bytes including Length and CRC + // Each full block has 16 bytes payload plus 2 bytes CRC + // Add remaining bytes of the last block and add 2 bytes for CRC + uint16_t totalLength = 12 + (nrFullBlocks * 18) + bytesLeft + 2; + + tx_frame->length = totalLength; + tx_frame->data = new uint8_t[tx_frame->length]; + tx_frame->next = NULL; + + // Prepare the raw RF frame + fillRfFrame(frame, tx_frame->data); +/* + print("TX LFN: "); + print(frame.rfLfn()); + + print(" len: "); + print(totalLength); + + printHex(" data:", tx_frame->data, totalLength); +*/ + if (_tx_queue.back == NULL) + { + _tx_queue.front = _tx_queue.back = tx_frame; + } + else + { + _tx_queue.back->next = tx_frame; + _tx_queue.back = tx_frame; + } +} + +bool RfDataLinkLayer::isTxQueueEmpty() +{ + if (_tx_queue.front == NULL) + { + return true; + } + return false; +} + +void RfDataLinkLayer::loadNextTxFrame(uint8_t** sendBuffer, uint16_t* sendBufferLength) +{ + if (_tx_queue.front == NULL) + { + return; + } + _tx_queue_frame_t* tx_frame = _tx_queue.front; + *sendBuffer = tx_frame->data; + *sendBufferLength = tx_frame->length; + _tx_queue.front = tx_frame->next; + + if (_tx_queue.front == NULL) + { + _tx_queue.back = NULL; + } + delete tx_frame; +} diff --git a/src/knx/rf_data_link_layer.h b/src/knx/rf_data_link_layer.h new file mode 100644 index 0000000..10092ca --- /dev/null +++ b/src/knx/rf_data_link_layer.h @@ -0,0 +1,58 @@ +#pragma once + +#include +#include "data_link_layer.h" + +#define MAX_KNX_TELEGRAM_SIZE 263 + +class RfPhysicalLayer; +class RfMediumObject; + +class RfDataLinkLayer : public DataLinkLayer +{ + friend class RfPhysicalLayer; + + using DataLinkLayer::_deviceObject; + using DataLinkLayer::_groupAddressTable; + using DataLinkLayer::_platform; + + public: + RfDataLinkLayer(DeviceObject& devObj, RfMediumObject& rfMediumObj, AddressTableObject& addrTab, NetworkLayer& layer, + Platform& platform); + + void loop(); + void enabled(bool value); + bool enabled() const; + + private: + bool _enabled = false; + uint8_t _loopState = 0; + + uint8_t _buffer[512]; + + uint8_t _frameNumber = 0; + + struct _tx_queue_frame_t + { + uint8_t* data; + uint16_t length; + _tx_queue_frame_t* next; + }; + + struct _tx_queue_t + { + _tx_queue_frame_t* front = NULL; + _tx_queue_frame_t* back = NULL; + } _tx_queue; + + RfMediumObject& _rfMediumObj; + RfPhysicalLayer _rfPhy; + + void fillRfFrame(CemiFrame& frame, uint8_t* data); + void addFrameTxQueue(CemiFrame& frame); + bool isTxQueueEmpty(); + void loadNextTxFrame(uint8_t** sendBuffer, uint16_t* sendBufferLength); + bool sendFrame(CemiFrame& frame); + void frameBytesReceived(uint8_t* buffer, uint16_t length); + uint16_t calcCrcRF(uint8_t* buffer, uint32_t offset, uint32_t len); +}; diff --git a/src/knx/rf_medium_object.cpp b/src/knx/rf_medium_object.cpp new file mode 100644 index 0000000..2e96d9d --- /dev/null +++ b/src/knx/rf_medium_object.cpp @@ -0,0 +1,128 @@ +#include +#include "rf_medium_object.h" +#include "bits.h" + +void RfMediumObject::readProperty(PropertyID propertyId, uint32_t start, uint32_t& count, uint8_t* data) +{ + switch (propertyId) + { + case PID_OBJECT_TYPE: + pushWord(OT_RF_MEDIUM, data); + break; + case PID_RF_MULTI_TYPE: + data[0] = 0x00; // KNX RF ready only + break; + case PID_RF_DOMAIN_ADDRESS: + pushByteArray((uint8_t*)_rfDomainAddress, 6, data); + break; + case PID_RF_RETRANSMITTER: + data[0] = 0x00; // No KNX RF retransmitter + break; + case PID_RF_BIDIR_TIMEOUT: // PDT_FUNCTION + data[0] = 0x00; // success + data[1] = 0xFF; // permanent bidirectional device + data[2] = 0xFF; // permanent bidirectional device + break; + case PID_RF_DIAG_SA_FILTER_TABLE: // PDT_GENERIC_03[] + pushByteArray((uint8_t*)_rfDiagSourceAddressFilterTable, 24, data); + break; + case PID_RF_DIAG_BUDGET_TABLE: + pushByteArray((uint8_t*)_rfDiagLinkBudgetTable, 24, data); + break; + case PID_RF_DIAG_PROBE: // PDT_FUNCTION + // Not supported yet + break; + default: + count = 0; + } +} + +void RfMediumObject::writeProperty(PropertyID id, uint8_t start, uint8_t* data, uint8_t count) +{ + switch (id) + { + case PID_RF_DOMAIN_ADDRESS: + for (uint8_t i = start; i < start + count; i++) + _rfDomainAddress[i-1] = data[i - start]; + break; + case PID_RF_BIDIR_TIMEOUT: // PDT_FUNCTION + // Not supported yet (permanent bidir device) + break; + case PID_RF_DIAG_SA_FILTER_TABLE: + for (uint8_t i = start; i < start + count; i++) + _rfDiagSourceAddressFilterTable[i-1] = data[i - start]; + break; + case PID_RF_DIAG_BUDGET_TABLE: + for (uint8_t i = start; i < start + count; i++) + _rfDiagLinkBudgetTable[i-1] = data[i - start]; + break; + case PID_RF_DIAG_PROBE: + // Not supported yet + break; + default: + break; + } +} + +uint8_t RfMediumObject::propertySize(PropertyID id) +{ + switch (id) + { + case PID_RF_MULTI_TYPE: + case PID_RF_RETRANSMITTER: + return 1; + case PID_OBJECT_TYPE: + return 2; + case PID_RF_DOMAIN_ADDRESS: + return 6; + case PID_RF_DIAG_SA_FILTER_TABLE: + case PID_RF_DIAG_BUDGET_TABLE: + return 24; + // case PID_RF_BIDIR_TIMEOUT: ? + // case PID_RF_DIAG_PROBE: ? + default: + break; + } + return 0; +} + +uint8_t* RfMediumObject::save(uint8_t* buffer) +{ + buffer = pushByteArray((uint8_t*)_rfDomainAddress, 6, buffer); + return buffer; +} + +uint8_t* RfMediumObject::restore(uint8_t* buffer) +{ + buffer = popByteArray((uint8_t*)_rfDomainAddress, 6, buffer); + return buffer; +} + +uint8_t* RfMediumObject::rfDomainAddress() +{ + return _rfDomainAddress; +} + +void RfMediumObject::rfDomainAddress(uint8_t* value) +{ + pushByteArray(value, 6, _rfDomainAddress); +} + +static PropertyDescription _propertyDescriptions[] = +{ + { PID_OBJECT_TYPE, false, PDT_UNSIGNED_INT, 1, ReadLv3 | WriteLv0 }, + { PID_RF_MULTI_TYPE, false, PDT_GENERIC_01, 1, ReadLv3 | WriteLv0 }, + { PID_RF_RETRANSMITTER, false, PDT_GENERIC_01, 1, ReadLv3 | WriteLv0 }, + { PID_RF_DOMAIN_ADDRESS, true, PDT_GENERIC_06, 1, ReadLv3 | WriteLv0 } +}; +static uint8_t _propertyCount = sizeof(_propertyDescriptions) / sizeof(PropertyDescription); + +uint8_t RfMediumObject::propertyCount() +{ + return _propertyCount; +} + +PropertyDescription* RfMediumObject::propertyDescriptions() +{ + return _propertyDescriptions; +} diff --git a/src/knx/rf_medium_object.h b/src/knx/rf_medium_object.h new file mode 100644 index 0000000..57c0a2e --- /dev/null +++ b/src/knx/rf_medium_object.h @@ -0,0 +1,27 @@ +#pragma once + +#include "interface_object.h" + +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); + 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); + + uint8_t* rfDomainAddress(); + void rfDomainAddress(uint8_t* value); + +protected: + uint8_t propertyCount(); + PropertyDescription* propertyDescriptions(); +private: + uint8_t _rfDomainAddress[6] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; // see KNX RF S-Mode AN160 p.11 + uint8_t _rfDiagSourceAddressFilterTable[24] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}; + uint8_t _rfDiagLinkBudgetTable[24] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}; + + +}; \ No newline at end of file diff --git a/src/knx/rf_physical_layer.cpp b/src/knx/rf_physical_layer.cpp new file mode 100644 index 0000000..03f551a --- /dev/null +++ b/src/knx/rf_physical_layer.cpp @@ -0,0 +1,799 @@ +#include "rf_physical_layer.h" +#include "rf_data_link_layer.h" + +#include "bits.h" +#include "platform.h" + +#include +#include + +#define MIN(a, b) ((a < b) ? (a) : (b)) +#define MAX(a, b) ((a > b) ? (a) : (b)) +#define ABS(x) ((x > 0) ? (x) : (-x)) + +// Table for encoding 4-bit data into a 8-bit Manchester encoding. +const uint8_t RfPhysicalLayer::manchEncodeTab[16] = {0xAA, // 0x0 Manchester encoded + 0xA9, // 0x1 Manchester encoded + 0xA6, // 0x2 Manchester encoded + 0xA5, // 0x3 Manchester encoded + 0x9A, // 0x4 Manchester encoded + 0x99, // 0x5 Manchester encoded + 0x96, // 0x6 Manchester encoded + 0x95, // 0x7 Manchester encoded + 0x6A, // 0x8 Manchester encoded + 0x69, // 0x9 Manchester encoded + 0x66, // 0xA Manchester encoded + 0x65, // 0xB Manchester encoded + 0x5A, // 0xC Manchester encoded + 0x59, // 0xD Manchester encoded + 0x56, // 0xE Manchester encoded + 0x55}; // 0xF Manchester encoded + +// Table for decoding 4-bit Manchester encoded data into 2-bit +// data. 0xFF indicates invalid Manchester encoding +const uint8_t RfPhysicalLayer::manchDecodeTab[16] = {0xFF, // Manchester encoded 0x0 decoded + 0xFF, // Manchester encoded 0x1 decoded + 0xFF, // Manchester encoded 0x2 decoded + 0xFF, // Manchester encoded 0x3 decoded + 0xFF, // Manchester encoded 0x4 decoded + 0x03, // Manchester encoded 0x5 decoded + 0x02, // Manchester encoded 0x6 decoded + 0xFF, // Manchester encoded 0x7 decoded + 0xFF, // Manchester encoded 0x8 decoded + 0x01, // Manchester encoded 0x9 decoded + 0x00, // Manchester encoded 0xA decoded + 0xFF, // Manchester encoded 0xB decoded + 0xFF, // Manchester encoded 0xC decoded + 0xFF, // Manchester encoded 0xD decoded + 0xFF, // Manchester encoded 0xE decoded + 0xFF};// Manchester encoded 0xF decoded + +// Product = CC1101 +// Chip version = A (VERSION = 0x04) +// Crystal accuracy = 10 ppm +// X-tal frequency = 26 MHz +// RF output power = + 10 dBm +// RX filterbandwidth = 270 kHz +// Deviation = 47 kHz +// Datarate = 32.73 kBaud +// Modulation = (0) 2-FSK +// Manchester enable = (0) Manchester disabled +// RF Frequency = 868.299866 MHz +// Channel spacing = 199.951172 kHz +// Channel number = 0 +// Optimization = - +// Sync mode = (5) 15/16 + carrier-sense above threshold +// Format of RX/TX data = (0) Normal mode, use FIFOs for RX and TX +// CRC operation = (0) CRC disabled for TX and RX +// Forward Error Correction = (0) FEC disabled +// Length configuration = (0) Fixed length packets, length configured in PKTLEN register. +// Packetlength = 255 +// Preamble count = (2) 4 bytes +// Append status = 0 +// Address check = (0) No address check +// FIFO autoflush = 0 +// Device address = 0 +// GDO0 signal selection = ( 6) Asserts when sync word has been sent / received, and de-asserts at the end of the packet +// GDO2 signal selection = ( 0) Asserts when RX FiFO threshold +const uint8_t RfPhysicalLayer::cc1101_2FSK_32_7_kb[CFG_REGISTER] = { + 0x00, // IOCFG2 GDO2 Output Pin Configuration + 0x2E, // IOCFG1 GDO1 Output Pin Configuration + 0x06, // IOCFG0 GDO0 Output Pin Configuration + 0x40, // FIFOTHR RX FIFO and TX FIFO Thresholds // 4 bytes in RX FIFO (2 bytes manchester encoded) + 0x76, // SYNC1 Sync Word + 0x96, // SYNC0 Sync Word + 0xFF, // PKTLEN Packet Length + 0x00, // PKTCTRL1 Packet Automation Control + 0x00, // PKTCTRL0 Packet Automation Control + 0x00, // ADDR Device Address + 0x00, // CHANNR Channel Number + 0x08, // FSCTRL1 Frequency Synthesizer Control + 0x00, // FSCTRL0 Frequency Synthesizer Control + 0x21, // FREQ2 Frequency Control Word + 0x65, // FREQ1 Frequency Control Word + 0x6A, // FREQ0 Frequency Control Word + 0x6A, // MDMCFG4 Modem Configuration + 0x4A, // MDMCFG3 Modem Configuration + 0x05, // MDMCFG2 Modem Configuration + 0x22, // MDMCFG1 Modem Configuration + 0xF8, // MDMCFG0 Modem Configuration + 0x47, // DEVIATN Modem Deviation Setting + 0x07, // MCSM2 Main Radio Control State Machine Configuration + 0x30, // MCSM1 Main Radio Control State Machine Configuration (IDLE after TX and RX) + 0x18, // MCSM0 Main Radio Control State Machine Configuration + 0x2E, // FOCCFG Frequency Offset Compensation Configuration + 0x6D, // BSCFG Bit Synchronization Configuration + 0x43, // AGCCTRL2 AGC Control 0x04, // AGCCTRL2 magn target 33dB vs 36dB (max LNA+LNA2 gain vs. ) (highest gain cannot be used vs. all gain settings) + 0x40, // AGCCTRL1 AGC Control 0x09, // AGCCTRL1 carrier sense threshold disabled vs. 7dB below magn target (LNA prio strat. 1 vs 0) + 0x91, // AGCCTRL0 AGC Control 0xB2, // AGCCTRL0 channel filter samples 16 vs.32 + 0x87, // WOREVT1 High Byte Event0 Timeout + 0x6B, // WOREVT0 Low Byte Event0 Timeout + 0xFB, // WORCTRL Wake On Radio Control + 0xB6, // FREND1 Front End RX Configuration + 0x10, // FREND0 Front End TX Configuration + 0xE9, // FSCAL3 Frequency Synthesizer Calibration 0xEA, // FSCAL3 + 0x2A, // FSCAL2 Frequency Synthesizer Calibration + 0x00, // FSCAL1 Frequency Synthesizer Calibration + 0x1F, // FSCAL0 Frequency Synthesizer Calibration + 0x41, // RCCTRL1 RC Oscillator Configuration + 0x00, // RCCTRL0 RC Oscillator Configuration + 0x59, // FSTEST Frequency Synthesizer Calibration Control + 0x7F, // PTEST Production Test + 0x3F, // AGCTEST AGC Test + 0x81, // TEST2 Various Test Settings + 0x35, // TEST1 Various Test Settings + 0x09 // TEST0 Various Test Settings +}; + + //Patable index: -30 -20- -15 -10 0 5 7 10 dBm +const uint8_t RfPhysicalLayer::paTablePower868[8] = {0x03,0x17,0x1D,0x26,0x50,0x86,0xCD,0xC0}; + +RfPhysicalLayer::RfPhysicalLayer(RfDataLinkLayer& rfDataLinkLayer, Platform& platform) + : _rfDataLinkLayer(rfDataLinkLayer), + _platform(platform) +{ +} + +void RfPhysicalLayer::manchEncode(uint8_t *uncodedData, uint8_t *encodedData) +{ + uint8_t data0, data1; + + // - Shift to get 4-bit data values + data1 = (((*uncodedData) >> 4) & 0x0F); + data0 = ((*uncodedData) & 0x0F); + + // - Perform Manchester encoding - + *encodedData = (manchEncodeTab[data1]); + *(encodedData + 1) = manchEncodeTab[data0]; +} + +bool RfPhysicalLayer::manchDecode(uint8_t *encodedData, uint8_t *decodedData) +{ + uint8_t data0, data1, data2, data3; + + // - Shift to get 4 bit data and decode + data3 = ((*encodedData >> 4) & 0x0F); + data2 = ( *encodedData & 0x0F); + data1 = ((*(encodedData + 1) >> 4) & 0x0F); + data0 = ((*(encodedData + 1)) & 0x0F); + + // Check for invalid Manchester encoding + if ( (manchDecodeTab[data3] == 0xFF ) | (manchDecodeTab[data2] == 0xFF ) | + (manchDecodeTab[data1] == 0xFF ) | (manchDecodeTab[data0] == 0xFF ) ) + { + return false; + } + + // Shift result into a byte + *decodedData = (manchDecodeTab[data3] << 6) | (manchDecodeTab[data2] << 4) | + (manchDecodeTab[data1] << 2) | manchDecodeTab[data0]; + + return true; +} + +int RfPhysicalLayer::crc16(uint8_t* buffer, int offset, int length) +{ + // CRC-16-DNP + // generator polynomial = 2^16 + 2^13 + 2^12 + 2^11 + 2^10 + 2^8 + 2^6 + 2^5 + 2^2 + 2^0 + int pn = 0x13d65; // 1 0011 1101 0110 0101 + + // for much data, using a lookup table would be a way faster CRC calculation + int crc = 0; + for (int i = offset; i < offset + length; i++) { + int bite = buffer[i] & 0xff; + for (int b = 8; b --> 0;) { + bool bit = ((bite >> b) & 1) == 1; + bool one = (crc >> 15 & 1) == 1; + crc <<= 1; + if (one ^ bit) + crc ^= pn; + } + } + return (~crc) & 0xffff; +} + +uint8_t RfPhysicalLayer::sIdle() +{ + uint8_t marcState; + uint32_t timeStart; + + spiWriteStrobe(SIDLE); //sets to idle first. must be in + + marcState = 0xFF; //set unknown/dummy state value + timeStart = millis(); + while((marcState != MARCSTATE_IDLE) && ((millis() - timeStart) < CC1101_TIMEOUT)) //0x01 = sidle + { + marcState = (spiReadRegister(MARCSTATE) & MARCSTATE_BITMASK); //read out state of cc1101 to be sure in RX + } + + //print("marcstate: 0x"); + //println(marcState, HEX); + + if(marcState != MARCSTATE_IDLE) + { + println("Timeout when trying to set idle state."); + return false; + } + + return true; +} + +uint8_t RfPhysicalLayer::sReceive() +{ + uint8_t marcState; + uint32_t timeStart; + + spiWriteStrobe(SRX); //writes receive strobe (receive mode) + + marcState = 0xFF; //set unknown/dummy state value + timeStart = millis(); + while((marcState != MARCSTATE_RX) && ((millis() - timeStart) < CC1101_TIMEOUT)) //0x0D = RX + { + marcState = (spiReadRegister(MARCSTATE) & MARCSTATE_BITMASK); //read out state of cc1101 to be sure in RX + } + + //print("marcstate: 0x"); + //println(marcState, HEX); + + if(marcState != MARCSTATE_RX) + { + println("Timeout when trying to set receive state."); + return false; + } + + return true; +} + +void RfPhysicalLayer::spiWriteRegister(uint8_t spi_instr, uint8_t value) +{ + uint8_t tbuf[2] = {0}; + tbuf[0] = spi_instr | WRITE_SINGLE_BYTE; + tbuf[1] = value; + uint8_t len = 2; + _platform.writeGpio(SPI_SS_PIN, LOW); + _platform.readWriteSpi(tbuf, len); + _platform.writeGpio(SPI_SS_PIN, HIGH); +} + +uint8_t RfPhysicalLayer::spiReadRegister(uint8_t spi_instr) +{ + uint8_t value; + uint8_t rbuf[2] = {0}; + rbuf[0] = spi_instr | READ_SINGLE_BYTE; + uint8_t len = 2; + _platform.writeGpio(SPI_SS_PIN, LOW); + _platform.readWriteSpi(rbuf, len); + _platform.writeGpio(SPI_SS_PIN, HIGH); + value = rbuf[1]; + //printf("SPI_arr_0: 0x%02X\n", rbuf[0]); + //printf("SPI_arr_1: 0x%02X\n", rbuf[1]); + return value; +} + +uint8_t RfPhysicalLayer::spiWriteStrobe(uint8_t spi_instr) +{ + uint8_t tbuf[1] = {0}; + tbuf[0] = spi_instr; + //printf("SPI_data: 0x%02X\n", tbuf[0]); + _platform.writeGpio(SPI_SS_PIN, LOW); + _platform.readWriteSpi(tbuf, 1); + _platform.writeGpio(SPI_SS_PIN, HIGH); + return tbuf[0]; +} + +void RfPhysicalLayer::spiReadBurst(uint8_t spi_instr, uint8_t *pArr, uint8_t len) +{ + uint8_t rbuf[len + 1]; + rbuf[0] = spi_instr | READ_BURST; + _platform.writeGpio(SPI_SS_PIN, LOW); + _platform.readWriteSpi(rbuf, len + 1); + _platform.writeGpio(SPI_SS_PIN, HIGH); + for (uint8_t i=0; i abort + if(version == 0x00 || version == 0xFF) + { + println("No CC11xx found!"); + stopChip(); + return false; + } + + print("Partnumber: 0x"); + println(partnum, HEX); + print("Version : 0x"); + println(version, HEX); + + // Set modulation mode 2FSK, 32768kbit/s + spiWriteBurst(WRITE_BURST,cc1101_2FSK_32_7_kb,CFG_REGISTER); + + // Set PA table + spiWriteBurst(PATABLE_BURST, paTablePower868, 8); + + // Set ISM band to 868.3MHz + spiWriteRegister(FREQ2,0x21); + spiWriteRegister(FREQ1,0x65); + spiWriteRegister(FREQ0,0x6A); + + // Set channel 0 in ISM band + spiWriteRegister(CHANNR, 0); + + // Set PA to 0dBm as default + setOutputPowerLevel(0); + + return true; +} + +void RfPhysicalLayer::stopChip() +{ + powerDownCC1101(); + + _platform.closeGpio(GPIO_GDO0_PIN); + _platform.closeGpio(GPIO_GDO2_PIN); + _platform.closeGpio(SPI_SS_PIN); + + _platform.closeSpi(); +} + +void RfPhysicalLayer::showRegisterSettings() +{ + uint8_t config_reg_verify[CFG_REGISTER]; + uint8_t Patable_verify[CFG_REGISTER]; + + spiReadBurst(READ_BURST,config_reg_verify,CFG_REGISTER); //reads all 47 config register from cc1101 + spiReadBurst(PATABLE_BURST,Patable_verify,8); //reads output power settings from cc1101 + + println("Config Register:"); + printHex("", config_reg_verify, CFG_REGISTER); + + println("PaTable:"); + printHex("", Patable_verify, 8); +} + +uint16_t RfPhysicalLayer::packetSize (uint8_t lField) +{ + uint16_t nrBytes; + uint8_t nrBlocks; + + // The 2 first blocks contains 25 bytes when excluding CRC and the L-field + // The other blocks contains 16 bytes when excluding the CRC-fields + // Less than 26 (15 + 10) + if ( lField < 26 ) + nrBlocks = 2; + else + nrBlocks = (((lField - 26) / 16) + 3); + + // Add all extra fields, excluding the CRC fields + nrBytes = lField + 1; + + // Add the CRC fields, each block has 2 CRC bytes + nrBytes += (2 * nrBlocks); + + return nrBytes; +} + +void RfPhysicalLayer::loop() +{ + switch (_loopState) + { + case TX_START: + { + prevStatusGDO0 = 0; + prevStatusGDO2 = 0; + // Set sync word in TX mode + // The same sync word is used in RX mode, but we use it in different way here: + // Important: the TX FIFO must provide the last byte of the + // sync word + spiWriteRegister(SYNC1, 0x54); + spiWriteRegister(SYNC0, 0x76); + // Set TX FIFO threshold to 33 bytes + spiWriteRegister(FIFOTHR, 0x47); + // Set GDO2 to be TX FIFO threshold signal + spiWriteRegister(IOCFG2, 0x02); + // Set GDO0 to be packet transmitted signal + spiWriteRegister(IOCFG0, 0x06); + // Flush TX FIFO + spiWriteStrobe(SFTX); + + _rfDataLinkLayer.loadNextTxFrame(&sendBuffer, &sendBufferLength); + + // Calculate total number of bytes in the KNX RF packet from L-field + pktLen = packetSize(sendBuffer[0]); + // Check for valid length + if ((pktLen == 0) || (pktLen > 290)) + { + println("TX packet length error!"); + break; + } + + // Manchester encoded data takes twice the space plus + // 1 byte for postamble and 1 byte (LSB) of the synchronization word + bytesLeft = (2 * pktLen) + 2; + // Last byte of synchronization word + buffer[0] = 0x96; + // Manchester encode packet + for (int i = 0; i < pktLen; i++) + { + manchEncode(&sendBuffer[i], &buffer[1 + i*2]); + } + // Append the postamble sequence + buffer[1 + bytesLeft - 1] = 0x55; + + // Fill TX FIFO + pByteIndex = &buffer[0]; + // Set fixed packet length mode if less than 256 bytes to transmit + if (bytesLeft < 256) + { + spiWriteRegister(PKTLEN, bytesLeft); + spiWriteRegister(PKTCTRL0, 0x00); // Set fixed pktlen mode + fixedLengthMode = true; + } + else // Else set infinite length mode + { + uint8_t fixedLength = bytesLeft % 256; + spiWriteRegister(PKTLEN, fixedLength); + spiWriteRegister(PKTCTRL0, 0x02); + fixedLengthMode = false; + } + + uint8_t bytesToWrite = MIN(64, bytesLeft); + spiWriteBurst(TXFIFO_BURST, pByteIndex, bytesToWrite); + pByteIndex += bytesToWrite; + bytesLeft -= bytesToWrite; + + // Enable transmission of packet + spiWriteStrobe(STX); + + _loopState = TX_ACTIVE; + } + // Fall through + + case TX_ACTIVE: + { + // Check if we have an incomplete packet transmission + if (syncStart && (millis() - packetStartTime > TX_PACKET_TIMEOUT)) + { + println("TX packet timeout!"); + // Set transceiver to IDLE (no RX or TX) + sIdle(); + _loopState = TX_END; + break; + } + + // Detect falling edge 1->0 on GDO2 + statusGDO2 = _platform.readGpio(GPIO_GDO2_PIN); + if(prevStatusGDO2 != statusGDO2) + { + prevStatusGDO2 = statusGDO2; + + // Check if signal GDO2 is de-asserted (TX FIFO is below threshold of 33 bytes, i.e. TX FIFO is half full) + if(statusGDO2 == 0) + { + // - TX FIFO half full detected (< 33 bytes) + // Write data fragment to TX FIFO + uint8_t bytesToWrite = MIN(64, bytesLeft); + spiWriteBurst(TXFIFO_BURST, pByteIndex, bytesToWrite); + pByteIndex += bytesToWrite; + bytesLeft -= bytesToWrite; + + // Set fixed length mode if less than 256 left to transmit + if ( (bytesLeft < (256 - 64)) && !fixedLengthMode ) + { + spiWriteRegister(PKTCTRL0, 0x00); // Set fixed pktlen mode + fixedLengthMode = true; + } + } + } + + // Detect falling edge 1->0 on GDO0 + statusGDO0 = _platform.readGpio(GPIO_GDO0_PIN); + if(prevStatusGDO0 != statusGDO0) + { + prevStatusGDO0 = statusGDO0; + + // If GDO0 is de-asserted: TX packet complete or TX FIFO underflow + if (statusGDO0 == 0x00) + { + // There might be an TX FIFO underflow + uint8_t chipStatusBytes = spiWriteStrobe(SNOP); + if ((chipStatusBytes & CHIPSTATUS_STATE_BITMASK) == CHIPSTATUS_STATE_TX_UNDERFLOW) + { + println("TX FIFO underflow!"); + // Set transceiver to IDLE (no RX or TX) + sIdle(); + } + _loopState = TX_END; + } + else + { + // GDO0 asserted because sync word was transmitted + //println("TX Syncword!"); + // wait for TX_PACKET_TIMEOUT milliseconds + // Complete packet must have been transmitted within this time + packetStartTime = millis(); + syncStart = true; + } + } + } + break; + + case TX_END: + { + // free buffer + delete sendBuffer; + // Go back to RX after TX + _loopState = RX_START; + } + break; + + case RX_START: + { + prevStatusGDO2 = 0; + prevStatusGDO0 = 0; + syncStart = false; + packetStart = true; + fixedLengthMode = false; + pByteIndex = buffer; + bytesLeft = 0; + pktLen = 0; + // Set sync word in RX mode + // The same sync word is used in TX mode, but we use it in different way + spiWriteRegister(SYNC1, 0x76); + spiWriteRegister(SYNC0, 0x96); + // Set GDO2 to be RX FIFO threshold signal + spiWriteRegister(IOCFG2, 0x00); + // Set GDO0 to be packet received signal + spiWriteRegister(IOCFG0, 0x06); + // Set RX FIFO threshold to 4 bytes + spiWriteRegister(FIFOTHR, 0x40); + // Set infinite pktlen mode + spiWriteRegister(PKTCTRL0, 0x02); + // Flush RX FIFO + spiWriteStrobe(SFRX); + // Start RX + sReceive(); + _loopState = RX_ACTIVE; + } + break; + + case RX_ACTIVE: + { + if (!_rfDataLinkLayer.isTxQueueEmpty() && !syncStart) + { + sIdle(); + _loopState = TX_START; + break; + } + + // Check if we have an incomplete packet reception + // This is related to CC1101 errata "Radio stays in RX state instead of entering RXFIFO_OVERFLOW state" + if (syncStart && (millis() - packetStartTime > RX_PACKET_TIMEOUT)) + { + println("RX packet timeout!"); + //uint8_t marcState = (spiReadRegister(MARCSTATE) & MARCSTATE_BITMASK); //read out state of cc1101 to be sure in RX + //print("marcstate: 0x"); + //println(marcState, HEX); + sIdle(); + _loopState = RX_START; + break; + } + + // Detect rising edge 0->1 on GDO2 + statusGDO2 = _platform.readGpio(GPIO_GDO2_PIN); + if(prevStatusGDO2 != statusGDO2) + { + prevStatusGDO2 = statusGDO2; + + // Check if signal GDO2 is asserted (RX FIFO is equal to or above threshold of 4 bytes) + if(statusGDO2 == 1) + { + if (packetStart) + { + // - RX FIFO 4 bytes detected - + // Calculate the total length of the packet, and set fixed mode if less + // than 255 bytes to receive + + // Read the 2 first bytes + spiReadBurst(RXFIFO_BURST, pByteIndex, 2); + + // Decode the L-field + if (!manchDecode(&buffer[0], &packet[0])) + { + //println("Could not decode L-field: manchester code violation"); + _loopState = RX_START; + break; + } + // Get bytes to receive from L-field, multiply by 2 because of manchester code + pktLen = 2 * packetSize(packet[0]); + + // - Length mode - + if (pktLen < 256) + { + // Set fixed packet length mode is less than 256 bytes + spiWriteRegister(PKTLEN, pktLen); + spiWriteRegister(PKTCTRL0, 0x00); // Set fixed pktlen mode + fixedLengthMode = true; + } + else + { + // Infinite packet length mode is more than 255 bytes + // Calculate the PKTLEN value + uint8_t fixedLength = pktLen % 256; + spiWriteRegister(PKTLEN, fixedLength); + } + + pByteIndex += 2; + bytesLeft = pktLen - 2; + + // Set RX FIFO threshold to 32 bytes + packetStart = false; + spiWriteRegister(FIFOTHR, 0x47); + } + else + { + // - RX FIFO Half Full detected - + // Read out the RX FIFO and set fixed mode if less + // than 255 bytes to receive + + // - Length mode - + // Set fixed packet length mode if less than 256 bytes + if ((bytesLeft < 256 ) && !fixedLengthMode) + { + spiWriteRegister(PKTCTRL0, 0x00); // Set fixed pktlen mode + fixedLengthMode = true; + } + + // Read out the RX FIFO + // Do not empty the FIFO (See the CC110x or 2500 Errata Note) + spiReadBurst(RXFIFO_BURST, pByteIndex, 32 - 1); + + bytesLeft -= (32 - 1); + pByteIndex += (32 - 1); + } + + } + } + + // Detect falling edge 1->0 on GDO0 + statusGDO0 = _platform.readGpio(GPIO_GDO0_PIN); + if(prevStatusGDO0 != statusGDO0) + { + prevStatusGDO0 = statusGDO0; + + // If GDO0 is de-asserted: RX packet complete or RX FIFO overflow + if (statusGDO0 == 0x00) + { + // There might be an RX FIFO overflow + uint8_t chipStatusBytes = spiWriteStrobe(SNOP); + if ((chipStatusBytes & CHIPSTATUS_STATE_BITMASK) == CHIPSTATUS_STATE_RX_OVERFLOW) + { + println("RX FIFO overflow!"); + _loopState = RX_START; + break; + } + + // Check if we are in the middle of the packet reception + if (!packetStart) + { + // Complete packet received + // Read out remaining bytes in the RX FIFO + spiReadBurst(RXFIFO_BURST, pByteIndex, bytesLeft); + _loopState = RX_END; + } + } + else + { + // GDO0 asserted because sync word was received and recognized + //println("RX Syncword!"); + // wait for RX_PACKET_TIMEOUT milliseconds + // Complete packet must have been received within this time + packetStartTime = millis(); + syncStart = true; + } + + } + } + break; + + case RX_END: + { + uint16_t pLen = packetSize(packet[0]); + // Decode the first block (always 10 bytes + 2 bytes CRC) + bool decodeOk = true; + for (uint16_t i = 1; i < pLen; i++) + { + // Check for manchester violation, abort if there is one + if(!manchDecode(&buffer[i*2], &packet[i])) + { + println("Could not decode packet: manchester code violation"); + decodeOk = false; + break; + } + } + + if (decodeOk) + { + _rfDataLinkLayer.frameBytesReceived(&packet[0], pLen); + } + + _loopState = RX_START; + } + break; + } +} diff --git a/src/knx/rf_physical_layer.h b/src/knx/rf_physical_layer.h new file mode 100644 index 0000000..931985e --- /dev/null +++ b/src/knx/rf_physical_layer.h @@ -0,0 +1,257 @@ + +#ifndef RF_PHYSICAL_LAYER_H +#define RF_PHYSICAL_LAYER_H + +#include + +#include "platform.h" + +/*----------------------------------[standard]--------------------------------*/ +#define CC1101_TIMEOUT 2000 // Time to wait for a response from CC1101 + +#define RX_PACKET_TIMEOUT 20 // Wait 20ms for packet reception to complete +#define TX_PACKET_TIMEOUT 20 // Wait 20ms for packet reception to complete + +//**************************** pins ******************************************// +#ifdef ARDUINO_ARCH_SAMD +#define SPI_SS_PIN 10 +#define GPIO_GDO2_PIN 9 +#define GPIO_GDO0_PIN 7 +#elif ARDUINO_ARCH_ESP8266 +#error KNX-RF not yet supported on ESP8266 +#elif ARDUINO_ARCH_ESP32 +#error KNX-RF not yet supported on ESP32 +#else // Linux Platform +extern void delayMicroseconds (unsigned int howLong); +#define SPI_SS_PIN 8 // GPIO 8 (SPI_CE0_N) -> WiringPi: 10 -> Pin number on header: 24 +#define GPIO_GDO2_PIN 25 // GPIO 25 (GPIO_GEN6) -> WiringPi: 6 -> Pin number on header: 22 +#define GPIO_GDO0_PIN 24 // GPIO 24 (GPIO_GEN5) -> WiringPi: 5 -> Pin number on header: 18 +#endif + +/*----------------------[CC1101 - misc]---------------------------------------*/ +#define CRYSTAL_FREQUENCY 26000000 +#define CFG_REGISTER 0x2F //47 registers +#define FIFOBUFFER 0x42 //size of Fifo Buffer +2 for rssi and lqi +#define RSSI_OFFSET_868MHZ 0x4E //dec = 74 +#define TX_RETRIES_MAX 0x05 //tx_retries_max +#define ACK_TIMEOUT 250 //ACK timeout in ms +#define CC1101_COMPARE_REGISTER 0x00 //register compare 0=no compare 1=compare +#define BROADCAST_ADDRESS 0x00 //broadcast address +#define CC1101_FREQ_315MHZ 0x01 +#define CC1101_FREQ_434MHZ 0x02 +#define CC1101_FREQ_868MHZ 0x03 +#define CC1101_FREQ_915MHZ 0x04 +#define CC1101_TEMP_ADC_MV 3.225 //3.3V/1023 . mV pro digit +#define CC1101_TEMP_CELS_CO 2.47 //Temperature coefficient 2.47mV per Grad Celsius + +/*---------------------------[CC1101 - R/W offsets]---------------------------*/ +#define WRITE_SINGLE_BYTE 0x00 +#define WRITE_BURST 0x40 +#define READ_SINGLE_BYTE 0x80 +#define READ_BURST 0xC0 +/*---------------------------[END R/W offsets]--------------------------------*/ + +/*------------------------[CC1101 - FIFO commands]----------------------------*/ +#define TXFIFO_BURST 0x7F //write burst only +#define TXFIFO_SINGLE_BYTE 0x3F //write single only +#define RXFIFO_BURST 0xFF //read burst only +#define RXFIFO_SINGLE_BYTE 0xBF //read single only +#define PATABLE_BURST 0x7E //power control read/write +#define PATABLE_SINGLE_BYTE 0xFE //power control read/write +/*---------------------------[END FIFO commands]------------------------------*/ + +/*----------------------[CC1101 - config register]----------------------------*/ +#define IOCFG2 0x00 // GDO2 output pin configuration +#define IOCFG1 0x01 // GDO1 output pin configuration +#define IOCFG0 0x02 // GDO0 output pin configuration +#define FIFOTHR 0x03 // RX FIFO and TX FIFO thresholds +#define SYNC1 0x04 // Sync word, high byte +#define SYNC0 0x05 // Sync word, low byte +#define PKTLEN 0x06 // Packet length +#define PKTCTRL1 0x07 // Packet automation control +#define PKTCTRL0 0x08 // Packet automation control +#define DADDR 0x09 // Device address +#define CHANNR 0x0A // Channel number +#define FSCTRL1 0x0B // Frequency synthesizer control +#define FSCTRL0 0x0C // Frequency synthesizer control +#define FREQ2 0x0D // Frequency control word, high byte +#define FREQ1 0x0E // Frequency control word, middle byte +#define FREQ0 0x0F // Frequency control word, low byte +#define MDMCFG4 0x10 // Modem configuration +#define MDMCFG3 0x11 // Modem configuration +#define MDMCFG2 0x12 // Modem configuration +#define MDMCFG1 0x13 // Modem configuration +#define MDMCFG0 0x14 // Modem configuration +#define DEVIATN 0x15 // Modem deviation setting +#define MCSM2 0x16 // Main Radio Cntrl State Machine config +#define MCSM1 0x17 // Main Radio Cntrl State Machine config +#define MCSM0 0x18 // Main Radio Cntrl State Machine config +#define FOCCFG 0x19 // Frequency Offset Compensation config +#define BSCFG 0x1A // Bit Synchronization configuration +#define AGCCTRL2 0x1B // AGC control +#define AGCCTRL1 0x1C // AGC control +#define AGCCTRL0 0x1D // AGC control +#define WOREVT1 0x1E // High byte Event 0 timeout +#define WOREVT0 0x1F // Low byte Event 0 timeout +#define WORCTRL 0x20 // Wake On Radio control +#define FREND1 0x21 // Front end RX configuration +#define FREND0 0x22 // Front end TX configuration +#define FSCAL3 0x23 // Frequency synthesizer calibration +#define FSCAL2 0x24 // Frequency synthesizer calibration +#define FSCAL1 0x25 // Frequency synthesizer calibration +#define FSCAL0 0x26 // Frequency synthesizer calibration +#define RCCTRL1 0x27 // RC oscillator configuration +#define RCCTRL0 0x28 // RC oscillator configuration +#define FSTEST 0x29 // Frequency synthesizer cal control +#define PTEST 0x2A // Production test +#define AGCTEST 0x2B // AGC test +#define TEST2 0x2C // Various test settings +#define TEST1 0x2D // Various test settings +#define TEST0 0x2E // Various test settings +/*-------------------------[END config register]------------------------------*/ + +/*------------------------[CC1101-command strobes]----------------------------*/ +#define SRES 0x30 // Reset chip +#define SFSTXON 0x31 // Enable/calibrate freq synthesizer +#define SXOFF 0x32 // Turn off crystal oscillator. +#define SCAL 0x33 // Calibrate freq synthesizer & disable +#define SRX 0x34 // Enable RX. +#define STX 0x35 // Enable TX. +#define SIDLE 0x36 // Exit RX / TX +#define SAFC 0x37 // AFC adjustment of freq synthesizer +#define SWOR 0x38 // Start automatic RX polling sequence +#define SPWD 0x39 // Enter pwr down mode when CSn goes hi +#define SFRX 0x3A // Flush the RX FIFO buffer. +#define SFTX 0x3B // Flush the TX FIFO buffer. +#define SWORRST 0x3C // Reset real time clock. +#define SNOP 0x3D // No operation. +/*-------------------------[END command strobes]------------------------------*/ + +/*----------------------[CC1101 - status register]----------------------------*/ +#define PARTNUM 0xF0 // Part number +#define VERSION 0xF1 // Current version number +#define FREQEST 0xF2 // Frequency offset estimate +#define LQI 0xF3 // Demodulator estimate for link quality +#define RSSI 0xF4 // Received signal strength indication +#define MARCSTATE 0xF5 // Control state machine state +#define WORTIME1 0xF6 // High byte of WOR timer +#define WORTIME0 0xF7 // Low byte of WOR timer +#define PKTSTATUS 0xF8 // Current GDOx status and packet status +#define VCO_VC_DAC 0xF9 // Current setting from PLL cal module +#define TXBYTES 0xFA // Underflow and # of bytes in TXFIFO +#define RXBYTES 0xFB // Overflow and # of bytes in RXFIFO +#define RCCTRL1_STATUS 0xFC //Last RC Oscillator Calibration Result +#define RCCTRL0_STATUS 0xFD //Last RC Oscillator Calibration Result +//--------------------------[END status register]------------------------------- + +/*----------------------[CC1101 - Main Radio Control State Machine states]-----*/ +#define MARCSTATE_BITMASK 0x1F +#define MARCSTATE_SLEEP 0x00 +#define MARCSTATE_IDLE 0x01 +#define MARCSTATE_XOFF 0x02 +#define MARCSTATE_VCOON_MC 0x03 +#define MARCSTATE_REGON_MC 0x04 +#define MARCSTATE_MANCAL 0x05 +#define MARCSTATE_VCOON 0x06 +#define MARCSTATE_REGON 0x07 +#define MARCSTATE_STARTCAL 0x08 +#define MARCSTATE_BWBOOST 0x09 +#define MARCSTATE_FS_LOCK 0x0A +#define MARCSTATE_IFADCON 0x0B +#define MARCSTATE_ENDCAL 0x0C +#define MARCSTATE_RX 0x0D +#define MARCSTATE_RX_END 0x0E +#define MARCSTATE_RX_RST 0x0F +#define MARCSTATE_TXRX_SWITCH 0x10 +#define MARCSTATE_RXFIFO_OVERFLOW 0x11 +#define MARCSTATE_FSTXON 0x12 +#define MARCSTATE_TX 0x13 +#define MARCSTATE_TX_END 0x14 +#define MARCSTATE_RXTX_SWITCH 0x15 +#define MARCSTATE_TXFIFO_UNDERFLOW 0x16 + +// Chip Status Byte +// Bit fields in the chip status byte +#define CHIPSTATUS_CHIP_RDYn_BITMASK 0x80 +#define CHIPSTATUS_STATE_BITMASK 0x70 +#define CHIPSTATUS_FIFO_BYTES_AVAILABLE_BITMASK 0x0F +// Chip states + #define CHIPSTATUS_STATE_IDLE 0x00 + #define CHIPSTATUS_STATE_RX 0x10 + #define CHIPSTATUS_STATE_TX 0x20 + #define CHIPSTATUS_STATE_FSTXON 0x30 + #define CHIPSTATUS_STATE_CALIBRATE 0x40 + #define CHIPSTATUS_STATE_SETTLING 0x50 + #define CHIPSTATUS_STATE_RX_OVERFLOW 0x60 + #define CHIPSTATUS_STATE_TX_UNDERFLOW 0x70 + +// loop states +#define RX_START 0 +#define RX_ACTIVE 1 +#define RX_END 2 +#define TX_START 3 +#define TX_ACTIVE 4 +#define TX_END 5 + +class RfDataLinkLayer; + +class RfPhysicalLayer +{ + public: + RfPhysicalLayer(RfDataLinkLayer& rfDataLinkLayer, Platform& platform); + + bool InitChip(); + void showRegisterSettings(); + void stopChip(); + void loop(); + + private: + // Table for encoding 4-bit data into a 8-bit Manchester encoding. + static const uint8_t manchEncodeTab[16]; + // Table for decoding 4-bit Manchester encoded data into 2-bit + static const uint8_t manchDecodeTab[16]; + + static const uint8_t cc1101_2FSK_32_7_kb[CFG_REGISTER]; + static const uint8_t paTablePower868[8]; + + void manchEncode(uint8_t *uncodedData, uint8_t *encodedData); + bool manchDecode(uint8_t *encodedData, uint8_t *decodedData); + + int crc16(uint8_t* buffer, int offset, int length); + void powerDownCC1101(); + void setOutputPowerLevel(int8_t dBm); + + uint16_t packetSize (uint8_t lField); + + uint8_t sIdle(); + uint8_t sReceive(); + + void spiWriteRegister(uint8_t spi_instr, uint8_t value); + uint8_t spiReadRegister(uint8_t spi_instr); + uint8_t spiWriteStrobe(uint8_t spi_instr); + void spiReadBurst(uint8_t spi_instr, uint8_t *pArr, uint8_t len); + void spiWriteBurst(uint8_t spi_instr, const uint8_t *pArr, uint8_t len); + + uint8_t _loopState = RX_START; + + bool syncStart = false; + bool packetStart = true; + bool fixedLengthMode = false; + uint8_t *sendBuffer {0}; + uint16_t sendBufferLength {0}; + uint8_t packet[512]; + uint8_t buffer[sizeof(packet)*2]; // We need twice the space due to manchester encoding + uint8_t* pByteIndex = &buffer[0]; + uint16_t pktLen {0}; + uint16_t bytesLeft = {0}; + uint8_t statusGDO0 {0}; + uint8_t statusGDO2 {0}; + uint8_t prevStatusGDO0 {0}; // for edge detection during polling + uint8_t prevStatusGDO2 {0}; // for edge detection during polling + uint32_t packetStartTime {0}; + + RfDataLinkLayer& _rfDataLinkLayer; + Platform& _platform; +}; + +#endif diff --git a/src/knx_facade.cpp b/src/knx_facade.cpp index 2b6d1cb..45cdbcc 100644 --- a/src/knx_facade.cpp +++ b/src/knx_facade.cpp @@ -3,15 +3,28 @@ #include "knx/bits.h" #ifdef ARDUINO_ARCH_SAMD -KnxFacade knx; -#define ICACHE_RAM_ATTR + // predefined global instance for TP or RF + #ifdef MEDIUM_TYPE + #if MEDIUM_TYPE == 0 + KnxFacade knx; + #elif MEDIUM_TYPE == 2 + KnxFacade knx; + #else + #error "Only TP and RF supported for Arduino SAMD platform!" + #endif + #else + #error "No medium type specified for platform Arduino_SAMD! Please set MEDIUM_TYPE! (TP:0, RF:2, IP:5)" + #endif + #define ICACHE_RAM_ATTR #elif ARDUINO_ARCH_ESP8266 -KnxFacade knx; + // predefined global instance for IP only + KnxFacade knx; #elif ARDUINO_ARCH_ESP32 -//KnxFacade knx; -KnxFacade knx; + // predefined global instance for IP only + KnxFacade knx; #elif __linux__ -#define ICACHE_RAM_ATTR + // no predefined global instance + #define ICACHE_RAM_ATTR #endif #ifndef __linux__ diff --git a/src/knx_facade.h b/src/knx_facade.h index 5328ac9..80c6976 100644 --- a/src/knx_facade.h +++ b/src/knx_facade.h @@ -3,19 +3,21 @@ #include "knx/bits.h" #ifdef ARDUINO_ARCH_SAMD -#include "samd_platform.h" -#include "knx/bau07B0.h" + #include "samd_platform.h" + #include "knx/bau07B0.h" + #include "knx/bau27B0.h" #elif ARDUINO_ARCH_ESP8266 -#include "esp_platform.h" -#include "knx/bau57B0.h" + #include "esp_platform.h" + #include "knx/bau57B0.h" #elif ARDUINO_ARCH_ESP32 -#define LED_BUILTIN 13 -#include "esp32_platform.h" -#include "knx/bau57B0.h" + #define LED_BUILTIN 13 + #include "esp32_platform.h" + #include "knx/bau57B0.h" #else -#include "linux_platform.h" -#include "knx/bau57B0.h" -#define LED_BUILTIN 0 + #define LED_BUILTIN 0 + #include "linux_platform.h" + #include "knx/bau57B0.h" + #include "knx/bau27B0.h" #endif void buttonUp(); @@ -297,11 +299,24 @@ template class KnxFacade : private SaveRestore }; #ifdef ARDUINO_ARCH_SAMD -extern KnxFacade knx; + // predefined global instance for TP or RF + #ifdef MEDIUM_TYPE + #if MEDIUM_TYPE == 0 + extern KnxFacade knx; + #elif MEDIUM_TYPE == 2 + extern KnxFacade knx; + #else + #error "Only TP and RF supported for Arduino SAMD platform!" + #endif + #else + #error "No medium type specified for Arduino_SAMD platform! Please set MEDIUM_TYPE! (TP:0, RF:2, IP:5)" + #endif #elif ARDUINO_ARCH_ESP8266 -extern KnxFacade knx; + // predefined global instance for IP only + extern KnxFacade knx; #elif ARDUINO_ARCH_ESP32 -extern KnxFacade knx; + // predefined global instance for IP only + extern KnxFacade knx; #elif __linux__ -// no predefined global instance -#endif \ No newline at end of file + // no predefined global instance +#endif diff --git a/src/linux_platform.cpp b/src/linux_platform.cpp index 8445381..8b67a39 100644 --- a/src/linux_platform.cpp +++ b/src/linux_platform.cpp @@ -19,6 +19,11 @@ #include #include +#include // Needed for SPI port +#include // Needed for SPI port +#include // Needed for GPIO edge detection +#include // Needed for delayMicroseconds() + #include "knx/device_object.h" #include "knx/address_table_object.h" #include "knx/association_table_object.h" @@ -299,6 +304,74 @@ void LinuxPlatform::setupUart() { } +void LinuxPlatform::closeSpi() +{ + close(_spiFd); + printf ("SPI device closed.\r\n"); +} + +int LinuxPlatform::readWriteSpi (uint8_t *data, size_t len) +{ + uint16_t spiDelay = 0 ; + uint32_t spiSpeed = 8000000; // 4 MHz SPI speed + uint8_t spiBPW = 8; // Bits per word + + struct spi_ioc_transfer spi ; + + // Mentioned in spidev.h but not used in the original kernel documentation + // test program )-: + + memset (&spi, 0, sizeof (spi)) ; + + spi.tx_buf = (uint64_t)data; + spi.rx_buf = (uint64_t)data; + spi.len = len; + spi.delay_usecs = spiDelay; + spi.speed_hz = spiSpeed; + spi.bits_per_word = spiBPW; + + return ioctl (_spiFd, SPI_IOC_MESSAGE(1), &spi) ; +} + +void LinuxPlatform::setupSpi() +{ + if ((_spiFd = open ("/dev/spidev0.0", O_RDWR)) < 0) + { + printf ("ERROR: SPI setup failed! Could not open SPI device!\r\n"); + return; + } + + // Set SPI parameters. + int mode = 0; // Mode 0 + uint8_t spiBPW = 8; // Bits per word + int speed = 8000000; // 4 MHz SPI speed + + if (ioctl (_spiFd, SPI_IOC_WR_MODE, &mode) < 0) + { + printf ("ERROR: SPI Mode Change failure: %s\n", strerror (errno)) ; + close(_spiFd); + return; + } + + if (ioctl (_spiFd, SPI_IOC_WR_BITS_PER_WORD, &spiBPW) < 0) + { + printf ("ERROR: SPI BPW Change failure: %s\n", strerror (errno)) ; + close(_spiFd); + return; + } + + if (ioctl (_spiFd, SPI_IOC_WR_MAX_SPEED_HZ, &speed) < 0) + { + printf ("ERROR: SPI Speed Change failure: %s\n", strerror (errno)) ; + close(_spiFd); + return; + } + + printf ("SPI device setup ok.\r\n"); + + +} + /* * On linux the memory addresses from malloc may be to big for usermermory_write. * So we allocate some memory at the beginning and use it for address table, group object table etc. @@ -519,4 +592,326 @@ void LinuxPlatform::cmdLineArgs(int argc, char** argv) memcpy(_args, argv, argc * sizeof(char*)); _args[argc] = 0; } -#endif \ No newline at end of file + +void LinuxPlatform::setupGpio(uint32_t dwPin, uint32_t dwMode) +{ + gpio_export(dwPin); + gpio_direction(dwPin, dwMode); +} + +void LinuxPlatform::closeGpio(uint32_t dwPin) +{ + gpio_unexport(dwPin); + // Set direction to input always if we do not need the GPIO anymore? Unsure... + //gpio_direction(dwPin, INPUT); +} + +void LinuxPlatform::writeGpio(uint32_t dwPin, uint32_t dwVal) +{ + gpio_write(dwPin, dwVal); +} + +uint32_t LinuxPlatform::readGpio(uint32_t dwPin) +{ + return gpio_read(dwPin); +} + +/* Datenpuffer fuer die GPIO-Funktionen */ +#define MAXBUFFER 100 + +/* GPIO-Pin aktivieren + * Schreiben der Pinnummer nach /sys/class/gpio/export + * Ergebnis: 0 = O.K., -1 = Fehler + */ +int gpio_export(int pin) +{ + char buffer[MAXBUFFER]; /* Output Buffer */ + ssize_t bytes; /* Datensatzlaenge */ + int fd; /* Filedescriptor */ + int res; /* Ergebnis von write */ + + fd = open("/sys/class/gpio/export", O_WRONLY); + if (fd < 0) + { + perror("Kann nicht auf export schreiben!\n"); + return(-1); + } + + bytes = snprintf(buffer, MAXBUFFER, "%d", pin); + res = write(fd, buffer, bytes); + + if (res < 0) + { + perror("Kann Pin nicht aktivieren (write)!\n"); + return(-1); + } + + close(fd); + delay(100); + + return(0); +} + +/* GPIO-Pin deaktivieren + * Schreiben der Pinnummer nach /sys/class/gpio/unexport + * Ergebnis: 0 = O.K., -1 = Fehler + */ +int gpio_unexport(int pin) +{ + char buffer[MAXBUFFER]; /* Output Buffer */ + ssize_t bytes; /* Datensatzlaenge */ + int fd; /* Filedescriptor */ + int res; /* Ergebnis von write */ + + fd = open("/sys/class/gpio/unexport", O_WRONLY); + if (fd < 0) + { + perror("Kann nicht auf unexport schreiben!\n"); + return(-1); + } + + bytes = snprintf(buffer, MAXBUFFER, "%d", pin); + res = write(fd, buffer, bytes); + + if (res < 0) + { + perror("Kann Pin nicht deaktivieren (write)!\n"); + return(-1); + } + + close(fd); + return(0); +} + +/* Datenrichtung GPIO-Pin festlegen + * Schreiben Pinnummer nach /sys/class/gpioXX/direction + * Richtung dir: 0 = Lesen, 1 = Schreiben + * Ergebnis: 0 = O.K., -1 = Fehler + */ +int gpio_direction(int pin, int dir) +{ + char path[MAXBUFFER]; /* Buffer fuer Pfad */ + int fd; /* Filedescriptor */ + int res; /* Ergebnis von write */ + + snprintf(path, MAXBUFFER, "/sys/class/gpio/gpio%d/direction", pin); + fd = open(path, O_WRONLY); + if (fd < 0) + { + perror("Kann Datenrichtung nicht setzen (open)!\n"); + return(-1); + } + + switch (dir) + { + case INPUT : res = write(fd,"in",2); break; + case OUTPUT: res = write(fd,"out",3); break; + default: res = -1; break; + } + + if (res < 0) + { + perror("Kann Datenrichtung nicht setzen (write)!\n"); + return(-1); + } + + close(fd); + return(0); +} + +/* vom GPIO-Pin lesen + * Ergebnis: -1 = Fehler, 0/1 = Portstatus + */ +int gpio_read(int pin) +{ + char path[MAXBUFFER]; /* Buffer fuer Pfad */ + int fd; /* Filedescriptor */ + char result[MAXBUFFER] = {0}; /* Buffer fuer Ergebnis */ + + snprintf(path, MAXBUFFER, "/sys/class/gpio/gpio%d/value", pin); + fd = open(path, O_RDONLY); + if (fd < 0) + { + perror("Kann vom GPIO nicht lesen (open)!\n"); + return(-1); + } + + if (read(fd, result, 3) < 0) + { + perror("Kann vom GPIO nicht lesen (read)!\n"); + return(-1); + } + + close(fd); + return(atoi(result)); +} + +/* auf GPIO schreiben + * Ergebnis: -1 = Fehler, 0 = O.K. + */ +int gpio_write(int pin, int value) +{ + char path[MAXBUFFER]; /* Buffer fuer Pfad */ + int fd; /* Filedescriptor */ + int res; /* Ergebnis von write */ + + snprintf(path, MAXBUFFER, "/sys/class/gpio/gpio%d/value", pin); + fd = open(path, O_WRONLY); + + if (fd < 0) + { + perror("Kann auf GPIO nicht schreiben (open)!\n"); + return(-1); + } + + switch (value) + { + case LOW : res = write(fd,"0",1); break; + case HIGH: res = write(fd,"1",1); break; + default: res = -1; break; + } + + if (res < 0) + { + perror("Kann auf GPIO nicht schreiben (write)!\n"); + return(-1); + } + + close(fd); + return(0); +} + +/* GPIO-Pin auf Detektion einer Flanke setzen. + * Fuer die Flanke (edge) koennen folgende Parameter gesetzt werden: + * 'r' (rising) - steigende Flanke, + * 'f' (falling) - fallende Flanke, + * 'b' (both) - beide Flanken. + */ +int gpio_edge(unsigned int pin, char edge) +{ + char path[MAXBUFFER]; /* Buffer fuer Pfad */ + int fd; /* Filedescriptor */ + + snprintf(path, MAXBUFFER, "/sys/class/gpio/gpio%d/edge", pin); + + fd = open(path, O_WRONLY | O_NONBLOCK ); + if (fd < 0) + { + perror("gpio_edge: Kann auf GPIO nicht schreiben (open)!\n"); + return(-1); + } + + switch (edge) + { + case 'r': strncpy(path,"rising",8); break; + case 'f': strncpy(path,"falling",8); break; + case 'b': strncpy(path,"both",8); break; + case 'n': strncpy(path,"none",8); break; + default: close(fd);return(-2); + } + + write(fd, path, strlen(path) + 1); + + close(fd); + return 0; +} + +/* Warten auf Flanke am GPIO-Pin. + * Eingabewerte: pin: GPIO-Pin + * timeout: Wartezeit in Millisekunden + * Der Pin muss voher eingerichtet werden (export, + * direction, edge) + * Rueckgabewerte: <0: Fehler, 0: poll() Timeout, + * 1: Flanke erkannt, Pin lieferte "0" + * 2: Flanke erkannt, Pin lieferte "1" + */ +int gpio_wait(unsigned int pin, int timeout) +{ + char path[MAXBUFFER]; /* Buffer fuer Pfad */ + int fd; /* Filedescriptor */ + struct pollfd polldat[1]; /* Variable fuer poll() */ + char buf[MAXBUFFER]; /* Lesepuffer */ + int rc; /* Hilfsvariablen */ + + /* GPIO-Pin dauerhaft oeffnen */ + snprintf(path, MAXBUFFER, "/sys/class/gpio/gpio%d/value", pin); + fd = open(path, O_RDONLY | O_NONBLOCK ); + if (fd < 0) + { + perror("gpio_wait: Kann von GPIO nicht lesen (open)!\n"); + return(-1); + } + + /* poll() vorbereiten */ + memset((void*)buf, 0, sizeof(buf)); + memset((void*)polldat, 0, sizeof(polldat)); + polldat[0].fd = fd; + polldat[0].events = POLLPRI; + + /* eventuell anstehende Interrupts loeschen */ + lseek(fd, 0, SEEK_SET); + rc = read(fd, buf, MAXBUFFER - 1); + + rc = poll(polldat, 1, timeout); + if (rc < 0) + { /* poll() failed! */ + perror("gpio_wait: Poll-Aufruf ging schief!\n"); + close(fd); + return(-1); + } + + if (rc == 0) + { /* poll() timeout! */ + close(fd); + return(0); + } + + if (polldat[0].revents & POLLPRI) + { + if (rc < 0) + { /* read() failed! */ + perror("gpio_wait: Kann von GPIO nicht lesen (read)!\n"); + close(fd); + return(-2); + } + /* printf("poll() GPIO %d interrupt occurred: %s\n", pin, buf); */ + close(fd); + return(1 + atoi(buf)); + } + + close(fd); + return(-1); +} + +void delayMicrosecondsHard (unsigned int howLong) +{ + struct timeval tNow, tLong, tEnd ; + + gettimeofday (&tNow, NULL) ; + tLong.tv_sec = howLong / 1000000 ; + tLong.tv_usec = howLong % 1000000 ; + timeradd (&tNow, &tLong, &tEnd) ; + + while (timercmp (&tNow, &tEnd, <)) + gettimeofday (&tNow, NULL) ; +} + +void delayMicroseconds (unsigned int howLong) +{ + struct timespec sleeper ; + unsigned int uSecs = howLong % 1000000 ; + unsigned int wSecs = howLong / 1000000 ; + + /**/ if (howLong == 0) + return ; + else if (howLong < 100) + delayMicrosecondsHard (howLong) ; + else + { + sleeper.tv_sec = wSecs ; + sleeper.tv_nsec = (long)(uSecs * 1000L) ; + nanosleep (&sleeper, NULL) ; + } +} + +#endif diff --git a/src/linux_platform.h b/src/linux_platform.h index 722c537..cda3af9 100644 --- a/src/linux_platform.h +++ b/src/linux_platform.h @@ -5,6 +5,13 @@ #include #include "knx/platform.h" +extern void delayMicroseconds (unsigned int howLong); +extern int gpio_direction(int pin, int dir); +extern int gpio_read(int pin); +extern int gpio_write(int pin, int value); +extern int gpio_export(int pin); +extern int gpio_unexport(int pin); + class LinuxPlatform: public Platform { using Platform::_memoryReference; @@ -43,6 +50,17 @@ public: int readUart() override; size_t readBytesUart(uint8_t *buffer, size_t length) override; + //spi + void setupSpi() override; + void closeSpi() override; + int readWriteSpi (uint8_t *data, size_t len) override; + + //gpio + virtual void setupGpio(uint32_t dwPin, uint32_t dwMode) override; + virtual void closeGpio(uint32_t dwPin) override; + virtual void writeGpio(uint32_t dwPin, uint32_t dwVal) override; + virtual uint32_t readGpio(uint32_t dwPin) override; + //memory uint8_t* getEepromBuffer(uint16_t size) override; void commitToEeprom() override; @@ -57,6 +75,7 @@ public: void doMemoryMapping(); uint8_t* _mappedFile = 0; int _fd = -1; + int _spiFd = -1; uint8_t* _currentMaxMem = 0; std::string _flashFilePath = "flash.bin"; char** _args = 0; From d79e022b765c8c5769a0256a22ab034e024b92d9 Mon Sep 17 00:00:00 2001 From: nanosonde <2073569+nanosonde@users.noreply.github.com> Date: Fri, 25 Oct 2019 16:47:44 +0200 Subject: [PATCH 05/21] Add fixes after review with upstream --- src/knx/bau.h | 2 +- src/knx/knx_types.h | 10 ---------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/knx/bau.h b/src/knx/bau.h index e13af25..d2d6ada 100644 --- a/src/knx/bau.h +++ b/src/knx/bau.h @@ -109,7 +109,7 @@ class BusAccessUnit virtual void keyWriteResponseConfirm(AckType ack, Priority priority, HopCountType hopType, uint16_t asap, uint8_t level, bool status); virtual void keyWriteAppLayerConfirm(Priority priority, HopCountType hopType, uint16_t asap, uint8_t level); - virtual bool connectConfirm(uint16_t destination); + virtual void connectConfirm(uint16_t destination); virtual void systemNetworkParameterReadIndication(Priority priority, HopCountType hopType, uint16_t objectType, uint16_t propertyId, uint8_t* testInfo, uint16_t testInfoLength); diff --git a/src/knx/knx_types.h b/src/knx/knx_types.h index 25afe8a..0421526 100644 --- a/src/knx/knx_types.h +++ b/src/knx/knx_types.h @@ -116,13 +116,3 @@ enum ApduType PropertyDescriptionRead = 0x3d8, PropertyDescriptionResponse = 0x3d9, }; - -enum KnxMediumType -{ - KnxMediumType_TP1 = 0, - KnxMediumType_PL = 1, - KnxMediumType_RF = 2, - KnxMediumType_TP0 = 3, // not supported anymore - KnxMediumType_PL132 = 4, // not supported anymore - KnxMediumType_IP = 5, -}; \ No newline at end of file From b4923815fe9b046bc40e8c45cae1c88c67dbebb8 Mon Sep 17 00:00:00 2001 From: nanosonde <2073569+nanosonde@users.noreply.github.com> Date: Fri, 25 Oct 2019 17:01:04 +0200 Subject: [PATCH 06/21] Fix blanks --- knx-linux/CMakeLists.txt | 82 ++++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/knx-linux/CMakeLists.txt b/knx-linux/CMakeLists.txt index d29ed91..76d1b3b 100644 --- a/knx-linux/CMakeLists.txt +++ b/knx-linux/CMakeLists.txt @@ -9,8 +9,8 @@ add_executable(knx-linux ../src/knx/association_table_object.cpp ../src/knx/bau.cpp ../src/knx/bau07B0.cpp - ../src/knx/bau27B0.cpp - ../src/knx/bau57B0.cpp + ../src/knx/bau27B0.cpp + ../src/knx/bau57B0.cpp ../src/knx/bau_systemB.cpp ../src/knx/bits.cpp ../src/knx/cemi_frame.cpp @@ -24,50 +24,50 @@ add_executable(knx-linux ../src/knx/memory.cpp ../src/knx/network_layer.cpp ../src/knx/npdu.cpp - ../src/knx/rf_physical_layer.cpp - ../src/knx/rf_data_link_layer.cpp - ../src/knx/rf_medium_object.cpp - ../src/knx/table_object.cpp + ../src/knx/rf_physical_layer.cpp + ../src/knx/rf_data_link_layer.cpp + ../src/knx/rf_medium_object.cpp + ../src/knx/table_object.cpp ../src/knx/tpdu.cpp ../src/knx/tpuart_data_link_layer.cpp ../src/knx/transport_layer.cpp ../src/knx/platform.cpp - ../src/knx/address_table_object.h - ../src/knx/apdu.h - ../src/knx/application_layer.h - ../src/knx/application_program_object.h - ../src/knx/association_table_object.h - ../src/knx/bau.h - ../src/knx/bau07B0.h - ../src/knx/bau27B0.h - ../src/knx/bau57B0.h - ../src/knx/bau_systemB.h - ../src/knx/bits.h - ../src/knx/cemi_frame.h - ../src/knx/data_link_layer.h - ../src/knx/device_object.h - ../src/knx/group_object.h - ../src/knx/group_object_table_object.h - ../src/knx/interface_object.h - ../src/knx/ip_data_link_layer.h - ../src/knx/ip_parameter_object.h - ../src/knx/memory.h - ../src/knx/network_layer.h - ../src/knx/npdu.h - ../src/knx/rf_physical_layer.h - ../src/knx/rf_data_link_layer.h - ../src/knx/rf_medium_object.h - ../src/knx/table_object.h - ../src/knx/tpdu.h - ../src/knx/tpuart_data_link_layer.h - ../src/knx/transport_layer.h - ../src/knx/platform.h + ../src/knx/address_table_object.h + ../src/knx/apdu.h + ../src/knx/application_layer.h + ../src/knx/application_program_object.h + ../src/knx/association_table_object.h + ../src/knx/bau.h + ../src/knx/bau07B0.h + ../src/knx/bau27B0.h + ../src/knx/bau57B0.h + ../src/knx/bau_systemB.h + ../src/knx/bits.h + ../src/knx/cemi_frame.h + ../src/knx/data_link_layer.h + ../src/knx/device_object.h + ../src/knx/group_object.h + ../src/knx/group_object_table_object.h + ../src/knx/interface_object.h + ../src/knx/ip_data_link_layer.h + ../src/knx/ip_parameter_object.h + ../src/knx/memory.h + ../src/knx/network_layer.h + ../src/knx/npdu.h + ../src/knx/rf_physical_layer.h + ../src/knx/rf_data_link_layer.h + ../src/knx/rf_medium_object.h + ../src/knx/table_object.h + ../src/knx/tpdu.h + ../src/knx/tpuart_data_link_layer.h + ../src/knx/transport_layer.h + ../src/knx/platform.h main.cpp - ../src/linux_platform.h - ../src/knx_facade.h - ../src/knx/dptconvert.h - ../src/knx/knx_value.h - ../src/knx/dpt.h + ../src/linux_platform.h + ../src/knx_facade.h + ../src/knx/dptconvert.h + ../src/knx/knx_value.h + ../src/knx/dpt.h ../src/linux_platform.cpp ../src/knx_facade.cpp ../src/knx/dptconvert.cpp From 0c8a79edbd28ca10b3d9f1a939914a7b2e7fb9f8 Mon Sep 17 00:00:00 2001 From: nanosonde <2073569+nanosonde@users.noreply.github.com> Date: Fri, 25 Oct 2019 17:13:59 +0200 Subject: [PATCH 07/21] Add KNX RF example --- examples/knx-rf-demo/knx-rf-demo.ino | 70 +++++++++++++++++++++ examples/knx-rf-demo/knx-rf-demo.knxprod | Bin 0 -> 35610 bytes examples/knx-rf-demo/knx-rf-demo.xml | 77 +++++++++++++++++++++++ 3 files changed, 147 insertions(+) create mode 100644 examples/knx-rf-demo/knx-rf-demo.ino create mode 100644 examples/knx-rf-demo/knx-rf-demo.knxprod create mode 100644 examples/knx-rf-demo/knx-rf-demo.xml diff --git a/examples/knx-rf-demo/knx-rf-demo.ino b/examples/knx-rf-demo/knx-rf-demo.ino new file mode 100644 index 0000000..b4aefed --- /dev/null +++ b/examples/knx-rf-demo/knx-rf-demo.ino @@ -0,0 +1,70 @@ +#include + +// 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; + +// Entry point for the example +void setup(void) +{ + Serial1.begin(115200); + ArduinoPlatform::SerialDebug = &Serial1; + delay(1000); + Serial1.println("start"); + + // 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(); +} + +// Function that is looped forever +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/examples/knx-rf-demo/knx-rf-demo.knxprod b/examples/knx-rf-demo/knx-rf-demo.knxprod new file mode 100644 index 0000000000000000000000000000000000000000..72c5f17435e073f4c2f2543dc6d28eb11b83a966 GIT binary patch literal 35610 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 z zU6O~_!!>}5xC4t-u|ovamJBOf_sTc-=_M(VBWwu7mJrLCw=iTUEYOu3bKkSvEnvp% z+1vXOpH{JhfT!U|^3!4!^iYQiJZ5PrHqOccnJvgunnm@dDT?+b?$kYG4HV+Ya$C9Q zRVrhua<_g6Fv18y4Tiz-@#*cTkV?T*wOc^jxlcVyyCHuk3JvboK9SO~z@9DBQryfa z(-Y=2x_1@vR;lj@pqD;DFAEgRP?*^2jco5W8q?{drWWlrml|nCz}6v8-R1_-z16O$ znB*t=RHkPsLAQ28MP7OZ+_8h{_U1+Va4p5_pMDjV@+;brmY|YsYF~fmt))yMOM1&4 z$W9u_j8q557`cB@Rk|;p?Z)8j(c^dPyG@mR#WG71kOsyRlUo8v#3jz0sCiw73h)+R zjy;cNPsKiDu1a4@nG{oUiYq0)S_@ru`g87HRw7p3C{%cpk{(KI_fAgE_%PtAy;B1| zN4fr@6z<%bUtjFFFSNWGXmLg9CeBVAOr4&KlRvF8J2$lHe!hMRjebHXFPWGbK!pIIlvaHJ3w2Of1_Hbf-eqTSUv1Zi+zcb(F7b`eJ*AVrBGAei3e&RJzLvAh$?E=fZJyha{z z=Ro%)nWQV}MIrV;WxsKyL_uh0Y3k&sGr}>02?_|2x_#b3XmRvMYIBhU>P?EuJ1qv| z0E`;EA?dU5Uy+MWlBtT8e=LZlVoec}9g-}Ibbn zxP*H-MIjzRWy6_+u*UoI5LDgy5gF<+;}TUlCuE9eA&$Qw#$WEgZNF+l!v~s8hQu_a zCmiaf|M(?O+CqW{?Eo^iv>i+};4YT69WOK`1Iwv68D0-a0Q=)ip8(YOYJ4lH()o3| zHQryf_~vB4`Dgs-E>Ew~!c{fLCh^hOc=P#w#6vqf%cV=Vv~sDG$D94Hd)(O_w)^L; zIWtX|tF8}^&+BKUOJ(Qx^(BRJT1nFhnSz-v`ouxQk7hY*ZQc8%rPclWxrYhohhlQ> zEwQ`7vDw)m-KevhvzIfky+(Nt6w_YP+BXLZpI7Er7EkYQuWcG#-1MVjEsjC9QW;2U zfZDX`NNi)P&3b^Y_WMVU34$I%x{ag?DJV7%*Re?2*elfeP^Lc&K`$b=C$BseyYOwX zOel@elt*(ljo$`tA=t+}6n$`vS(~mU3;z~Q3ENvKG9fJJ0+W=XK2&Yc@!B;%X%uN0 zvOm-e;|}Xb1Pb;i2SF^E=c8wX%0z`g0SAG|HG!@*$WQ46&$0yHzdZYvs~5U15bLAU zSH{Q!9z`hhR#}LYiQFL5rwGNWDeJjw_AWT3(R)~de|khSCyXbi6@|y)1lG6v;wJS# z{7_&a# zIePv>A@+59YQ%A=DzQ%M*=$0LBip)`GA+9CLs*oAP1Zgug6yS_H zH@{>)nS>3hyBE;0TGPZ02IAjb>SozhV0MPGS;v3P!s8*;+M=ZJJOu!Dc(Bir;5bgwgRaaSFr%@>j-CI8 zq6K;R=d>V2Ga+%Uf}{ua<#?8WN!71CY{>%>Z56 zFA$|Amd+VYPa}ji|Ct#bgNWmtVQ6$wvS8?+Qu>0_r=BiO%7%d#$e~uUHqi+AIWUyV z^0tSknxfqWtYQM*2tn=NeiPTky3zB{>?GHX*0c6(_I=)ZQX(5g>sfjx`!*Vy zCQOx2O`xbHq4f+rcdv^3aGB|tBzl-5X~}FLfGN4doy{kIQ7^s!m5HmcLeh#sIJhVo zkZy$BEWW(@-Ijv9Y)eTG;onvI#*DFngwDn&*&Vp;c)EFND}JiLpwL~W2^zCkkTvCD z9#D_gGsYyxS6spEpCd(86D8}Ky++8E6>JKWekzbtZQSBsp9yFLO)kH2k2Pp*f{Gp< zQqdiq@=d{(F47aS6X2=vASc&YWddYuy8?tBq6*b#356@>NZfGoq&7g>VxeoMN!T)j zZNZHclOtCGGMF$h`RFs}f{Opa{h{heHc&X*Sb7Lv{#;hq<$tIc2?|p(C{(qw^oYN( zUF~dr-R6#_M_qlqKJj8pvS(-nv-H@%0A0-pzwKKAoVj|f0@up~H))4d@pJms((|wA| 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 3625f31d95a1cb204dadba35f67113225e29cf03 Mon Sep 17 00:00:00 2001 From: nanosonde <2073569+nanosonde@users.noreply.github.com> Date: Sun, 27 Oct 2019 10:03:48 +0100 Subject: [PATCH 08/21] Add README.md to KNX-RF demo folder --- examples/knx-rf-demo/README.md | 79 ++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 examples/knx-rf-demo/README.md diff --git a/examples/knx-rf-demo/README.md b/examples/knx-rf-demo/README.md new file mode 100644 index 0000000..f20cbd0 --- /dev/null +++ b/examples/knx-rf-demo/README.md @@ -0,0 +1,79 @@ +KNX-RF S-Mode +============= + +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 + 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! + 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) +* 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 +* 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. + 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 + 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 + 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. + +Connection wiring: +------------------ + +Signal SAMD21 CC1101 +----------+-------------+--------------- +SPI_nCS | D10(PA18) | CSN +SPI_MOSI | D11(PA16) | SI +SPI_MISO | D12(PA19) | SO +SPI_SCK | D13(PA17) | SCLK +GDO0 | D7(PA21) | GDO0 +GDO2 | D9(PA07) | GDO2 + +Arduino MZEROUSB variant needs patching to enable SPI on SERCOM1 on D10-D13. + +variant.h +--------- +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 1 + +#define PIN_SPI_MISO (18u) +#define PIN_SPI_MOSI (21u) +#define PIN_SPI_SCK (20u) +#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] + { NOT_A_PORT, 0, PIO_NOT_A_PIN, PIN_ATTR_NONE, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_NONE }, // 5V0 + { PORTA, 17, PIO_SERCOM, PIN_ATTR_DIGITAL, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_11 }, // SCK: SERCOM1/PAD[1] + { 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 From 8a3d5edd8fd8f941a0a7a2d53c9f8916fcf795c6 Mon Sep 17 00:00:00 2001 From: nanosonde <2073569+nanosonde@users.noreply.github.com> Date: Sun, 27 Oct 2019 10:54:42 +0100 Subject: [PATCH 09/21] Make max APDU length and interface object list configurable in device object --- src/knx/bau07B0.cpp | 5 +++++ src/knx/bau07B0.h | 2 ++ src/knx/bau27B0.cpp | 12 ++++++++++++ src/knx/bau27B0.h | 2 ++ src/knx/bau57B0.cpp | 5 +++++ src/knx/bau57B0.h | 2 ++ src/knx/device_object.cpp | 31 +++++++++++++++++++++++-------- src/knx/device_object.h | 6 ++++++ 8 files changed, 57 insertions(+), 8 deletions(-) diff --git a/src/knx/bau07B0.cpp b/src/knx/bau07B0.cpp index 87db8ce..6d5b464 100644 --- a/src/knx/bau07B0.cpp +++ b/src/knx/bau07B0.cpp @@ -15,6 +15,11 @@ Bau07B0::Bau07B0(Platform& platform) uint16_t maskVersion; popWord(maskVersion, _descriptor); _deviceObj.maskVersion(maskVersion); + + // Set which interface objects are available in the device object + // This differs from BAU to BAU with different medium types. + // See PID_IO_LIST + _deviceObj.ifObj(_ifObjs); } InterfaceObject* Bau07B0::getInterfaceObject(uint8_t idx) diff --git a/src/knx/bau07B0.h b/src/knx/bau07B0.h index 84d8aba..5fe3f67 100644 --- a/src/knx/bau07B0.h +++ b/src/knx/bau07B0.h @@ -16,4 +16,6 @@ class Bau07B0 : public BauSystemB private: TpUartDataLinkLayer _dlLayer; uint8_t _descriptor[2] = {0x07, 0xb0}; + const uint32_t _ifObjs[6] = { 5, // length + OT_DEVICE, OT_ADDR_TABLE, OT_ASSOC_TABLE, OT_GRP_OBJ_TABLE, OT_APPLICATION_PROG}; }; \ No newline at end of file diff --git a/src/knx/bau27B0.cpp b/src/knx/bau27B0.cpp index 2b5365d..f704eb5 100644 --- a/src/knx/bau27B0.cpp +++ b/src/knx/bau27B0.cpp @@ -16,6 +16,18 @@ Bau27B0::Bau27B0(Platform& platform) uint16_t maskVersion; popWord(maskVersion, _descriptor); _deviceObj.maskVersion(maskVersion); + + // Set the maximum APDU length + // ETS will consider this value while programming the device + // For KNX-RF we use a smallest allowed value for now, + // although long frame are also supported by the implementation. + // Needs some experimentation. + _deviceObj.maxApduLength(15); + + // Set which interface objects are available in the device object + // This differs from BAU to BAU with different medium types. + // See PID_IO_LIST + _deviceObj.ifObj(_ifObjs); } // see KNX AN160 p.74 for mask 27B0 diff --git a/src/knx/bau27B0.h b/src/knx/bau27B0.h index f1b30e6..b2c129e 100644 --- a/src/knx/bau27B0.h +++ b/src/knx/bau27B0.h @@ -20,6 +20,8 @@ class Bau27B0 : public BauSystemB RfMediumObject _rfMediumObj; uint8_t _descriptor[2] = {0x27, 0xB0}; + 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}; 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 74373c9..648d777 100644 --- a/src/knx/bau57B0.cpp +++ b/src/knx/bau57B0.cpp @@ -17,6 +17,11 @@ Bau57B0::Bau57B0(Platform& platform) uint16_t maskVersion; popWord(maskVersion, _descriptor); _deviceObj.maskVersion(maskVersion); + + // Set which interface objects are available in the device object + // This differs from BAU to BAU with different medium types. + // See PID_IO_LIST + _deviceObj.ifObj(_ifObjs); } InterfaceObject* Bau57B0::getInterfaceObject(uint8_t idx) diff --git a/src/knx/bau57B0.h b/src/knx/bau57B0.h index 297f4aa..8835150 100644 --- a/src/knx/bau57B0.h +++ b/src/knx/bau57B0.h @@ -18,4 +18,6 @@ class Bau57B0 : public BauSystemB IpParameterObject _ipParameters; IpDataLinkLayer _dlLayer; uint8_t _descriptor[2] = {0x57, 0xb0}; + const uint32_t _ifObjs[7] = { 6, // length + OT_DEVICE, OT_ADDR_TABLE, OT_ASSOC_TABLE, OT_GRP_OBJ_TABLE, OT_APPLICATION_PROG, OT_IP_PARAMETER}; }; \ No newline at end of file diff --git a/src/knx/device_object.cpp b/src/knx/device_object.cpp index f7a5f85..e9d0905 100644 --- a/src/knx/device_object.cpp +++ b/src/knx/device_object.cpp @@ -35,7 +35,7 @@ void DeviceObject::readProperty(PropertyID propertyId, uint32_t start, uint32_t& *data = _prgMode; break; case PID_MAX_APDU_LENGTH: - pushWord(254, data); + pushWord(_maxApduLength, data); break; case PID_SUBNET_ADDR: *data = ((_ownAddress >> 8) & 0xff); @@ -45,13 +45,8 @@ void DeviceObject::readProperty(PropertyID propertyId, uint32_t start, uint32_t& break; case PID_IO_LIST: { - uint32_t ifObjs[] = { - 6, // length - OT_DEVICE, OT_ADDR_TABLE, OT_ASSOC_TABLE, OT_GRP_OBJ_TABLE, OT_APPLICATION_PROG, OT_IP_PARAMETER}; - - for (uint32_t i = start; i < (ifObjs[0] + 1) && i < count; i++) - pushInt(ifObjs[i], data); - + for (uint32_t i = start; i < (_ifObjs[0] + 1) && i < count; i++) + pushInt(_ifObjs[i], data); break; } case PID_DEVICE_DESCRIPTOR: @@ -263,6 +258,26 @@ void DeviceObject::maskVersion(uint16_t value) _maskVersion = value; } +void DeviceObject::maxApduLength(uint16_t value) +{ + _maxApduLength = value; +} + +uint16_t DeviceObject::maxApduLength() +{ + return _maxApduLength; +} + +const uint32_t* DeviceObject::ifObj() +{ + return _ifObjs; +} + +void DeviceObject::ifObj(const uint32_t* value) +{ + _ifObjs = value; +} + 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 de86bb5..2f01665 100644 --- a/src/knx/device_object.h +++ b/src/knx/device_object.h @@ -37,6 +37,10 @@ public: void version(uint16_t value); uint16_t maskVersion(); void maskVersion(uint16_t value); + uint16_t maxApduLength(); + void maxApduLength(uint16_t value); + const uint32_t* ifObj(); + void ifObj(const uint32_t* value); protected: uint8_t propertyCount(); PropertyDescription* propertyDescriptions(); @@ -51,4 +55,6 @@ private: 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; }; \ No newline at end of file From b9e42c16d2287fa5d48b062f2cfec6fc0654fa94 Mon Sep 17 00:00:00 2001 From: nanosonde <2073569+nanosonde@users.noreply.github.com> Date: Sun, 27 Oct 2019 14:10:05 +0100 Subject: [PATCH 10/21] Fix Linux compilation --- knx-linux/main.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/knx-linux/main.cpp b/knx-linux/main.cpp index 21e7abc..39e4528 100644 --- a/knx-linux/main.cpp +++ b/knx-linux/main.cpp @@ -1,14 +1,14 @@ #include "knx_facade.h" -#include "knx/bau57B0.h" -//#include "knx/bau2705.h" +//#include "knx/bau57B0.h" +#include "knx/bau27B0.h" #include "knx/group_object_table_object.h" #include "knx/bits.h" #include #include #include -KnxFacade knx; -//KnxFacade knx; +//KnxFacade knx; +KnxFacade knx; long lastsend = 0; From c9819663fa984f28e8e55879d0178d6c85738168 Mon Sep 17 00:00:00 2001 From: nanosonde <2073569+nanosonde@users.noreply.github.com> Date: Mon, 28 Oct 2019 10:18:26 +0100 Subject: [PATCH 11/21] Remove knx-rf-demo folder and move doc --- doc/CC1101-868mhz-radio-module-pinout.jpg | Bin 0 -> 124939 bytes .../README.md => doc/knx_rf_notes.md | 0 .../knx-demo-rf.knxprod} | Bin .../knx-demo-rf.xml} | 0 examples/knx-rf-demo/knx-rf-demo.ino | 70 ------------------ 5 files changed, 70 deletions(-) create mode 100644 doc/CC1101-868mhz-radio-module-pinout.jpg rename examples/knx-rf-demo/README.md => doc/knx_rf_notes.md (100%) rename examples/{knx-rf-demo/knx-rf-demo.knxprod => knx-demo/knx-demo-rf.knxprod} (100%) rename examples/{knx-rf-demo/knx-rf-demo.xml => knx-demo/knx-demo-rf.xml} (100%) delete mode 100644 examples/knx-rf-demo/knx-rf-demo.ino diff --git a/doc/CC1101-868mhz-radio-module-pinout.jpg b/doc/CC1101-868mhz-radio-module-pinout.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1071fc9bf8ab12d6e7f92b4ea717088a8573531e GIT binary patch literal 124939 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 gkDh - -// 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; - -// Entry point for the example -void setup(void) -{ - Serial1.begin(115200); - ArduinoPlatform::SerialDebug = &Serial1; - delay(1000); - Serial1.println("start"); - - // 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(); -} - -// Function that is looped forever -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); - } - -} From e77f61d2491cc48f7af404ea3e7f4769ab7bcf97 Mon Sep 17 00:00:00 2001 From: nanosonde <2073569+nanosonde@users.noreply.github.com> Date: Mon, 28 Oct 2019 10:25:51 +0100 Subject: [PATCH 12/21] Use knx-demo-tp.xml as template for knx-demo-rf.xml --- examples/knx-demo/knx-demo-rf.knxprod | Bin 35610 -> 36279 bytes examples/knx-demo/knx-demo-rf.xml | 113 ++++++++++++++++++++------ 2 files changed, 86 insertions(+), 27 deletions(-) diff --git a/examples/knx-demo/knx-demo-rf.knxprod b/examples/knx-demo/knx-demo-rf.knxprod index 72c5f17435e073f4c2f2543dc6d28eb11b83a966..ece15a9dece821c97d43a5d408528b88290e4584 100644 GIT binary patch delta 3881 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 delta 3205 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 diff --git a/examples/knx-demo/knx-demo-rf.xml b/examples/knx-demo/knx-demo-rf.xml index 1c67cf4..38993f6 100644 --- a/examples/knx-demo/knx-demo-rf.xml +++ b/examples/knx-demo/knx-demo-rf.xml @@ -1,73 +1,132 @@  - + - - + + - + - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + - + + + + + - - + + + + - - + + + + - - + + - + - + - - - - + + + + + + + + + + - + - + - - - + + + From 93a92228b89c66f7e3f61668c9e168e2a3ccc859 Mon Sep 17 00:00:00 2001 From: nanosonde <2073569+nanosonde@users.noreply.github.com> Date: Mon, 28 Oct 2019 11:01:01 +0100 Subject: [PATCH 13/21] Add enum for NM_Read_SerialNumber_By_* --- src/knx/bau_systemB.cpp | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/knx/bau_systemB.cpp b/src/knx/bau_systemB.cpp index 807b5ef..9840c0d 100644 --- a/src/knx/bau_systemB.cpp +++ b/src/knx/bau_systemB.cpp @@ -3,6 +3,14 @@ #include #include +enum NmReadSerialNumberType +{ + NM_Read_SerialNumber_By_ProgrammingMode = 0x01, + NM_Read_SerialNumber_By_ExFactoryState = 0x02, + NM_Read_SerialNumber_By_PowerReset = 0x03, + NM_Read_SerialNumber_By_ManufacturerSpecific = 0xFE, +}; + BauSystemB::BauSystemB(Platform& platform): _memory(platform), _addrTable(platform), _assocTable(platform), _groupObjTable(platform), _appProgram(platform), _platform(platform), _appLayer(_assocTable, *this), @@ -359,9 +367,9 @@ void BauSystemB::systemNetworkParameterReadIndication(Priority priority, HopCoun popByte(operand, testInfo + 1); // First byte (+ 0) contains only 4 reserved bits (0) // See KNX spec. 3.5.2 p.33 (Management Procedures: Procedures with A_SystemNetworkParameter_Read) - switch(operand) + switch((NmReadSerialNumberType)operand) { - case 0x01: // NM_Read_SerialNumber_By_ProgrammingMode + case NM_Read_SerialNumber_By_ProgrammingMode: // NM_Read_SerialNumber_By_ProgrammingMode // Only send a reply if programming mode is on if (_deviceObj.progMode() && (objectType == OT_DEVICE) && (propertyId == PID_SERIAL_NUMBER)) { @@ -373,13 +381,13 @@ void BauSystemB::systemNetworkParameterReadIndication(Priority priority, HopCoun } break; - case 0x02: // NM_Read_SerialNumber_By_ExFactoryState + case NM_Read_SerialNumber_By_ExFactoryState: // NM_Read_SerialNumber_By_ExFactoryState break; - case 0x03: // NM_Read_SerialNumber_By_PowerReset + case NM_Read_SerialNumber_By_PowerReset: // NM_Read_SerialNumber_By_PowerReset break; - case 0xFE: // Manufacturer specific use of A_SystemNetworkParameter_Read + case NM_Read_SerialNumber_By_ManufacturerSpecific: // Manufacturer specific use of A_SystemNetworkParameter_Read break; } } From 1bf8874c05592dfeef2ce9e9226fcd13570ed217 Mon Sep 17 00:00:00 2001 From: nanosonde <2073569+nanosonde@users.noreply.github.com> Date: Mon, 28 Oct 2019 11:04:15 +0100 Subject: [PATCH 14/21] Change default bauNumber to zero --- src/knx/device_object.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/knx/device_object.h b/src/knx/device_object.h index 2f01665..35704f1 100644 --- a/src/knx/device_object.h +++ b/src/knx/device_object.h @@ -50,7 +50,7 @@ private: uint8_t _prgMode = 0; uint16_t _ownAddress = 0; uint16_t _manufacturerId = 0xfa; //Default to KNXA - uint32_t _bauNumber = 0xaabbccdd; + uint32_t _bauNumber = 0; char _orderNumber[10] = ""; uint8_t _hardwareType[6] = { 0, 0, 0, 0, 0, 0}; uint16_t _version = 0; From 6e654d4bf5884ca9c19896b905abfcaeb88d1993 Mon Sep 17 00:00:00 2001 From: nanosonde <2073569+nanosonde@users.noreply.github.com> Date: Mon, 28 Oct 2019 12:08:01 +0100 Subject: [PATCH 15/21] Remove GPIO methods from platform classes --- src/arduino_platform.cpp | 20 ----- src/arduino_platform.h | 5 -- src/knx/bits.h | 2 + src/knx/platform.h | 5 -- src/knx/rf_physical_layer.cpp | 48 +++++----- src/linux_platform.cpp | 163 +++++++++++++++------------------- src/linux_platform.h | 7 -- 7 files changed, 96 insertions(+), 154 deletions(-) diff --git a/src/arduino_platform.cpp b/src/arduino_platform.cpp index 874f3b1..58d9986 100644 --- a/src/arduino_platform.cpp +++ b/src/arduino_platform.cpp @@ -157,26 +157,6 @@ int ArduinoPlatform::readWriteSpi(uint8_t *data, size_t len) return 0; } -void ArduinoPlatform::setupGpio(uint32_t dwPin, uint32_t dwMode) -{ - pinMode(dwPin, dwMode); -} - -void ArduinoPlatform::closeGpio(uint32_t dwPin) -{ - // not used -} - -void ArduinoPlatform::writeGpio(uint32_t dwPin, uint32_t dwVal) -{ - digitalWrite(dwPin, dwVal); -} - -uint32_t ArduinoPlatform::readGpio(uint32_t dwPin) -{ - return digitalRead(dwPin); -} - void print(const char* s) { ArduinoPlatform::SerialDebug->print(s); diff --git a/src/arduino_platform.h b/src/arduino_platform.h index 53a32a8..d7b6f69 100644 --- a/src/arduino_platform.h +++ b/src/arduino_platform.h @@ -40,11 +40,6 @@ class ArduinoPlatform : public Platform void closeSpi() override; int readWriteSpi (uint8_t *data, size_t len) override; - virtual void setupGpio(uint32_t dwPin, uint32_t dwMode) override; - virtual void closeGpio(uint32_t dwPin) override; - virtual void writeGpio(uint32_t dwPin, uint32_t dwVal) override; - virtual uint32_t readGpio(uint32_t dwPin) override; - static Stream* SerialDebug; protected: diff --git a/src/knx/bits.h b/src/knx/bits.h index de25682..3e6a95e 100644 --- a/src/knx/bits.h +++ b/src/knx/bits.h @@ -26,9 +26,11 @@ #define RISING 4 void delay(uint32_t millis); +void delayMicroseconds (unsigned int howLong); uint32_t millis(); void pinMode(uint32_t dwPin, uint32_t dwMode); void digitalWrite(uint32_t dwPin, uint32_t dwVal); +uint32_t digitalRead(uint32_t dwPin); typedef void (*voidFuncPtr)(void); void attachInterrupt(uint32_t pin, voidFuncPtr callback, uint32_t mode); diff --git a/src/knx/platform.h b/src/knx/platform.h index 903ad32..5a082aa 100644 --- a/src/knx/platform.h +++ b/src/knx/platform.h @@ -33,11 +33,6 @@ class Platform virtual void closeSpi() = 0; virtual int readWriteSpi (uint8_t *data, size_t len) = 0; - virtual void setupGpio(uint32_t dwPin, uint32_t dwMode) = 0; - virtual void closeGpio(uint32_t dwPin) = 0; - virtual void writeGpio(uint32_t dwPin, uint32_t dwVal) = 0; - virtual uint32_t readGpio(uint32_t dwPin) = 0; - virtual uint8_t* getEepromBuffer(uint16_t size) = 0; virtual void commitToEeprom() = 0; diff --git a/src/knx/rf_physical_layer.cpp b/src/knx/rf_physical_layer.cpp index 03f551a..5b5c9c8 100644 --- a/src/knx/rf_physical_layer.cpp +++ b/src/knx/rf_physical_layer.cpp @@ -250,9 +250,9 @@ void RfPhysicalLayer::spiWriteRegister(uint8_t spi_instr, uint8_t value) tbuf[0] = spi_instr | WRITE_SINGLE_BYTE; tbuf[1] = value; uint8_t len = 2; - _platform.writeGpio(SPI_SS_PIN, LOW); + digitalWrite(SPI_SS_PIN, LOW); _platform.readWriteSpi(tbuf, len); - _platform.writeGpio(SPI_SS_PIN, HIGH); + digitalWrite(SPI_SS_PIN, HIGH); } uint8_t RfPhysicalLayer::spiReadRegister(uint8_t spi_instr) @@ -261,9 +261,9 @@ uint8_t RfPhysicalLayer::spiReadRegister(uint8_t spi_instr) uint8_t rbuf[2] = {0}; rbuf[0] = spi_instr | READ_SINGLE_BYTE; uint8_t len = 2; - _platform.writeGpio(SPI_SS_PIN, LOW); + digitalWrite(SPI_SS_PIN, LOW); _platform.readWriteSpi(rbuf, len); - _platform.writeGpio(SPI_SS_PIN, HIGH); + digitalWrite(SPI_SS_PIN, HIGH); value = rbuf[1]; //printf("SPI_arr_0: 0x%02X\n", rbuf[0]); //printf("SPI_arr_1: 0x%02X\n", rbuf[1]); @@ -275,9 +275,9 @@ uint8_t RfPhysicalLayer::spiWriteStrobe(uint8_t spi_instr) uint8_t tbuf[1] = {0}; tbuf[0] = spi_instr; //printf("SPI_data: 0x%02X\n", tbuf[0]); - _platform.writeGpio(SPI_SS_PIN, LOW); + digitalWrite(SPI_SS_PIN, LOW); _platform.readWriteSpi(tbuf, 1); - _platform.writeGpio(SPI_SS_PIN, HIGH); + digitalWrite(SPI_SS_PIN, HIGH); return tbuf[0]; } @@ -285,9 +285,9 @@ void RfPhysicalLayer::spiReadBurst(uint8_t spi_instr, uint8_t *pArr, uint8_t len { uint8_t rbuf[len + 1]; rbuf[0] = spi_instr | READ_BURST; - _platform.writeGpio(SPI_SS_PIN, LOW); + digitalWrite(SPI_SS_PIN, LOW); _platform.readWriteSpi(rbuf, len + 1); - _platform.writeGpio(SPI_SS_PIN, HIGH); + digitalWrite(SPI_SS_PIN, HIGH); for (uint8_t i=0; i0 on GDO2 - statusGDO2 = _platform.readGpio(GPIO_GDO2_PIN); + statusGDO2 = digitalRead(GPIO_GDO2_PIN); if(prevStatusGDO2 != statusGDO2) { prevStatusGDO2 = statusGDO2; @@ -562,7 +558,7 @@ void RfPhysicalLayer::loop() } // Detect falling edge 1->0 on GDO0 - statusGDO0 = _platform.readGpio(GPIO_GDO0_PIN); + statusGDO0 = digitalRead(GPIO_GDO0_PIN); if(prevStatusGDO0 != statusGDO0) { prevStatusGDO0 = statusGDO0; @@ -655,7 +651,7 @@ void RfPhysicalLayer::loop() } // Detect rising edge 0->1 on GDO2 - statusGDO2 = _platform.readGpio(GPIO_GDO2_PIN); + statusGDO2 = digitalRead(GPIO_GDO2_PIN); if(prevStatusGDO2 != statusGDO2) { prevStatusGDO2 = statusGDO2; @@ -731,7 +727,7 @@ void RfPhysicalLayer::loop() } // Detect falling edge 1->0 on GDO0 - statusGDO0 = _platform.readGpio(GPIO_GDO0_PIN); + statusGDO0 = digitalRead(GPIO_GDO0_PIN); if(prevStatusGDO0 != statusGDO0) { prevStatusGDO0 = statusGDO0; diff --git a/src/linux_platform.cpp b/src/linux_platform.cpp index 8b67a39..758d642 100644 --- a/src/linux_platform.cpp +++ b/src/linux_platform.cpp @@ -572,10 +572,18 @@ void println(void) void pinMode(uint32_t dwPin, uint32_t dwMode) { + gpio_export(dwPin); + gpio_direction(dwPin, dwMode); } void digitalWrite(uint32_t dwPin, uint32_t dwVal) { + gpio_write(dwPin, dwVal); +} + +uint32_t digitalRead(uint32_t dwPin) +{ + return gpio_read(dwPin); } typedef void (*voidFuncPtr)(void); @@ -593,47 +601,24 @@ void LinuxPlatform::cmdLineArgs(int argc, char** argv) _args[argc] = 0; } -void LinuxPlatform::setupGpio(uint32_t dwPin, uint32_t dwMode) -{ - gpio_export(dwPin); - gpio_direction(dwPin, dwMode); -} - -void LinuxPlatform::closeGpio(uint32_t dwPin) -{ - gpio_unexport(dwPin); - // Set direction to input always if we do not need the GPIO anymore? Unsure... - //gpio_direction(dwPin, INPUT); -} - -void LinuxPlatform::writeGpio(uint32_t dwPin, uint32_t dwVal) -{ - gpio_write(dwPin, dwVal); -} - -uint32_t LinuxPlatform::readGpio(uint32_t dwPin) -{ - return gpio_read(dwPin); -} - -/* Datenpuffer fuer die GPIO-Funktionen */ +/* Buffer size for string operations (e.g. snprintf())*/ #define MAXBUFFER 100 -/* GPIO-Pin aktivieren - * Schreiben der Pinnummer nach /sys/class/gpio/export - * Ergebnis: 0 = O.K., -1 = Fehler +/* Activate GPIO-Pin + * Write GPIO pin number to /sys/class/gpio/export + * Result: 0 = success, -1 = error */ int gpio_export(int pin) { - char buffer[MAXBUFFER]; /* Output Buffer */ - ssize_t bytes; /* Datensatzlaenge */ - int fd; /* Filedescriptor */ - int res; /* Ergebnis von write */ + char buffer[MAXBUFFER]; /* Output Buffer */ + ssize_t bytes; /* Used Buffer length */ + int fd; /* Filedescriptor */ + int res; /* Result from write() */ fd = open("/sys/class/gpio/export", O_WRONLY); if (fd < 0) { - perror("Kann nicht auf export schreiben!\n"); + perror("Could not export GPIO pin(open)!\n"); return(-1); } @@ -642,7 +627,7 @@ int gpio_export(int pin) if (res < 0) { - perror("Kann Pin nicht aktivieren (write)!\n"); + perror("Could not export GPIO pin(write)!\n"); return(-1); } @@ -652,21 +637,21 @@ int gpio_export(int pin) return(0); } -/* GPIO-Pin deaktivieren - * Schreiben der Pinnummer nach /sys/class/gpio/unexport - * Ergebnis: 0 = O.K., -1 = Fehler +/* Deactivate GPIO pin + * Write GPIO pin number to /sys/class/gpio/unexport + * Result: 0 = success, -1 = error */ int gpio_unexport(int pin) { - char buffer[MAXBUFFER]; /* Output Buffer */ - ssize_t bytes; /* Datensatzlaenge */ - int fd; /* Filedescriptor */ - int res; /* Ergebnis von write */ + char buffer[MAXBUFFER]; /* Output Buffer */ + ssize_t bytes; /* Used Buffer length */ + int fd; /* Filedescriptor */ + int res; /* Result from write() */ fd = open("/sys/class/gpio/unexport", O_WRONLY); if (fd < 0) { - perror("Kann nicht auf unexport schreiben!\n"); + perror("Could not unexport GPIO pin(open)!\n"); return(-1); } @@ -675,7 +660,7 @@ int gpio_unexport(int pin) if (res < 0) { - perror("Kann Pin nicht deaktivieren (write)!\n"); + perror("Could not unexport GPIO pin(write)!\n"); return(-1); } @@ -683,22 +668,22 @@ int gpio_unexport(int pin) return(0); } -/* Datenrichtung GPIO-Pin festlegen - * Schreiben Pinnummer nach /sys/class/gpioXX/direction - * Richtung dir: 0 = Lesen, 1 = Schreiben - * Ergebnis: 0 = O.K., -1 = Fehler +/* Set GPIO pin mode (input/output) + * Write GPIO pin number to /sys/class/gpioXX/direction + * Direction: 0 = input, 1 = output + * Result: 0 = success, -1 = error */ int gpio_direction(int pin, int dir) { - char path[MAXBUFFER]; /* Buffer fuer Pfad */ - int fd; /* Filedescriptor */ - int res; /* Ergebnis von write */ + char path[MAXBUFFER]; /* Buffer for path */ + int fd; /* Filedescriptor */ + int res; /* Result from write() */ snprintf(path, MAXBUFFER, "/sys/class/gpio/gpio%d/direction", pin); fd = open(path, O_WRONLY); if (fd < 0) { - perror("Kann Datenrichtung nicht setzen (open)!\n"); + perror("Could not set mode for GPIO pin(open)!\n"); return(-1); } @@ -711,7 +696,7 @@ int gpio_direction(int pin, int dir) if (res < 0) { - perror("Kann Datenrichtung nicht setzen (write)!\n"); + perror("Could not set mode for GPIO pin(write)!\n"); return(-1); } @@ -719,26 +704,26 @@ int gpio_direction(int pin, int dir) return(0); } -/* vom GPIO-Pin lesen - * Ergebnis: -1 = Fehler, 0/1 = Portstatus +/* Read from GPIO pin + * Result: -1 = error, 0/1 = GPIO pin state */ int gpio_read(int pin) { - char path[MAXBUFFER]; /* Buffer fuer Pfad */ - int fd; /* Filedescriptor */ - char result[MAXBUFFER] = {0}; /* Buffer fuer Ergebnis */ + char path[MAXBUFFER]; /* Buffer for path */ + int fd; /* Filedescriptor */ + char result[MAXBUFFER] = {0}; /* Buffer for result */ snprintf(path, MAXBUFFER, "/sys/class/gpio/gpio%d/value", pin); fd = open(path, O_RDONLY); if (fd < 0) { - perror("Kann vom GPIO nicht lesen (open)!\n"); + perror("Could not read from GPIO(open)!\n"); return(-1); } if (read(fd, result, 3) < 0) { - perror("Kann vom GPIO nicht lesen (read)!\n"); + perror("Could not read from GPIO(read)!\n"); return(-1); } @@ -746,21 +731,21 @@ int gpio_read(int pin) return(atoi(result)); } -/* auf GPIO schreiben - * Ergebnis: -1 = Fehler, 0 = O.K. +/* Write to GPIO pin + * Result: -1 = error, 0 = success */ int gpio_write(int pin, int value) { - char path[MAXBUFFER]; /* Buffer fuer Pfad */ + char path[MAXBUFFER]; /* Buffer for path */ int fd; /* Filedescriptor */ - int res; /* Ergebnis von write */ + int res; /* Result from write()*/ snprintf(path, MAXBUFFER, "/sys/class/gpio/gpio%d/value", pin); fd = open(path, O_WRONLY); if (fd < 0) { - perror("Kann auf GPIO nicht schreiben (open)!\n"); + perror("Could not write to GPIO(open)!\n"); return(-1); } @@ -773,7 +758,7 @@ int gpio_write(int pin, int value) if (res < 0) { - perror("Kann auf GPIO nicht schreiben (write)!\n"); + perror("Could not write to GPIO(write)!\n"); return(-1); } @@ -781,23 +766,22 @@ int gpio_write(int pin, int value) return(0); } -/* GPIO-Pin auf Detektion einer Flanke setzen. - * Fuer die Flanke (edge) koennen folgende Parameter gesetzt werden: - * 'r' (rising) - steigende Flanke, - * 'f' (falling) - fallende Flanke, - * 'b' (both) - beide Flanken. +/* Set GPIO pin edge detection + * 'r' (rising) + * 'f' (falling) + * 'b' (both) */ int gpio_edge(unsigned int pin, char edge) { - char path[MAXBUFFER]; /* Buffer fuer Pfad */ - int fd; /* Filedescriptor */ + char path[MAXBUFFER]; /* Buffer for path */ + int fd; /* Filedescriptor */ snprintf(path, MAXBUFFER, "/sys/class/gpio/gpio%d/edge", pin); fd = open(path, O_WRONLY | O_NONBLOCK ); if (fd < 0) { - perror("gpio_edge: Kann auf GPIO nicht schreiben (open)!\n"); + perror("Could not set GPIO edge detection(open)!\n"); return(-1); } @@ -816,46 +800,43 @@ int gpio_edge(unsigned int pin, char edge) return 0; } -/* Warten auf Flanke am GPIO-Pin. - * Eingabewerte: pin: GPIO-Pin - * timeout: Wartezeit in Millisekunden - * Der Pin muss voher eingerichtet werden (export, - * direction, edge) - * Rueckgabewerte: <0: Fehler, 0: poll() Timeout, - * 1: Flanke erkannt, Pin lieferte "0" - * 2: Flanke erkannt, Pin lieferte "1" +/* Wait for edge on GPIO pin + * timeout in milliseconds + * Result: <0: error, 0: poll() Timeout, + * 1: edge detected, GPIO pin reads "0" + * 2: edge detected, GPIO pin reads "1" */ int gpio_wait(unsigned int pin, int timeout) { - char path[MAXBUFFER]; /* Buffer fuer Pfad */ - int fd; /* Filedescriptor */ - struct pollfd polldat[1]; /* Variable fuer poll() */ - char buf[MAXBUFFER]; /* Lesepuffer */ - int rc; /* Hilfsvariablen */ + char path[MAXBUFFER]; /* Buffer for path */ + int fd; /* Filedescriptor */ + struct pollfd polldat[1]; /* Variable for poll() */ + char buf[MAXBUFFER]; /* Read buffer */ + int rc; /* Result */ - /* GPIO-Pin dauerhaft oeffnen */ + /* Open GPIO pin */ snprintf(path, MAXBUFFER, "/sys/class/gpio/gpio%d/value", pin); fd = open(path, O_RDONLY | O_NONBLOCK ); if (fd < 0) { - perror("gpio_wait: Kann von GPIO nicht lesen (open)!\n"); + perror("Could not wait for GPIO edge(open)!\n"); return(-1); } - /* poll() vorbereiten */ + /* prepare poll() */ memset((void*)buf, 0, sizeof(buf)); memset((void*)polldat, 0, sizeof(polldat)); polldat[0].fd = fd; polldat[0].events = POLLPRI; - /* eventuell anstehende Interrupts loeschen */ + /* clear any existing detected edges before */ lseek(fd, 0, SEEK_SET); rc = read(fd, buf, MAXBUFFER - 1); rc = poll(polldat, 1, timeout); if (rc < 0) { /* poll() failed! */ - perror("gpio_wait: Poll-Aufruf ging schief!\n"); + perror("Could not wait for GPIO edge(poll)!\n"); close(fd); return(-1); } @@ -870,7 +851,7 @@ int gpio_wait(unsigned int pin, int timeout) { if (rc < 0) { /* read() failed! */ - perror("gpio_wait: Kann von GPIO nicht lesen (read)!\n"); + perror("Could not wait for GPIO edge(read)!\n"); close(fd); return(-2); } diff --git a/src/linux_platform.h b/src/linux_platform.h index cda3af9..a5605cd 100644 --- a/src/linux_platform.h +++ b/src/linux_platform.h @@ -5,7 +5,6 @@ #include #include "knx/platform.h" -extern void delayMicroseconds (unsigned int howLong); extern int gpio_direction(int pin, int dir); extern int gpio_read(int pin); extern int gpio_write(int pin, int value); @@ -55,12 +54,6 @@ public: void closeSpi() override; int readWriteSpi (uint8_t *data, size_t len) override; - //gpio - virtual void setupGpio(uint32_t dwPin, uint32_t dwMode) override; - virtual void closeGpio(uint32_t dwPin) override; - virtual void writeGpio(uint32_t dwPin, uint32_t dwVal) override; - virtual uint32_t readGpio(uint32_t dwPin) override; - //memory uint8_t* getEepromBuffer(uint16_t size) override; void commitToEeprom() override; From 9f2981abcb63ac6191359c3f5faad26cfaf9fbac Mon Sep 17 00:00:00 2001 From: nanosonde <2073569+nanosonde@users.noreply.github.com> Date: Mon, 28 Oct 2019 12:39:15 +0100 Subject: [PATCH 16/21] Add network layer primitives for broadcastIndication() and broadcastConfirm() --- src/knx/data_link_layer.cpp | 8 ++++++-- src/knx/network_layer.cpp | 14 +++++++++++++- src/knx/network_layer.h | 3 +++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/knx/data_link_layer.cpp b/src/knx/data_link_layer.cpp index ca3a107..92d34a6 100644 --- a/src/knx/data_link_layer.cpp +++ b/src/knx/data_link_layer.cpp @@ -36,9 +36,13 @@ void DataLinkLayer::dataConReceived(CemiFrame& frame, bool success) FrameFormat type = frame.frameType(); Priority priority = frame.priority(); NPDU& npdu = frame.npdu(); + SystemBroadcast systemBroadcast = frame.systemBroadcast(); if (addrType == GroupAddress && destination == 0) - _networkLayer.systemBroadcastConfirm(ack, type, priority, source, npdu, success); + if (systemBroadcast == SysBroadcast) + _networkLayer.systemBroadcastConfirm(ack, type, priority, source, npdu, success); + else + _networkLayer.broadcastConfirm(ack, type, priority, source, npdu, success); else _networkLayer.dataConfirm(ack, addrType, destination, type, priority, source, npdu, success); @@ -64,7 +68,7 @@ void DataLinkLayer::frameRecieved(CemiFrame& frame) if (systemBroadcast == SysBroadcast) _networkLayer.systemBroadcastIndication(ack, type, npdu, priority, source); else - _networkLayer.dataIndication(ack, addrType, destination, type, npdu, priority, source); + _networkLayer.broadcastIndication(ack, type, npdu, priority, source); } else { diff --git a/src/knx/network_layer.cpp b/src/knx/network_layer.cpp index 4089c2e..ffea9d3 100644 --- a/src/knx/network_layer.cpp +++ b/src/knx/network_layer.cpp @@ -66,6 +66,18 @@ void NetworkLayer::dataConfirm(AckType ack, AddressType addressType, uint16_t de _transportLayer.dataBroadcastConfirm(ack, hopType, priority, npdu.tpdu(), status); } +void NetworkLayer::broadcastIndication(AckType ack, FrameFormat format, NPDU& npdu, Priority priority, uint16_t source) +{ + HopCountType hopType = npdu.hopCount() == 7 ? UnlimitedRouting : NetworkLayerParameter; + _transportLayer.dataBroadcastIndication(hopType, priority, source, npdu.tpdu()); +} + +void NetworkLayer::broadcastConfirm(AckType ack, FrameFormat format, Priority priority, uint16_t source, NPDU& npdu, bool status) +{ + HopCountType hopType = npdu.hopCount() == 7 ? UnlimitedRouting : NetworkLayerParameter; + _transportLayer.dataBroadcastConfirm(ack, hopType, priority, npdu.tpdu(), status); +} + void NetworkLayer::systemBroadcastIndication(AckType ack, FrameFormat format, NPDU& npdu, Priority priority, uint16_t source) { HopCountType hopType = npdu.hopCount() == 7 ? UnlimitedRouting : NetworkLayerParameter; @@ -75,7 +87,7 @@ void NetworkLayer::systemBroadcastIndication(AckType ack, FrameFormat format, NP void NetworkLayer::systemBroadcastConfirm(AckType ack, FrameFormat format, Priority priority, uint16_t source, NPDU& npdu, bool status) { HopCountType hopType = npdu.hopCount() == 7 ? UnlimitedRouting : NetworkLayerParameter; - _transportLayer.dataBroadcastConfirm(ack, hopType, priority, npdu.tpdu(), status); + _transportLayer.dataSystemBroadcastConfirm(ack, hopType, npdu.tpdu(), priority, status); } void NetworkLayer::dataIndividualRequest(AckType ack, uint16_t destination, HopCountType hopType, Priority priority, TPDU& tpdu) diff --git a/src/knx/network_layer.h b/src/knx/network_layer.h index 2386469..0b1f88e 100644 --- a/src/knx/network_layer.h +++ b/src/knx/network_layer.h @@ -20,6 +20,9 @@ class NetworkLayer Priority priority, uint16_t source); void dataConfirm(AckType ack, AddressType addressType, uint16_t destination, FrameFormat format, Priority priority, uint16_t source, NPDU& npdu, bool status); + void broadcastIndication(AckType ack, FrameFormat format, NPDU& npdu, + Priority priority, uint16_t source); + void broadcastConfirm(AckType ack, FrameFormat format, Priority priority, uint16_t source, NPDU& npdu, bool status); void systemBroadcastIndication(AckType ack, FrameFormat format, NPDU& npdu, Priority priority, uint16_t source); void systemBroadcastConfirm(AckType ack, FrameFormat format, Priority priority, uint16_t source, NPDU& npdu, bool status); From c93dc813674eba65113e62aed30c4b23038c0f2a Mon Sep 17 00:00:00 2001 From: nanosonde <2073569+nanosonde@users.noreply.github.com> Date: Mon, 28 Oct 2019 13:33:06 +0100 Subject: [PATCH 17/21] Add default MEDIUM_TYPE 0 (TP) --- src/knx_facade.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/knx_facade.h b/src/knx_facade.h index 80c6976..ab4c0a4 100644 --- a/src/knx_facade.h +++ b/src/knx_facade.h @@ -2,6 +2,11 @@ #include "knx/bits.h" +// Set default medium type to TP if no external definitions was given +#ifndef MEDIUM_TYPE +#define MEDIUM_TYPE 0 +#endif + #ifdef ARDUINO_ARCH_SAMD #include "samd_platform.h" #include "knx/bau07B0.h" From eb87ec1fe93a62df9e5e666e20cb361abba19d42 Mon Sep 17 00:00:00 2001 From: nanosonde <2073569+nanosonde@users.noreply.github.com> Date: Mon, 28 Oct 2019 14:03:35 +0100 Subject: [PATCH 18/21] Compile KNX-RF specific bau27B0 and layers only if MEDIUM_TYPE is 2 (RF). For knx-linux add MEDIUM_TYPE 0 (TP) as default to the CMakeLists.txt --- knx-linux/CMakeLists.txt | 2 +- src/knx/bau27B0.cpp | 4 ++++ src/knx/rf_data_link_layer.cpp | 4 ++++ src/knx/rf_physical_layer.cpp | 4 ++++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/knx-linux/CMakeLists.txt b/knx-linux/CMakeLists.txt index 76d1b3b..dd55a25 100644 --- a/knx-linux/CMakeLists.txt +++ b/knx-linux/CMakeLists.txt @@ -78,4 +78,4 @@ include_directories(../src) set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -Wall -Wno-unknown-pragmas -Wno-switch -g -O0") set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Wall -Wno-unknown-pragmas -Wno-switch -g -O0") set_property(TARGET knx-linux PROPERTY CXX_STANDARD 11) -install(TARGETS knx-linux RUNTIME DESTINATION /tmp) +add_definitions(-DMEDIUM_TYPE=0) diff --git a/src/knx/bau27B0.cpp b/src/knx/bau27B0.cpp index f704eb5..3e1b548 100644 --- a/src/knx/bau27B0.cpp +++ b/src/knx/bau27B0.cpp @@ -1,3 +1,5 @@ +#if MEDIUM_TYPE == 2 + #include "bau27B0.h" #include "bits.h" #include @@ -113,3 +115,5 @@ void Bau27B0::individualAddressSerialNumberReadIndication(Priority priority, Hop if (!memcmp(knxSerialNumber, curSerialNumber, 6)) _appLayer.IndividualAddressSerialNumberReadResponse(priority, hopType, _rfMediumObj.rfDomainAddress(), knxSerialNumber); } + +#endif // #if MEDIUM_TYPE == 2 diff --git a/src/knx/rf_data_link_layer.cpp b/src/knx/rf_data_link_layer.cpp index 253083e..f4b6ff6 100644 --- a/src/knx/rf_data_link_layer.cpp +++ b/src/knx/rf_data_link_layer.cpp @@ -1,3 +1,5 @@ +#if MEDIUM_TYPE == 2 + #include "rf_physical_layer.h" #include "rf_data_link_layer.h" @@ -363,3 +365,5 @@ void RfDataLinkLayer::loadNextTxFrame(uint8_t** sendBuffer, uint16_t* sendBuffer } delete tx_frame; } + +#endif // #if MEDIUM_TYPE == 2 diff --git a/src/knx/rf_physical_layer.cpp b/src/knx/rf_physical_layer.cpp index 5b5c9c8..590667d 100644 --- a/src/knx/rf_physical_layer.cpp +++ b/src/knx/rf_physical_layer.cpp @@ -1,3 +1,5 @@ +#if MEDIUM_TYPE == 2 + #include "rf_physical_layer.h" #include "rf_data_link_layer.h" @@ -793,3 +795,5 @@ void RfPhysicalLayer::loop() break; } } + +#endif // #if MEDIUM_TYPE == 2 From ddea3eab994b6829c0b6a6fcc1729c1bf992d14e Mon Sep 17 00:00:00 2001 From: nanosonde <2073569+nanosonde@users.noreply.github.com> Date: Mon, 28 Oct 2019 18:26:24 +0100 Subject: [PATCH 19/21] knx-linux: use default MEDIUM_TYPE IP --- knx-linux/CMakeLists.txt | 2 +- knx-linux/main.cpp | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/knx-linux/CMakeLists.txt b/knx-linux/CMakeLists.txt index dd55a25..6b2f1ad 100644 --- a/knx-linux/CMakeLists.txt +++ b/knx-linux/CMakeLists.txt @@ -78,4 +78,4 @@ include_directories(../src) set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -Wall -Wno-unknown-pragmas -Wno-switch -g -O0") set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Wall -Wno-unknown-pragmas -Wno-switch -g -O0") set_property(TARGET knx-linux PROPERTY CXX_STANDARD 11) -add_definitions(-DMEDIUM_TYPE=0) +add_definitions(-DMEDIUM_TYPE=5) diff --git a/knx-linux/main.cpp b/knx-linux/main.cpp index 39e4528..382f11a 100644 --- a/knx-linux/main.cpp +++ b/knx-linux/main.cpp @@ -1,14 +1,24 @@ #include "knx_facade.h" -//#include "knx/bau57B0.h" +#if MEDIUM_TYPE == 5 +#include "knx/bau57B0.h" +#elif MEDIUM_TYPE == 2 #include "knx/bau27B0.h" +#else +#error Only MEDIUM_TYPE IP and RF supported +#endif #include "knx/group_object_table_object.h" #include "knx/bits.h" #include #include #include -//KnxFacade knx; +#if MEDIUM_TYPE == 5 +KnxFacade knx; +#elif MEDIUM_TYPE == 2 KnxFacade knx; +#else +#error Only MEDIUM_TYPE IP and RF supported +#endif long lastsend = 0; @@ -96,6 +106,6 @@ int main(int argc, char **argv) knx.loop(); if(knx.configured()) appLoop(); - delayMicroseconds(1000); + delayMicroseconds(100); } -} \ No newline at end of file +} From 15b318992d0023ba04e58d89ef0ad077761dfb89 Mon Sep 17 00:00:00 2001 From: Nanosonde <2073569+nanosonde@users.noreply.github.com> Date: Mon, 28 Oct 2019 20:37:40 +0100 Subject: [PATCH 20/21] Keep GPIO value sysfs file open for reading/writing --- knx-linux/main.cpp | 29 +++++++++++- src/linux_platform.cpp | 102 ++++++++++++++++++++++++----------------- 2 files changed, 87 insertions(+), 44 deletions(-) diff --git a/knx-linux/main.cpp b/knx-linux/main.cpp index 382f11a..9a0144c 100644 --- a/knx-linux/main.cpp +++ b/knx-linux/main.cpp @@ -11,6 +11,16 @@ #include #include #include +#include + +volatile sig_atomic_t loopActive = 1; +void signalHandler(int sig) +{ + (void)sig; + + // can be called asynchronously + loopActive = 0; +} #if MEDIUM_TYPE == 5 KnxFacade knx; @@ -97,15 +107,32 @@ void setup() int main(int argc, char **argv) { + printf("main() start.\n"); + + // Register signals + signal(SIGINT, signalHandler); + signal(SIGTERM, signalHandler); + knx.platform().cmdLineArgs(argc, argv); setup(); - while (1) + while (loopActive) { knx.loop(); if(knx.configured()) appLoop(); delayMicroseconds(100); } + + // pinMode() will automatically export GPIO pin in sysfs + // Read or writing the GPIO pin for the first time automatically + // opens the "value" sysfs file to read or write the GPIO pin value. + // The following calls will close the "value" sysfs fiel for the pin + // and unexport the GPIO pin. + gpio_unexport(SPI_SS_PIN); + gpio_unexport(GPIO_GDO2_PIN); + gpio_unexport(GPIO_GDO0_PIN); + + printf("main() exit.\n"); } diff --git a/src/linux_platform.cpp b/src/linux_platform.cpp index 758d642..e7f8b4a 100644 --- a/src/linux_platform.cpp +++ b/src/linux_platform.cpp @@ -602,7 +602,16 @@ void LinuxPlatform::cmdLineArgs(int argc, char** argv) } /* Buffer size for string operations (e.g. snprintf())*/ -#define MAXBUFFER 100 +#define MAX_STRBUF_SIZE 100 +#define MAX_NUM_GPIO 64 + +static int gpioFds [MAX_NUM_GPIO] = +{ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, +} ; /* Activate GPIO-Pin * Write GPIO pin number to /sys/class/gpio/export @@ -610,10 +619,12 @@ void LinuxPlatform::cmdLineArgs(int argc, char** argv) */ int gpio_export(int pin) { - char buffer[MAXBUFFER]; /* Output Buffer */ - ssize_t bytes; /* Used Buffer length */ - int fd; /* Filedescriptor */ - int res; /* Result from write() */ + char buffer[MAX_STRBUF_SIZE]; /* Output Buffer */ + ssize_t bytes; /* Used Buffer length */ + int fd; /* Filedescriptor */ + int res; /* Result from write() */ + + fprintf(stderr, "Export GPIO pin %d\n", pin); fd = open("/sys/class/gpio/export", O_WRONLY); if (fd < 0) @@ -622,7 +633,7 @@ int gpio_export(int pin) return(-1); } - bytes = snprintf(buffer, MAXBUFFER, "%d", pin); + bytes = snprintf(buffer, MAX_STRBUF_SIZE, "%d", pin); res = write(fd, buffer, bytes); if (res < 0) @@ -643,10 +654,14 @@ int gpio_export(int pin) */ int gpio_unexport(int pin) { - char buffer[MAXBUFFER]; /* Output Buffer */ - ssize_t bytes; /* Used Buffer length */ - int fd; /* Filedescriptor */ - int res; /* Result from write() */ + char buffer[MAX_STRBUF_SIZE]; /* Output Buffer */ + ssize_t bytes; /* Used Buffer length */ + int fd; /* Filedescriptor */ + int res; /* Result from write() */ + + fprintf(stderr, "Unexport GPIO pin %d\n", pin); + + close(gpioFds[pin]); fd = open("/sys/class/gpio/unexport", O_WRONLY); if (fd < 0) @@ -655,7 +670,7 @@ int gpio_unexport(int pin) return(-1); } - bytes = snprintf(buffer, MAXBUFFER, "%d", pin); + bytes = snprintf(buffer, MAX_STRBUF_SIZE, "%d", pin); res = write(fd, buffer, bytes); if (res < 0) @@ -675,11 +690,13 @@ int gpio_unexport(int pin) */ int gpio_direction(int pin, int dir) { - char path[MAXBUFFER]; /* Buffer for path */ - int fd; /* Filedescriptor */ - int res; /* Result from write() */ + char path[MAX_STRBUF_SIZE]; /* Buffer for path */ + int fd; /* Filedescriptor */ + int res; /* Result from write() */ - snprintf(path, MAXBUFFER, "/sys/class/gpio/gpio%d/direction", pin); + fprintf(stderr, "Set GPIO direction for pin %d to %s\n", pin, (dir==INPUT) ? "INPUT":"OUTPUT"); + + snprintf(path, MAX_STRBUF_SIZE, "/sys/class/gpio/gpio%d/direction", pin); fd = open(path, O_WRONLY); if (fd < 0) { @@ -709,26 +726,26 @@ int gpio_direction(int pin, int dir) */ int gpio_read(int pin) { - char path[MAXBUFFER]; /* Buffer for path */ - int fd; /* Filedescriptor */ - char result[MAXBUFFER] = {0}; /* Buffer for result */ + char path[MAX_STRBUF_SIZE]; /* Buffer for path */ + char c; - snprintf(path, MAXBUFFER, "/sys/class/gpio/gpio%d/value", pin); - fd = open(path, O_RDONLY); - if (fd < 0) + snprintf(path, MAX_STRBUF_SIZE, "/sys/class/gpio/gpio%d/value", pin); + if (gpioFds[pin] < 0) + gpioFds[pin] = open(path, O_RDWR); + if (gpioFds[pin] < 0) { perror("Could not read from GPIO(open)!\n"); return(-1); } - if (read(fd, result, 3) < 0) + lseek(gpioFds [pin], 0L, SEEK_SET) ; + if (read(gpioFds[pin], &c, 1) < 0) { perror("Could not read from GPIO(read)!\n"); return(-1); } - close(fd); - return(atoi(result)); + return (c == '0') ? LOW : HIGH; } /* Write to GPIO pin @@ -736,14 +753,14 @@ int gpio_read(int pin) */ int gpio_write(int pin, int value) { - char path[MAXBUFFER]; /* Buffer for path */ - int fd; /* Filedescriptor */ - int res; /* Result from write()*/ + char path[MAX_STRBUF_SIZE]; /* Buffer for path */ + int res; /* Result from write()*/ - snprintf(path, MAXBUFFER, "/sys/class/gpio/gpio%d/value", pin); - fd = open(path, O_WRONLY); + snprintf(path, MAX_STRBUF_SIZE, "/sys/class/gpio/gpio%d/value", pin); + if (gpioFds[pin] < 0) + gpioFds[pin] = open(path, O_RDWR); - if (fd < 0) + if (gpioFds[pin] < 0) { perror("Could not write to GPIO(open)!\n"); return(-1); @@ -751,8 +768,8 @@ int gpio_write(int pin, int value) switch (value) { - case LOW : res = write(fd,"0",1); break; - case HIGH: res = write(fd,"1",1); break; + case LOW : res = write(gpioFds[pin], "0\n", 2); break; + case HIGH: res = write(gpioFds[pin], "1\n", 2); break; default: res = -1; break; } @@ -762,7 +779,6 @@ int gpio_write(int pin, int value) return(-1); } - close(fd); return(0); } @@ -773,10 +789,10 @@ int gpio_write(int pin, int value) */ int gpio_edge(unsigned int pin, char edge) { - char path[MAXBUFFER]; /* Buffer for path */ - int fd; /* Filedescriptor */ + char path[MAX_STRBUF_SIZE]; /* Buffer for path */ + int fd; /* Filedescriptor */ - snprintf(path, MAXBUFFER, "/sys/class/gpio/gpio%d/edge", pin); + snprintf(path, MAX_STRBUF_SIZE, "/sys/class/gpio/gpio%d/edge", pin); fd = open(path, O_WRONLY | O_NONBLOCK ); if (fd < 0) @@ -808,14 +824,14 @@ int gpio_edge(unsigned int pin, char edge) */ int gpio_wait(unsigned int pin, int timeout) { - char path[MAXBUFFER]; /* Buffer for path */ - int fd; /* Filedescriptor */ - struct pollfd polldat[1]; /* Variable for poll() */ - char buf[MAXBUFFER]; /* Read buffer */ - int rc; /* Result */ + char path[MAX_STRBUF_SIZE]; /* Buffer for path */ + int fd; /* Filedescriptor */ + struct pollfd polldat[1]; /* Variable for poll() */ + char buf[MAX_STRBUF_SIZE]; /* Read buffer */ + int rc; /* Result */ /* Open GPIO pin */ - snprintf(path, MAXBUFFER, "/sys/class/gpio/gpio%d/value", pin); + snprintf(path, MAX_STRBUF_SIZE, "/sys/class/gpio/gpio%d/value", pin); fd = open(path, O_RDONLY | O_NONBLOCK ); if (fd < 0) { @@ -831,7 +847,7 @@ int gpio_wait(unsigned int pin, int timeout) /* clear any existing detected edges before */ lseek(fd, 0, SEEK_SET); - rc = read(fd, buf, MAXBUFFER - 1); + rc = read(fd, buf, MAX_STRBUF_SIZE - 1); rc = poll(polldat, 1, timeout); if (rc < 0) From 5f36196e78fd27ab8da7b471d89b676949180574 Mon Sep 17 00:00:00 2001 From: Nanosonde <2073569+nanosonde@users.noreply.github.com> Date: Mon, 28 Oct 2019 20:42:51 +0100 Subject: [PATCH 21/21] Prevent swapping of process --- knx-linux/main.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/knx-linux/main.cpp b/knx-linux/main.cpp index 9a0144c..4b64f64 100644 --- a/knx-linux/main.cpp +++ b/knx-linux/main.cpp @@ -11,7 +11,10 @@ #include #include #include +#include #include +#include +#include volatile sig_atomic_t loopActive = 1; void signalHandler(int sig) @@ -109,6 +112,13 @@ int main(int argc, char **argv) { printf("main() start.\n"); + // Prevent swapping of this process + struct sched_param sp; + memset(&sp, 0, sizeof(sp)); + sp.sched_priority = sched_get_priority_max(SCHED_FIFO); + sched_setscheduler(0, SCHED_FIFO, &sp); + mlockall(MCL_CURRENT | MCL_FUTURE); + // Register signals signal(SIGINT, signalHandler); signal(SIGTERM, signalHandler);