diff --git a/.ci/doxygen.groovy b/.ci/doxygen.groovy index cce4d513f..e325cdb7e 100644 --- a/.ci/doxygen.groovy +++ b/.ci/doxygen.groovy @@ -6,7 +6,7 @@ def call(config) { Documentation/doxygen.sh""" warnings canComputeNew: false, canResolveRelativePaths: false, defaultEncoding: '', - excludePattern: '''.*/hal/architecture/Linux/drivers/.*,.*/hal/architecture/AVR/drivers/.*,.*/drivers/TinyGSM/.*''', + excludePattern: '''.*/hal/architecture/Linux/drivers/.*,.*/hal/transport/PJON/driver/.*,.*/hal/architecture/AVR/drivers/.*,.*/drivers/TinyGSM/.*''', failedTotalAll: '', healthy: '', includePattern: '', messagesPattern: '', parserConfigurations: [[parserName: 'Doxygen', pattern: config.repository_root+'doxygen.log']], unHealthy: '', unstableTotalAll: '0' diff --git a/.mystools/cppcheck/config/suppressions.cfg b/.mystools/cppcheck/config/suppressions.cfg index a2450122c..30a42f60d 100644 --- a/.mystools/cppcheck/config/suppressions.cfg +++ b/.mystools/cppcheck/config/suppressions.cfg @@ -4,3 +4,4 @@ // 3rd party *:hal/architecture/Linux/* *:drivers/* +*:hal/transport/PJON/driver/* \ No newline at end of file diff --git a/MyConfig.h b/MyConfig.h index 0aa451420..c519f2745 100755 --- a/MyConfig.h +++ b/MyConfig.h @@ -204,6 +204,55 @@ * @{ */ +/** + * @defgroup PJONSettingGrpPub PJON + * @ingroup RadioSettingGrpPub + * @brief These options are specific to the PJON wired transport. + * @{ + */ + +/** + * @def MY_PJON + * @brief Define this to use the PJON wired transport for sensor network communication. + */ +//#define MY_PJON + +/** + * @def MY_PJON_PIN + * @brief Define this to change pin for PJON communication + */ +#ifndef MY_PJON_PIN +#define MY_PJON_PIN (12u) +#endif + +/** + * @def MY_DEBUG_VERBOSE_PJON + * @brief Define this for verbose debug prints related to the %PJON driver. + */ +//#define MY_DEBUG_VERBOSE_PJON + +/** + * @def MY_PJON_MAX_RETRIES + * @brief Define this to change max send retry in PJON communication + */ +#ifndef MY_PJON_MAX_RETRIES +#define MY_PJON_MAX_RETRIES (5u) +#endif + +#ifdef MY_PJON + +#ifndef PJON_STRATEGY_ALL +#define PJON_STRATEGY_BITBANG +#endif + +#define PJON_NOT_ASSIGNED (253u) +#define PJON_BROADCAST (255u) + +#define SWBB_MAX_ATTEMPTS (50u) +#define PJON_INCLUDE_SWBB +#endif + +/** @}*/ // End of PJONSettingGrpPub group /** * @defgroup RS485SettingGrpPub RS485 @@ -2226,7 +2275,7 @@ #endif // Enable sensor network "feature" if one of the transport types was enabled -#if defined(MY_RADIO_RF24) || defined(MY_RADIO_NRF5_ESB) || defined(MY_RADIO_RFM69) || defined(MY_RADIO_RFM95) || defined(MY_RS485) +#if defined(MY_RADIO_RF24) || defined(MY_RADIO_NRF5_ESB) || defined(MY_RADIO_RFM69) || defined(MY_RADIO_RFM95) || defined(MY_RS485) || defined(MY_PJON) #define MY_SENSOR_NETWORK #endif @@ -2391,6 +2440,9 @@ #define MY_RS485_DE_PIN #define MY_RS485_DE_INVERSE #define MY_RS485_HWSERIAL +// PJON +#define MY_PJON +#define MY_DEBUG_VERBOSE_PJON // RF24 #define MY_RADIO_RF24 #define MY_RADIO_NRF24 //deprecated diff --git a/MySensors.h b/MySensors.h index 36aee1702..f7d109a72 100644 --- a/MySensors.h +++ b/MySensors.h @@ -285,8 +285,13 @@ MY_DEFAULT_RX_LED_PIN in your sketch instead to enable LEDs #else #define __RS485CNT 0 //!< __RS485CNT #endif +#if defined(MY_PJON) +#define _PJONCNT 1 //!< _PJONCNT +#else +#define _PJONCNT 0 //!< _PJONCNT +#endif -#if (__RF24CNT + __NRF5ESBCNT + __RFM69CNT + __RFM95CNT + __RS485CNT > 1) +#if (__RF24CNT + __NRF5ESBCNT + __RFM69CNT + __RFM95CNT + __RS485CNT + _PJONCNT > 1) #error Only one forward link driver can be activated #endif #endif //DOXYGEN @@ -297,7 +302,7 @@ MY_DEFAULT_RX_LED_PIN in your sketch instead to enable LEDs #endif // TRANSPORT INCLUDES -#if defined(MY_RADIO_RF24) || defined(MY_RADIO_NRF5_ESB) || defined(MY_RADIO_RFM69) || defined(MY_RADIO_RFM95) || defined(MY_RS485) +#if defined(MY_RADIO_RF24) || defined(MY_RADIO_NRF5_ESB) || defined(MY_RADIO_RFM69) || defined(MY_RADIO_RFM95) || defined(MY_RS485) || defined (MY_PJON) #include "hal/transport/MyTransportHAL.h" #include "core/MyTransport.h" @@ -381,6 +386,13 @@ MY_DEFAULT_RX_LED_PIN in your sketch instead to enable LEDs #elif defined(MY_RADIO_RFM95) #include "hal/transport/RFM95/driver/RFM95.cpp" #include "hal/transport/RFM95/MyTransportRFM95.cpp" +#elif defined(MY_PJON) +#include "hal/transport/PJON/driver/PJON.h" +#include "hal/transport/PJON/driver/PJONSoftwareBitBang.h" +#if (PJON_BROADCAST == 0) +#error "You must change PJON_BROADCAST to BROADCAST_ADDRESS (255u) and PJON_NOT_ASSIGNED to other one." +#endif +#include "hal/transport/PJON/MyTransportPJON.cpp" #endif #if (defined(MY_RF24_ENABLE_ENCRYPTION) && defined(MY_RADIO_RF24)) || (defined(MY_NRF5_ESB_ENABLE_ENCRYPTION) && defined(MY_RADIO_NRF5_ESB)) || (defined(MY_RFM69_ENABLE_ENCRYPTION) && defined(MY_RADIO_RFM69)) || (defined(MY_RFM95_ENABLE_ENCRYPTION) && defined(MY_RADIO_RFM95)) diff --git a/examples/AirQualitySensor/AirQualitySensor.ino b/examples/AirQualitySensor/AirQualitySensor.ino index ad680271e..2a2c732a0 100644 --- a/examples/AirQualitySensor/AirQualitySensor.ino +++ b/examples/AirQualitySensor/AirQualitySensor.ino @@ -42,6 +42,7 @@ //#define MY_RADIO_NRF5_ESB //#define MY_RADIO_RFM69 //#define MY_RADIO_RFM95 +//#define MY_PJON #include diff --git a/examples/BatteryPoweredSensor/BatteryPoweredSensor.ino b/examples/BatteryPoweredSensor/BatteryPoweredSensor.ino index ee795354f..5562211e4 100644 --- a/examples/BatteryPoweredSensor/BatteryPoweredSensor.ino +++ b/examples/BatteryPoweredSensor/BatteryPoweredSensor.ino @@ -36,6 +36,7 @@ //#define MY_RADIO_NRF5_ESB //#define MY_RADIO_RFM69 //#define MY_RADIO_RFM95 +//#define MY_PJON #include diff --git a/examples/BinarySwitchSleepSensor/BinarySwitchSleepSensor.ino b/examples/BinarySwitchSleepSensor/BinarySwitchSleepSensor.ino index 9206eb5aa..bbe4437f1 100644 --- a/examples/BinarySwitchSleepSensor/BinarySwitchSleepSensor.ino +++ b/examples/BinarySwitchSleepSensor/BinarySwitchSleepSensor.ino @@ -38,6 +38,7 @@ //#define MY_RADIO_NRF5_ESB //#define MY_RADIO_RFM69 //#define MY_RADIO_RFM95 +//#define MY_PJON #include diff --git a/examples/CO2Sensor/CO2Sensor.ino b/examples/CO2Sensor/CO2Sensor.ino index 253e01124..1a74e2d86 100644 --- a/examples/CO2Sensor/CO2Sensor.ino +++ b/examples/CO2Sensor/CO2Sensor.ino @@ -46,6 +46,7 @@ //#define MY_RADIO_NRF5_ESB //#define MY_RADIO_RFM69 //#define MY_RADIO_RFM95 +//#define MY_PJON #include diff --git a/examples/DimmableLEDActuator/DimmableLEDActuator.ino b/examples/DimmableLEDActuator/DimmableLEDActuator.ino index 31e401d91..f4afbd61c 100644 --- a/examples/DimmableLEDActuator/DimmableLEDActuator.ino +++ b/examples/DimmableLEDActuator/DimmableLEDActuator.ino @@ -43,6 +43,7 @@ //#define MY_RADIO_NRF5_ESB //#define MY_RADIO_RFM69 //#define MY_RADIO_RFM95 +//#define MY_PJON #include diff --git a/examples/DimmableLight/DimmableLight.ino b/examples/DimmableLight/DimmableLight.ino index c72fc4629..440fb583a 100644 --- a/examples/DimmableLight/DimmableLight.ino +++ b/examples/DimmableLight/DimmableLight.ino @@ -37,6 +37,7 @@ //#define MY_RADIO_NRF5_ESB //#define MY_RADIO_RFM69 //#define MY_RADIO_RFM95 +//#define MY_PJON #include diff --git a/examples/DustSensor/DustSensor.ino b/examples/DustSensor/DustSensor.ino index fe561ae47..9ab21a445 100644 --- a/examples/DustSensor/DustSensor.ino +++ b/examples/DustSensor/DustSensor.ino @@ -46,6 +46,7 @@ //#define MY_RADIO_NRF5_ESB //#define MY_RADIO_RFM69 //#define MY_RADIO_RFM95 +//#define MY_PJON #include diff --git a/examples/GatewaySerial/GatewaySerial.ino b/examples/GatewaySerial/GatewaySerial.ino index 3a58f84bd..8d0b1a530 100644 --- a/examples/GatewaySerial/GatewaySerial.ino +++ b/examples/GatewaySerial/GatewaySerial.ino @@ -45,6 +45,10 @@ //#define MY_RADIO_NRF5_ESB //#define MY_RADIO_RFM69 //#define MY_RADIO_RFM95 +//#define MY_PJON + +// Set pin for PJON wired communication. +//#define MY_PJON_PIN 12 // Set LOW transmit power level as default, if you have an amplified NRF-module and // power your radio separately with a good regulator you can turn up PA level. diff --git a/examples/GatewayW5100/GatewayW5100.ino b/examples/GatewayW5100/GatewayW5100.ino index 882d36cc6..45aeba0a3 100644 --- a/examples/GatewayW5100/GatewayW5100.ino +++ b/examples/GatewayW5100/GatewayW5100.ino @@ -49,6 +49,7 @@ //#define MY_RADIO_NRF5_ESB //#define MY_RADIO_RFM69 //#define MY_RADIO_RFM95 +//#define MY_PJON // Enable gateway ethernet module type #define MY_GATEWAY_W5100 diff --git a/examples/GatewayW5100MQTTClient/GatewayW5100MQTTClient.ino b/examples/GatewayW5100MQTTClient/GatewayW5100MQTTClient.ino index f157673f1..85a4d81d4 100644 --- a/examples/GatewayW5100MQTTClient/GatewayW5100MQTTClient.ino +++ b/examples/GatewayW5100MQTTClient/GatewayW5100MQTTClient.ino @@ -65,6 +65,7 @@ #define MY_RADIO_RF24 //#define MY_RADIO_RFM69 //#define MY_RADIO_RFM95 +//#define MY_PJON #define MY_GATEWAY_MQTT_CLIENT diff --git a/examples/MockMySensors/MockMySensors.ino b/examples/MockMySensors/MockMySensors.ino index 264ceed1a..c8f72d2be 100644 --- a/examples/MockMySensors/MockMySensors.ino +++ b/examples/MockMySensors/MockMySensors.ino @@ -34,6 +34,7 @@ //#define MY_RADIO_NRF5_ESB //#define MY_RADIO_RFM69 //#define MY_RADIO_RFM95 +//#define MY_PJON #define MY_NODE_ID 254 diff --git a/examples/MotionSensor/MotionSensor.ino b/examples/MotionSensor/MotionSensor.ino index 691002115..5bd37f219 100644 --- a/examples/MotionSensor/MotionSensor.ino +++ b/examples/MotionSensor/MotionSensor.ino @@ -35,6 +35,7 @@ //#define MY_RADIO_NRF5_ESB //#define MY_RADIO_RFM69 //#define MY_RADIO_RFM95 +//#define MY_PJON #include diff --git a/examples/PHSensor/PHSensor.ino b/examples/PHSensor/PHSensor.ino index bee5bcd75..18c48b680 100644 --- a/examples/PHSensor/PHSensor.ino +++ b/examples/PHSensor/PHSensor.ino @@ -33,6 +33,7 @@ //#define MY_RADIO_NRF5_ESB //#define MY_RADIO_RFM69 //#define MY_RADIO_RFM95 +//#define MY_PJON #include diff --git a/examples/PassiveNode/PassiveNode.ino b/examples/PassiveNode/PassiveNode.ino index d17a0a1bb..04a3c6912 100644 --- a/examples/PassiveNode/PassiveNode.ino +++ b/examples/PassiveNode/PassiveNode.ino @@ -40,6 +40,7 @@ //#define MY_RADIO_NRF5_ESB //#define MY_RADIO_RFM69 //#define MY_RADIO_RFM95 +//#define MY_PJON #include diff --git a/examples/PingPongSensor/PingPongSensor.ino b/examples/PingPongSensor/PingPongSensor.ino index 3328cd107..4ca80ee53 100644 --- a/examples/PingPongSensor/PingPongSensor.ino +++ b/examples/PingPongSensor/PingPongSensor.ino @@ -35,6 +35,7 @@ //#define MY_RADIO_NRF5_ESB //#define MY_RADIO_RFM69 //#define MY_RADIO_RFM95 +//#define MY_PJON #include #include "MYSLog.h" diff --git a/examples/RelayActuator/RelayActuator.ino b/examples/RelayActuator/RelayActuator.ino index 34cb9d9d2..953d5f144 100644 --- a/examples/RelayActuator/RelayActuator.ino +++ b/examples/RelayActuator/RelayActuator.ino @@ -35,6 +35,7 @@ //#define MY_RADIO_NRF5_ESB //#define MY_RADIO_RFM69 //#define MY_RADIO_RFM95 +//#define MY_PJON // Enable repeater functionality for this node #define MY_REPEATER_FEATURE diff --git a/examples/RepeaterNode/RepeaterNode.ino b/examples/RepeaterNode/RepeaterNode.ino index 718c501c5..e77b6208e 100644 --- a/examples/RepeaterNode/RepeaterNode.ino +++ b/examples/RepeaterNode/RepeaterNode.ino @@ -36,6 +36,7 @@ //#define MY_RADIO_NRF5_ESB //#define MY_RADIO_RFM69 //#define MY_RADIO_RFM95 +//#define MY_PJON // Enabled repeater feature for this node #define MY_REPEATER_FEATURE diff --git a/examples/SecretKnockSensor/SecretKnockSensor.ino b/examples/SecretKnockSensor/SecretKnockSensor.ino index 05a096b7c..1534fb9e2 100644 --- a/examples/SecretKnockSensor/SecretKnockSensor.ino +++ b/examples/SecretKnockSensor/SecretKnockSensor.ino @@ -59,6 +59,7 @@ //#define MY_RADIO_NRF5_ESB //#define MY_RADIO_RFM69 //#define MY_RADIO_RFM95 +//#define MY_PJON #include diff --git a/examples/SensebenderGatewaySerial/SensebenderGatewaySerial.ino b/examples/SensebenderGatewaySerial/SensebenderGatewaySerial.ino index c725f91bd..60261bf9d 100644 --- a/examples/SensebenderGatewaySerial/SensebenderGatewaySerial.ino +++ b/examples/SensebenderGatewaySerial/SensebenderGatewaySerial.ino @@ -45,6 +45,7 @@ //#define MY_RADIO_NRF5_ESB //#define MY_RADIO_RFM69 //#define MY_RADIO_RFM95 +//#define MY_PJON // Set LOW transmit power level as default, if you have an amplified NRF-module and // power your radio separately with a good regulator you can turn up PA level. diff --git a/examples/SoilMoistSensor/SoilMoistSensor.ino b/examples/SoilMoistSensor/SoilMoistSensor.ino index 484b3ea3b..a0c6e52f0 100644 --- a/examples/SoilMoistSensor/SoilMoistSensor.ino +++ b/examples/SoilMoistSensor/SoilMoistSensor.ino @@ -68,6 +68,7 @@ //#define MY_RADIO_NRF5_ESB //#define MY_RADIO_RFM69 //#define MY_RADIO_RFM95 +//#define MY_PJON #include // Conversion equation from resistance to % #include diff --git a/examples/UVSensor/UVSensor.ino b/examples/UVSensor/UVSensor.ino index d25932a98..301612df3 100644 --- a/examples/UVSensor/UVSensor.ino +++ b/examples/UVSensor/UVSensor.ino @@ -44,6 +44,7 @@ //#define MY_RADIO_NRF5_ESB //#define MY_RADIO_RFM69 //#define MY_RADIO_RFM95 +//#define MY_PJON #include diff --git a/examples/VibrationSensor/VibrationSensor.ino b/examples/VibrationSensor/VibrationSensor.ino index 9b52b4f2a..56fe05829 100644 --- a/examples/VibrationSensor/VibrationSensor.ino +++ b/examples/VibrationSensor/VibrationSensor.ino @@ -40,6 +40,7 @@ //#define MY_RADIO_NRF5_ESB //#define MY_RADIO_RFM69 //#define MY_RADIO_RFM95 +//#define MY_PJON #include #include diff --git a/examples/WaterMeterPulseSensor/WaterMeterPulseSensor.ino b/examples/WaterMeterPulseSensor/WaterMeterPulseSensor.ino index 72b86f442..ca3d56879 100644 --- a/examples/WaterMeterPulseSensor/WaterMeterPulseSensor.ino +++ b/examples/WaterMeterPulseSensor/WaterMeterPulseSensor.ino @@ -42,6 +42,7 @@ //#define MY_RADIO_NRF5_ESB //#define MY_RADIO_RFM69 //#define MY_RADIO_RFM95 +//#define MY_PJON #include diff --git a/hal/transport/PJON/MyTransportPJON.cpp b/hal/transport/PJON/MyTransportPJON.cpp new file mode 100644 index 000000000..0a0e5e2c7 --- /dev/null +++ b/hal/transport/PJON/MyTransportPJON.cpp @@ -0,0 +1,172 @@ +/* + * The MySensors Arduino library handles the wireless radio link and protocol + * between your home built sensors/actuators and HA controller of choice. + * The sensors forms a self healing radio network with optional repeaters. Each + * repeater and gateway builds a routing tables in EEPROM which keeps track of the + * network topology allowing messages to be routed to nodes. + * + * Created by Henrik Ekblad + * Copyright (C) 2013-2020 Sensnology AB + * Full contributor list: https://github.com/mysensors/MySensors/graphs/contributors + * + * Documentation: http://www.mysensors.org + * Support Forum: http://forum.mysensors.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * version 2 as published by the Free Software Foundation. + * + * MySensors PJON transport layer integration: Adrian Sławiński + * ------------------------------------------------------------------------------- + * + */ + +// Set a higher polling duration if the device is executing long tasks +#define PJON_POLLING_DURATION 1000 + +PJONSoftwareBitBang bus; + +// debug +#if defined(MY_DEBUG_VERBOSE_PJON) +#define PJON_DEBUG(x,...) DEBUG_OUTPUT(x, ##__VA_ARGS__) //!< Debug print +#else +#define PJON_DEBUG(x,...) //!< DEBUG null +#endif + +char _data[MAX_MESSAGE_SIZE]; +uint8_t _packet_len; +unsigned char _packet_from; +bool _packet_received; + +bool transportSend(const uint8_t to, const void *data, const uint8_t length, const bool noACK) +{ + const char *datap = static_cast(data); + char *dataToSend = const_cast(datap); + uint16_t res = bus.send(to, dataToSend, length, + (noACK) ? (bus.config & ~PJON_ACK_REQ_BIT) : PJON_NO_HEADER); + bus.update(); + if(res == PJON_FAIL) { + PJON_DEBUG(PSTR("!PJON:SND:FAIL\n")); + return false; + } else { + return true; + } +} + +void _receiver_function(uint8_t *payload, uint16_t length, const PJON_Packet_Info &packet_info) +{ + PJON_DEBUG(PSTR("PJON:RCV:TO=%" PRIu8 ",LEN=%" PRIu8 "\n"), packet_info.rx.id, length); + if (!_packet_received) { + _packet_len = length; + _packet_received = true; + (void)memcpy((void *)_data, (const void *)payload, length); + } +} + +bool transportInit(void) +{ + PJON_DEBUG(PSTR("PJON:INIT:PIN=%" PRIu8 "\n"), MY_PJON_PIN); + bus.begin(); + bus.set_receiver(_receiver_function); + bus.strategy.set_pin(MY_PJON_PIN); + return true; +} + +void transportSetAddress(const uint8_t address) +{ + bus.set_id(address); +} + +uint8_t transportGetAddress(void) +{ + return bus.device_id(); +} + + +bool transportDataAvailable(void) +{ + bus.receive(PJON_POLLING_DURATION); + bus.update(); + return _packet_received; +} + +bool transportSanityCheck(void) +{ + // not implemented on PHY layer + return true; +} + +uint8_t transportReceive(void *data) +{ + if (_packet_received) { + (void)memcpy(data, (const void *)_data, _packet_len); + _packet_received = false; + } else { + _packet_len = 0; + } + return _packet_len; + +} + +void transportPowerDown(void) +{ + // Nothing to shut down here +} + +void transportPowerUp(void) +{ + // not implemented +} + +void transportSleep(void) +{ + // not implemented +} + +void transportStandBy(void) +{ + // not implemented +} + +int16_t transportGetSendingRSSI(void) +{ + // not implemented + return INVALID_RSSI; +} + +int16_t transportGetReceivingRSSI(void) +{ + // not implemented + return INVALID_RSSI; +} + +int16_t transportGetSendingSNR(void) +{ + // not implemented + return INVALID_SNR; +} + +int16_t transportGetReceivingSNR(void) +{ + // not implemented + return INVALID_SNR; +} + +int16_t transportGetTxPowerPercent(void) +{ + // not implemented + return static_cast(100); +} + +int16_t transportGetTxPowerLevel(void) +{ + // not implemented + return static_cast(100); +} + +bool transportSetTxPowerPercent(const uint8_t powerPercent) +{ + // not possible + (void)powerPercent; + return false; +} diff --git a/hal/transport/PJON/driver/LICENSE.md b/hal/transport/PJON/driver/LICENSE.md new file mode 100644 index 000000000..7c67cd6df --- /dev/null +++ b/hal/transport/PJON/driver/LICENSE.md @@ -0,0 +1,9 @@ +All the software included in this project is experimental and it is distributed "AS IS" without any warranty, use it at your own risk. All specifications, all related software implementations and documentation released as a part of this repository are and will always remain free for personal, educational, experimental and commercial purposes. + +Giovanni Blu Mitolo (gioscarab@gmail.com) Copyright 2010-2020 + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/hal/transport/PJON/driver/PJON.h b/hal/transport/PJON/driver/PJON.h new file mode 100644 index 000000000..c791965bb --- /dev/null +++ b/hal/transport/PJON/driver/PJON.h @@ -0,0 +1,1047 @@ + +/*-O//\ __ __ + |-gfo\ |__| | | | |\ | ® + |!y°o:\ | __| |__| | \| 13.0 + |y"s§+`\ multi-master, multi-media bus network protocol + /so+:-..`\ Copyright 2010-2020 by Giovanni Blu Mitolo gioscarab@gmail.com + |+/:ngr-*.`\ + |5/:%&-a3f.:;\ + \+//u/+g%{osv,,\ + \=+&/osw+olds.\\ + \:/+-.-°-:+oss\ + | | \oy\\ + > < +______-| |-__________________________________________________________________ + +For the PJON® Protocol specification see the specification directory. + +Thanks to the support, expertise, kindness and talent of the following +contributors, the protocol's documentation, specification and implementation +have been strongly tested, enhanced and verified: + + Fred Larsen, Zbigniew Zasieczny, Matheus Garbelini, sticilface, + Felix Barbalet, Oleh Halitskiy, fotosettore, fabpolli, Adrian Sławiński, + Osman Selçuk Aktepe, Jorgen-VikingGod, drtrigon, Endre Karlson, + Wilfried Klaas, budaics, ibantxo, gonnavis, maxidroms83, Evgeny Dontsov, + zcattacz, Valerii Koval, Ivan Kravets, Esben Soeltoft, Alex Grishin, + Andrew Grande, Michael Teeww, Paolo Paolucci, per1234, Santiago Castro, + pacproduct, elusive-code, Emanuele Iannone, Christian Pointner, + Fabian Gärtner, Mauro Mombelli, Remo Kallio, hyndruide, sigmaeo, filogranaf, + Maximiliano Duarte, Viktor Szépe, Shachar Limor, Andrei Volkau, maniekq, + DetAtHome, Michael Branson, chestwood96, Mattze96, Steven Bense, + Jack Anderson, callalilychen and Julio Aguirre. + +Compatible tools: + + - ModuleInterface - https://github.com/fredilarsen/ModuleInterface + - PJON-cython - https://github.com/xlfe/PJON-cython + - PJON-piper - https://github.com/Girgitt/PJON-piper + - PJON-python - https://github.com/Girgitt/PJON-python + - PJON-gRPC - https://github.com/Galitskiy/PJON-gRPC +_____________________________________________________________________________ + +This software is experimental and it is distributed "AS IS" without any +warranty, use it at your own risk. + +Copyright 2010-2020 by Giovanni Blu Mitolo gioscarab@gmail.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ + +#pragma once +#include "interfaces/PJON_Interfaces.h" +#include "PJONDefines.h" + +static void PJON_dummy_receiver_handler( + uint8_t *, // payload + uint16_t, // length + const PJON_Packet_Info & // packet_info +) {}; + +static void PJON_dummy_error_handler( + uint8_t, // code + uint16_t, // data + void * // custom_pointer +) {}; + +template +class PJON +{ +public: + Strategy strategy; + uint8_t config = PJON_TX_INFO_BIT | PJON_ACK_REQ_BIT; + uint8_t data[PJON_PACKET_MAX_LENGTH]; + PJON_Packet_Info last_packet_info; + PJON_Packet packets[PJON_MAX_PACKETS]; + PJON_Endpoint tx; + +#if(PJON_INCLUDE_PACKET_ID) + PJON_Packet_Record recent_packet_ids[PJON_MAX_RECENT_PACKET_IDS]; +#endif + +#if(PJON_INCLUDE_PORT) + uint16_t port = PJON_BROADCAST; +#endif + + /* PJON initialization with no parameters: + State: Local (bus_id: 0.0.0.0) + Acknowledge: true + device id: PJON_NOT_ASSIGNED (255) + Mode: PJON_HALF_DUPLEX + Sender info: true (Sender info is included in the packet) + + PJON bus; */ + + PJON() : strategy(Strategy()) + { + set_default(); + }; + + /* PJON initialization passing device id: + PJON bus(1); */ + + PJON(uint8_t device_id) : strategy(Strategy()) + { + tx.id = device_id; + set_default(); + }; + + /* PJON initialization passing bus and device id: + uint8_t my_bus = {1, 1, 1, 1}; + PJON bus(my_bys, 1); */ + + PJON(const uint8_t *b_id, uint8_t device_id) : strategy(Strategy()) + { + tx.id = device_id; + PJONTools::copy_id(tx.bus_id, b_id, 4); + config |= PJON_MODE_BIT; + set_default(); + }; + +#if(PJON_INCLUDE_MAC) + + /* PJON initialization passing the mac address: + const uint8_t mac[6] = {1, 2, 3, 4, 5, 6}; + PJON bus(mac); */ + + PJON(const uint8_t *mac_addr) : strategy(Strategy()) + { + PJONTools::copy_id(tx.mac, mac_addr, 6); + config |= PJON_MAC_BIT; + set_default(); + }; + +#endif + + /* Begin function to be called after initialization: */ + + void begin() + { + PJON_RANDOM_SEED(PJON_ANALOG_READ(_random_seed) + tx.id); + strategy.begin(tx.id); +#if(PJON_INCLUDE_PACKET_ID) + _packet_id_seed = PJON_RANDOM(65535) + tx.id; +#endif + }; + + /* Compose packet in PJON format: */ + + uint16_t compose_packet( + PJON_Packet_Info info, + uint8_t *destination, + const void *source, + uint16_t length + ) + { + info.header = (info.header == PJON_NO_HEADER) ? config : info.header; + info.tx = tx; +#if(PJON_INCLUDE_PACKET_ID) + if(!info.id && (info.header & PJON_PACKET_ID_BIT)) { + info.id = PJONTools::new_packet_id(_packet_id_seed++); + } +#endif +#if(PJON_INCLUDE_PORT) + if( + (port != PJON_BROADCAST) && (info.port == PJON_BROADCAST) && + (info.header & PJON_PORT_BIT) + ) { + info.port = port; + } +#endif +#if(PJON_INCLUDE_MAC) + if(info.header & PJON_MAC_BIT) { + PJONTools::copy_id(info.tx.mac, tx.mac, 6); + } +#endif + uint16_t l = PJONTools::compose_packet( + info, destination, source, length + ); + if(l < PJON_PACKET_MAX_LENGTH) { + return l; + } + _error(PJON_CONTENT_TOO_LONG, l, _custom_pointer); + return 0; + }; + + /* Get device id: */ + + uint8_t device_id() const + { + return tx.id; + }; + + /* Add packet to buffer (delivery attempt by the next update() call): */ + + uint16_t dispatch( + const PJON_Packet_Info &info, + const void *packet, + uint16_t length, + uint32_t timing = 0, + uint16_t packet_index = PJON_FAIL + ) + { + bool p = (packet_index != PJON_FAIL); + for(uint16_t i = ((p) ? packet_index : 0); i < PJON_MAX_PACKETS; i++) + if(packets[i].state == 0 || p) { + if(!(length = compose_packet( + info, packets[i].content, packet, length + ))) { + return PJON_FAIL; + } + packets[i].length = length; + packets[i].state = PJON_TO_BE_SENT; + packets[i].registration = PJON_MICROS(); + packets[i].timing = timing; + return i; + } + _error(PJON_PACKETS_BUFFER_FULL, PJON_MAX_PACKETS, _custom_pointer); + return PJON_FAIL; + }; + + /* Returns a pointer to the bus id used by the instance: */ + + const uint8_t *get_bus_id() const + { + return tx.bus_id; + }; + + /* Get count of packets: + Don't pass any parameter to count all dispatched packets + Pass a device id to count all it's related packets */ + + uint16_t get_packets_count(uint8_t device_id = PJON_NOT_ASSIGNED) const + { + uint16_t packets_count = 0; + for(uint16_t i = 0; i < PJON_MAX_PACKETS; i++) { + if(packets[i].state == 0) { + continue; + } + if( + device_id == PJON_NOT_ASSIGNED || + packets[i].content[0] == device_id + ) { + packets_count++; + } + } + return packets_count; + }; + + /* Fill a PJON_Packet_Info using parameters: */ + + PJON_Packet_Info fill_info( + uint8_t rx_id, + uint8_t header, + uint16_t packet_id, + uint16_t rx_port + ) + { + PJON_Packet_Info info; + info.rx.id = rx_id; + info.header = header; + PJONTools::copy_id(info.rx.bus_id, tx.bus_id, 4); +#if(PJON_INCLUDE_PACKET_ID) + info.id = packet_id; +#else + (void)packet_id; +#endif +#if(PJON_INCLUDE_PORT) + info.port = rx_port; +#else + (void)rx_port; +#endif + return info; + }; + + /* Calculate packet overhead: */ + + uint8_t packet_overhead(uint8_t header = PJON_NO_HEADER) const + { + return PJONTools::packet_overhead( + (header == PJON_NO_HEADER) ? config : header + ); + }; + + /* Fill a PJON_Packet_Info struct with data parsing a packet: */ + + void parse(const uint8_t *packet, PJON_Packet_Info &packet_info) const + { + PJONTools::parse_header(packet, packet_info); + packet_info.custom_pointer = _custom_pointer; + }; + + /* Try to receive data: */ + + uint16_t receive() + { + uint16_t length = PJON_PACKET_MAX_LENGTH; + uint16_t batch_length = 0; + uint8_t overhead = 0; + bool extended_length = false, mac = false, drop = false; + for(uint16_t i = 0; i < length; i++) { + if(!batch_length) { + batch_length = strategy.receive_frame(data + i, length - i); + if(batch_length == PJON_FAIL || batch_length == 0) { + return PJON_FAIL; + } + } + batch_length--; + + if(i == 0) + if((data[i] != tx.id) && (data[i] != PJON_BROADCAST) && !_router) { + drop = true; + } + + if(i == 1) { + mac = (data[1] & PJON_MAC_BIT); + if( + ( + !_router && + ((config & PJON_MODE_BIT) && !(data[1] & PJON_MODE_BIT)) + ) || ( + (data[0] == PJON_BROADCAST) && (data[1] & PJON_ACK_REQ_BIT) + ) || ( + (data[1] & PJON_EXT_LEN_BIT) && !(data[1] & PJON_CRC_BIT) + ) || ( + !PJON_INCLUDE_PACKET_ID && (data[1] & PJON_PACKET_ID_BIT) + ) || ( + !PJON_INCLUDE_PORT && (data[1] & PJON_PORT_BIT) + ) || ( + (!PJON_INCLUDE_MAC && mac) || (mac && !(data[1] & PJON_CRC_BIT)) + ) || (drop && !mac) + ) { + return PJON_BUSY; + } + extended_length = data[i] & PJON_EXT_LEN_BIT; + overhead = packet_overhead(data[i]); + } + + if((i == 2) && !extended_length) { + length = data[i]; + if( + length < (uint8_t)(overhead + 1) || + length >= PJON_PACKET_MAX_LENGTH + ) { + return PJON_BUSY; + } + if(length > 15 && !(data[1] & PJON_CRC_BIT)) { + return PJON_BUSY; + } + } + + if((i == 3) && extended_length) { + length = (data[i - 1] << 8) | (data[i] & 0xFF); + if( + length < (uint8_t)(overhead + 1) || + length >= PJON_PACKET_MAX_LENGTH + ) { + return PJON_BUSY; + } + if(length > 15 && !(data[1] & PJON_CRC_BIT)) { + return PJON_BUSY; + } + } + + if( + ((data[1] & PJON_MODE_BIT) && !_router && !mac) && + (i > (uint8_t)(3 + extended_length)) && + (i < (uint8_t)(8 + extended_length)) + ) { + if(config & PJON_MODE_BIT) { + if(tx.bus_id[i - 4 - extended_length] != data[i]) { + return PJON_BUSY; + } + } else if(data[i] != 0) { + return PJON_BUSY; // Do not reject localhost + } + } + } + + if( + PJON_crc8::compute(data, 3 + extended_length) != + data[3 + extended_length] + ) { + return PJON_NAK; + } + + if(data[1] & PJON_CRC_BIT) { + if( + !PJON_crc32::compare( + PJON_crc32::compute(data, length - 4), data + (length - 4) + ) + ) { + return PJON_NAK; + } + } else if(PJON_crc8::compute(data, length - 1) != data[length - 1]) { + return PJON_NAK; + } + +#if(PJON_INCLUDE_MAC) + if(mac && (length > 15) && !_router) + if(!PJONTools::id_equality(data + (overhead - 16), tx.mac, 6)) + if(! + PJONTools::id_equality( + data + (overhead - 16), + PJONTools::no_mac(), 6 + ) + ) { + return PJON_BUSY; + } +#endif + + if(data[1] & PJON_ACK_REQ_BIT && data[0] != PJON_BROADCAST) + if((_mode != PJON_SIMPLEX) && !_router) { + strategy.send_response(PJON_ACK); + } + + parse(data, last_packet_info); + +#if(PJON_INCLUDE_PACKET_ID) + if( + !_router && + (last_packet_info.header & PJON_PACKET_ID_BIT) && + known_packet_id(last_packet_info) + ) { + return PJON_ACK; + } +#endif + +#if(PJON_INCLUDE_PORT) + if((port != PJON_BROADCAST) && (port != last_packet_info.port)) { + return PJON_BUSY; + } +#endif + + _receiver( + data + (overhead - PJONTools::crc_overhead(data[1])), + length - overhead, + last_packet_info + ); + + return PJON_ACK; + }; + + /* Try to receive data repeatedly with a maximum duration: */ + + uint16_t receive(uint32_t duration) + { + uint32_t time = PJON_MICROS(); + uint16_t response; + do { + response = receive(); + } while( + (response != PJON_ACK) && + ((uint32_t)(PJON_MICROS() - time) <= duration) + ); + return response; + }; + + /* Remove a packet from buffer: */ + + void remove(uint16_t index) + { + if((index >= 0) && (index < PJON_MAX_PACKETS)) { + packets[index].attempts = 0; + packets[index].length = 0; + packets[index].registration = 0; + packets[index].state = 0; + } + }; + + /* Remove all packets from the buffer: + Don't pass any parameter to delete all packets + Pass a device id to delete all it's related packets */ + + void remove_all_packets(uint8_t device_id = 0) + { + for(uint16_t i = 0; i < PJON_MAX_PACKETS; i++) { + if(packets[i].state == 0) { + continue; + } + if(!device_id || packets[i].content[0] == device_id) { + remove(i); + } + } + }; + + /* Reset a packet sending present in the buffer: */ + + bool reset_packet(uint16_t id) + { + if(!packets[id].timing) { + if(_auto_delete) { + remove(id); + return true; + } + } else { + packets[id].attempts = 0; + packets[id].registration = PJON_MICROS(); + packets[id].state = PJON_TO_BE_SENT; + } + return false; + }; + + /* Schedule a packet sending to the sender of the last packet received. + This function is typically called within the receive callback to + deliver a response to a request. */ + + uint16_t reply(const void *payload, uint16_t length) + { + PJON_Packet_Info info; + info = last_packet_info; + info.rx = info.tx; + info.header = config; +#ifndef PJON_LOCAL + info.hops = 0; +#endif + return dispatch(info, payload, length); + }; + + uint16_t reply_blocking(const void *payload, uint16_t length) + { + PJON_Packet_Info info; + info = last_packet_info; + info.rx = info.tx; + info.header = config; +#ifndef PJON_LOCAL + info.hops = 0; +#endif + return send_packet_blocking(info, payload, length); + }; + + /* Schedule a packet sending: */ + + uint16_t send( + uint8_t rx_id, + const void *payload, + uint16_t length, + uint8_t header = PJON_NO_HEADER, + uint16_t packet_id = 0, + uint16_t rx_port = PJON_BROADCAST + ) + { + PJON_Packet_Info info = fill_info(rx_id, header, packet_id, rx_port); + return dispatch(info, payload, length); + }; + + uint16_t send( + const PJON_Packet_Info &info, + const void *payload, + uint16_t length + ) + { + return dispatch(info, payload, length); + }; + + /* Forward a packet: */ + + uint16_t forward( + PJON_Packet_Info info, + const void *payload, + uint16_t length + ) + { + PJON_Endpoint original_end_point = tx; + tx = info.tx; +#ifndef PJON_LOCAL + if(++info.hops > PJON_MAX_HOPS) { + return PJON_FAIL; + } +#endif + uint16_t result = dispatch(info, payload, length); + tx = original_end_point; + return result; + }; + + /* Forward a packet: */ + + uint16_t forward_blocking( + PJON_Packet_Info info, + const void *payload, + uint16_t length + ) + { + PJON_Endpoint original_end_point = tx; + tx = info.tx; +#ifndef PJON_LOCAL + if(++info.hops > PJON_MAX_HOPS) { + return PJON_FAIL; + } +#endif + uint16_t result = send_packet_blocking(info, payload, length); + tx = original_end_point; + return result; + }; + + /* IMPORTANT: send_repeatedly timing maximum + is 4293014170 microseconds or 71.55 minutes */ + + uint16_t send_repeatedly( + uint8_t rx_id, + const void *payload, + uint16_t length, + uint32_t timing, + uint8_t header = PJON_NO_HEADER, + uint16_t packet_id = 0, + uint16_t rx_port = PJON_BROADCAST + ) + { + PJON_Packet_Info info = fill_info(rx_id, header, packet_id, rx_port); + return dispatch(info, payload, length, timing); + }; + + uint16_t send_repeatedly( + const PJON_Packet_Info &info, + const void *payload, + uint16_t length, + uint32_t timing + ) + { + return dispatch(info, payload, length, timing); + }; + + /* Transmit an already composed packet: */ + + uint16_t send_packet(const uint8_t *payload, uint16_t length) + { + if(!payload) { + return PJON_FAIL; + } + if(_mode != PJON_SIMPLEX && !strategy.can_start()) { + return PJON_BUSY; + } + strategy.send_frame((uint8_t *)payload, length); + if( + payload[0] == PJON_BROADCAST || + !(payload[1] & PJON_ACK_REQ_BIT) || + _mode == PJON_SIMPLEX + ) { + return PJON_ACK; + } + return (strategy.receive_response() == PJON_ACK) ? PJON_ACK : PJON_FAIL; + }; + + /* Compose and transmit a packet passing its info as parameters: */ + + uint16_t send_packet( + uint8_t rx_id, + const void *payload, + uint16_t length, + uint8_t header = PJON_NO_HEADER, + uint16_t packet_id = 0, + uint16_t rx_port = PJON_BROADCAST + ) + { + PJON_Packet_Info info = fill_info(rx_id, header, packet_id, rx_port); + if(!(length = compose_packet(info, data, payload, length))) { + return PJON_FAIL; + } + return send_packet(data, length); + }; + + uint16_t send_packet( + const PJON_Packet_Info &info, + const void *payload, + uint16_t length + ) + { + if(!(length = compose_packet(info, data, payload, length))) { + return PJON_FAIL; + } + return send_packet(data, length); + }; + + /* Transmit a packet without using the packet's buffer. Tries to transmit + a packet multiple times within an internal cycle until the packet is + delivered, or timing limit is reached. */ + + uint16_t send_packet_blocking( + const PJON_Packet_Info &packet_info, + const void *payload, + uint16_t length, + uint32_t timeout = 3500000 + ) + { + uint16_t state = PJON_FAIL; + uint32_t attempts = 0; + uint32_t start = PJON_MICROS(); + uint16_t old_length = length; + + _recursion++; + while( + (state != PJON_ACK) && (attempts <= strategy.get_max_attempts()) && + (uint32_t)(PJON_MICROS() - start) <= timeout + ) { + if(!(length = compose_packet(packet_info, data, payload, old_length))) { + _recursion--; + return PJON_FAIL; + } + state = send_packet(data, length); + if(state == PJON_ACK) { + _recursion--; + return state; + } + attempts++; + if(state != PJON_FAIL) { + strategy.handle_collision(); + } +#if(PJON_RECEIVE_WHILE_SENDING_BLOCKING) + if(_recursion <= 1) { + receive(strategy.back_off(attempts)); + } else +#endif + PJON_DELAY((uint32_t)(strategy.back_off(attempts) / 1000)); + } + _recursion--; + return state; + }; + + uint16_t send_packet_blocking( + uint8_t rx_id, + const void *payload, + uint16_t length, + uint8_t header = PJON_NO_HEADER, + uint16_t packet_id = 0, + uint16_t rx_port = PJON_BROADCAST, + uint32_t timeout = 3500000 + ) + { + PJON_Packet_Info info = fill_info(rx_id, header, packet_id, rx_port); + return send_packet_blocking(info, payload, length, timeout); + }; + + /* In router mode, the receiver function can acknowledge + for selected receiver device ids for which the route is known */ + + void send_acknowledge() + { + strategy.send_response(PJON_ACK); + }; + + /* Set the config bit state: */ + + void set_config_bit(bool new_state, uint8_t bit) + { + if(new_state) { + config |= bit; + } else { + config &= ~bit; + } + }; + + /* Configure acknowledge: + state = true -> Request acknowledgement + state = false -> Do not request acknowledgement */ + + void set_acknowledge(bool state) + { + set_config_bit(state, PJON_ACK_REQ_BIT); + }; + + /* Configure CRC selected for packet checking: + state = true -> Use CRC32 + state = false -> Use CRC8 */ + + void set_crc_32(bool state) + { + set_config_bit(state, PJON_CRC_BIT); + }; + + /* Set communication mode: + mode = 0 or PJON_SIMPLEX -> Communication is mono-directional + mode = 1 or PJON_HALF_DUPLEX -> Communication is bi-directional */ + + void set_communication_mode(bool mode) + { + _mode = mode; + }; + + /* Set a custom receiver callback pointer: + (Generally needed to call a custom member function) */ + + void set_custom_pointer(void *pointer) + { + _custom_pointer = pointer; + }; + + /* Set bus state default configuration: */ + + void set_default() + { + _mode = PJON_HALF_DUPLEX; + set_error(PJON_dummy_error_handler); + set_receiver(PJON_dummy_receiver_handler); + }; + + /* Pass as a parameter a function you previously defined in the code. + This function is called when PJON detects an error + + void error_handler(uint8_t code, uint16_t data) { + Serial.print(code); + Serial.print(" "); + Serial.println(data); + }; + + bus.set_error(error_handler); */ + + void set_error(PJON_Error e) + { + _error = e; + }; + + /* Set the device id passing a single byte (watch out to id collision): */ + + void set_id(uint8_t id) + { + tx.id = id; + }; + + /* Setting bus id */ + + void set_bus_id(const uint8_t *b_id) + { + PJONTools::copy_id(tx.bus_id, b_id, 4); + }; + + /* Configure sender's information inclusion in the packet. + state = true -> +8 bits (device id) in local mode + +40 bits (bus id + device id) in shared mode + state = false -> No overhead added + + If you don't need the sender info disable the inclusion to reduce + overhead and higher communication speed. */ + + void include_sender_info(bool state) + { + set_config_bit(state, PJON_TX_INFO_BIT); + }; + + /* Configure network interface identification inclusion in the packet. + state = true -> +96 bits (sender's and recipient's MAC address) + state = false -> No overhead added */ + + void include_mac(bool state) + { + set_config_bit(state, PJON_MAC_BIT); + }; + +#if(PJON_INCLUDE_MAC) + + /* Returns a pointer to the mac address used by the instance: */ + + const uint8_t *get_mac() const + { + return tx.mac; + }; + + /* Set the mac address used by the instance: + It receives a pointer to the mac address */ + + void set_mac(const uint8_t *mac) + { + PJONTools::copy_id(tx.mac, mac, 6); + }; + +#endif + + /* Configure the bus network behaviour. + state = true -> Include 32 bits bus id or group identification. + state = false -> Use only a 8 bits local device identification. */ + + void set_shared_network(bool state) + { + set_config_bit(state, PJON_MODE_BIT); + }; + + /* Set if packets are automatically deleted in case of success or failure: + state = true -> Packets are deleted automatically + state = false -> Packets are not deleted */ + + void set_packet_auto_deletion(bool state) + { + _auto_delete = state; + }; + + /* Set the analog pin used as a seed for random generation: */ + + void set_random_seed(uint8_t seed) + { + _random_seed = seed; + }; + + /* Pass as a parameter a receiver function you previously defined in your + code that should be called when a message is received. + Inside there you can code how to react when data is received. */ + + void set_receiver(PJON_Receiver r) + { + _receiver = r; + }; + + /* Configure if device acts as a router: + state = true -> All packets are received (acknowledgement not sent) + state = false -> Normal operation */ + + void set_router(bool state) + { + _router = state; + }; + + /* Update the state of the send list: + Checks if there are packets to be sent or to be erased if correctly + delivered. Returns the actual number of packets to be sent. */ + + uint16_t update() + { + uint16_t packets_count = 0; + for(uint16_t i = 0; i < PJON_MAX_PACKETS; i++) { + if(packets[i].state == 0) { + continue; + } + packets_count++; + + if( + (uint32_t)(PJON_MICROS() - packets[i].registration) > + (uint32_t)( + packets[i].timing + + strategy.back_off(packets[i].attempts) + ) + ) { + if(packets[i].state != PJON_ACK) + packets[i].state = + send_packet(packets[i].content, packets[i].length); + } else { + continue; + } + + packets[i].attempts++; + + if(packets[i].state == PJON_ACK) { + packets_count -= reset_packet(i); + continue; + } + + if(packets[i].state != PJON_FAIL && packets[i].state != PJON_ACK) { + strategy.handle_collision(); + } + + if(packets[i].attempts > strategy.get_max_attempts()) { + _error(PJON_CONNECTION_LOST, i, _custom_pointer); + packets_count -= reset_packet(i); + } + } + return packets_count; + }; + +#if(PJON_INCLUDE_PACKET_ID) + + /* Checks if the packet id and its transmitter info are already present + in the known packets buffer, if not add it to the buffer */ + + bool known_packet_id(const PJON_Packet_Info &info) + { + for(uint8_t i = 0; i < PJON_MAX_RECENT_PACKET_IDS; i++) + if( + info.id == recent_packet_ids[i].id && + info.tx.id == recent_packet_ids[i].sender_id && ( + ( + (info.header & PJON_MODE_BIT) && + (recent_packet_ids[i].header & PJON_MODE_BIT) && + PJONTools::id_equality( + (uint8_t *)info.tx.bus_id, + (uint8_t *)recent_packet_ids[i].sender_bus_id, + 4 + ) + ) || ( + !(info.header & PJON_MODE_BIT) && + !(recent_packet_ids[i].header & PJON_MODE_BIT) + ) + ) + ) { + return true; + } + save_packet_id(info); + return false; + }; + + /* Save packet id in the buffer: */ + + void save_packet_id(const PJON_Packet_Info &info) + { + for(uint8_t i = PJON_MAX_RECENT_PACKET_IDS - 1; i > 0; i--) { + recent_packet_ids[i] = recent_packet_ids[i - 1]; + } + recent_packet_ids[0].id = info.id; + recent_packet_ids[0].header = info.header; + recent_packet_ids[0].sender_id = info.tx.id; + PJONTools::copy_id( + recent_packet_ids[0].sender_bus_id, + info.tx.bus_id, + 4 + ); + }; + + /* Configure packet id presence: + state = true -> Include 16 bits packet id + state = false -> Avoid packet id inclusion */ + + void set_packet_id(bool state) + { + set_config_bit(state, PJON_PACKET_ID_BIT); + }; + +#endif + +#if(PJON_INCLUDE_PORT) + + /* Include the port: + p = 1-65535 -> Include 16 bits port id + p = 0 -> Avoid port id inclusion */ + + void include_port(uint16_t p) + { + set_config_bit((p != 0) ? 1 : 0, PJON_PORT_BIT); + port = p; + }; + +#endif + +private: + bool _auto_delete = true; + void *_custom_pointer; + PJON_Error _error; + bool _mode; + uint16_t _packet_id_seed = 0; + uint8_t _random_seed = A0; + PJON_Receiver _receiver; + uint8_t _recursion = 0; + bool _router = false; +}; diff --git a/hal/transport/PJON/driver/PJONAnalogSampling.h b/hal/transport/PJON/driver/PJONAnalogSampling.h new file mode 100644 index 000000000..15353a73b --- /dev/null +++ b/hal/transport/PJON/driver/PJONAnalogSampling.h @@ -0,0 +1,7 @@ + +#pragma once + +#include "PJON.h" +#include "strategies/AnalogSampling/AnalogSampling.h" + +#define PJONAnalogSampling PJON diff --git a/hal/transport/PJON/driver/PJONAny.h b/hal/transport/PJON/driver/PJONAny.h new file mode 100644 index 000000000..53715f3dc --- /dev/null +++ b/hal/transport/PJON/driver/PJONAny.h @@ -0,0 +1,7 @@ + +#pragma once + +#include "PJON.h" +#include "strategies/Any/Any.h" + +#define PJONAny PJON diff --git a/hal/transport/PJON/driver/PJONDefines.h b/hal/transport/PJON/driver/PJONDefines.h new file mode 100644 index 000000000..6c7ffc359 --- /dev/null +++ b/hal/transport/PJON/driver/PJONDefines.h @@ -0,0 +1,453 @@ + +/*-O//\ __ __ + |-gfo\ |__| | | | |\ | ® + |!y°o:\ | __| |__| | \| 13.0 + |y"s§+`\ multi-master, multi-media bus network protocol + /so+:-..`\ Copyright 2010-2020 by Giovanni Blu Mitolo gioscarab@gmail.com + |+/:ngr-*.`\ + |5/:%&-a3f.:;\ + \+//u/+g%{osv,,\ + \=+&/osw+olds.\\ + \:/+-.-°-:+oss\ + | | \oy\\ + > < +______-| |-__________________________________________________________________ + +For the PJON® Protocol specification see the specification directory. + +Thanks to the support, expertise, kindness and talent of the following +contributors, the protocol's documentation, specification and implementation +have been strongly tested, enhanced and verified: + + Fred Larsen, Zbigniew Zasieczny, Matheus Garbelini, sticilface, + Felix Barbalet, Oleh Halitskiy, fotosettore, fabpolli, Adrian Sławiński, + Osman Selçuk Aktepe, Jorgen-VikingGod, drtrigon, Endre Karlson, + Wilfried Klaas, budaics, ibantxo, gonnavis, maxidroms83, Evgeny Dontsov, + zcattacz, Valerii Koval, Ivan Kravets, Esben Soeltoft, Alex Grishin, + Andrew Grande, Michael Teeww, Paolo Paolucci, per1234, Santiago Castro, + pacproduct, elusive-code, Emanuele Iannone, Christian Pointner, + Fabian Gärtner, Mauro Mombelli, Remo Kallio, hyndruide, sigmaeo, filogranaf, + Maximiliano Duarte, Viktor Szépe, Shachar Limor, Andrei Volkau, maniekq, + DetAtHome, Michael Branson, chestwood96, Mattze96, Steven Bense, + Jack Anderson, callalilychen and Julio Aguirre. + +Compatible tools: + + - ModuleInterface - https://github.com/fredilarsen/ModuleInterface + - PJON-cython - https://github.com/xlfe/PJON-cython + - PJON-piper - https://github.com/Girgitt/PJON-piper + - PJON-python - https://github.com/Girgitt/PJON-python + - PJON-gRPC - https://github.com/Galitskiy/PJON-gRPC +_____________________________________________________________________________ + +This software is experimental and it is distributed "AS IS" without any +warranty, use it at your own risk. + +Copyright 2010-2020 by Giovanni Blu Mitolo gioscarab@gmail.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ + +#pragma once +#include "utils/crc/PJON_CRC8.h" +#include "utils/crc/PJON_CRC32.h" + +/* Protocol symbols: */ +#define PJON_ACK 6 +#define PJON_BUSY 666 +#define PJON_NAK 21 + +/* Id used for broadcasting to all devices */ +#ifndef PJON_BROADCAST +#define PJON_BROADCAST 0 +#endif + +/* Device id of still unindexed devices */ +#ifndef PJON_NOT_ASSIGNED +#define PJON_NOT_ASSIGNED 255 +#endif + +/* Internal constants: */ +#define PJON_FAIL 65535 +#define PJON_TO_BE_SENT 74 + +/* Communication modes: */ +#define PJON_SIMPLEX false +#define PJON_HALF_DUPLEX true + +/* Header bits definition: */ + +/* No header present (unacceptable value used)*/ +#define PJON_NO_HEADER 0B01001000 +/* 0 - Local network + 1 - Shared network */ +#define PJON_MODE_BIT 0B00000001 +/* 0 - No info inclusion + 1 - Local: Sender device id included + Shared: Sender device id + Sender bus id */ +#define PJON_TX_INFO_BIT 0B00000010 +/* 0 - Synchronous acknowledgement disabled + 1 - Synchronous acknowledgement enabled */ +#define PJON_ACK_REQ_BIT 0B00000100 +/* 0 - MAC address inclusion disabled + 1 - MAC address inclusion enabled (2x 48 bits) */ +#define PJON_MAC_BIT 0B00001000 +/* 0 - No port id contained + 1 - Port id contained (2 bytes integer) */ +#define PJON_PORT_BIT 0B00010000 +/* 0 - CRC8 (1 byte) included at the end of the packet + 1 - CRC32 (4 bytes) included at the end of the packet */ +#define PJON_CRC_BIT 0B00100000 +/* 0 - 1 byte long (max 255 bytes) + 1 - 2 bytes long (max 65535 bytes) */ +#define PJON_EXT_LEN_BIT 0B01000000 +/* 0 - Packet id not present + 1 - Packet id present */ +#define PJON_PACKET_ID_BIT 0B10000000 + +/* Errors: */ + +#define PJON_CONNECTION_LOST 101 +#define PJON_PACKETS_BUFFER_FULL 102 +#define PJON_CONTENT_TOO_LONG 104 + +/* Constraints: */ + +/* Maximum amount of routers a packet can pass before being discarded: */ +#ifndef PJON_MAX_HOPS +#define PJON_MAX_HOPS 15 +#endif + +/* Packet buffer length, if full PJON_PACKETS_BUFFER_FULL error is thrown. + The packet buffer is preallocated, so its length strongly affects + memory consumption */ +#ifndef PJON_MAX_PACKETS +#define PJON_MAX_PACKETS 5 +#endif + +/* Max packet length, higher if necessary. + The max packet length defines the length of packets pre-allocated buffers + so it strongly affects memory consumption */ +#ifndef PJON_PACKET_MAX_LENGTH +#define PJON_PACKET_MAX_LENGTH 50 +#endif + +/* Maximum packet ids record kept in memory (to avoid duplicated exchanges) */ +#ifndef PJON_MAX_RECENT_PACKET_IDS +#define PJON_MAX_RECENT_PACKET_IDS 10 +#endif + +/* Optional features: */ + +/* If defined includes the packet id feature */ +#ifdef PJON_INCLUDE_PACKET_ID +#undef PJON_INCLUDE_PACKET_ID +#define PJON_INCLUDE_PACKET_ID true +#else +#define PJON_INCLUDE_PACKET_ID false +#endif + +/* If defined includes the port id feature */ +#ifdef PJON_INCLUDE_PORT +#undef PJON_INCLUDE_PORT +#define PJON_INCLUDE_PORT true +#else +#define PJON_INCLUDE_PORT false +#endif + +/* If defined includes the mac address feature */ +#ifdef PJON_INCLUDE_MAC +#undef PJON_INCLUDE_MAC +#define PJON_INCLUDE_MAC true +#else +#define PJON_INCLUDE_MAC false +#endif + +/* Data structures: */ + +struct PJON_Packet { + uint8_t attempts = 0; + uint8_t content[PJON_PACKET_MAX_LENGTH]; + uint16_t length; + uint32_t registration; + uint16_t state = 0; + uint32_t timing = 0; +}; + +struct PJON_Packet_Record { + uint8_t header; + uint8_t sender_id; +#ifndef PJON_LOCAL + uint8_t sender_bus_id[4]; +#endif +#if(PJON_INCLUDE_PACKET_ID) + uint16_t id; +#endif +}; + +struct PJON_Endpoint { + uint8_t id = PJON_NOT_ASSIGNED; +#ifndef PJON_LOCAL + uint8_t bus_id[4] = {0, 0, 0, 0}; +#endif +#if(PJON_INCLUDE_MAC) + uint8_t mac[6] = {0, 0, 0, 0, 0, 0}; +#endif +}; + +struct PJON_Packet_Info { + PJON_Endpoint tx; + PJON_Endpoint rx; + uint8_t header = PJON_NO_HEADER; +#ifndef PJON_LOCAL + void *custom_pointer; + uint8_t hops = 0; +#endif +#if(PJON_INCLUDE_PACKET_ID) + uint16_t id = 0; +#endif +#if(PJON_INCLUDE_PORT) + uint16_t port = PJON_BROADCAST; +#endif +}; + +typedef void (* PJON_Receiver)( + uint8_t *payload, + uint16_t length, + const PJON_Packet_Info &packet_info +); + +typedef void (* PJON_Error)( + uint8_t code, + uint16_t data, + void *custom_pointer +); + +/* PJON general purpose functions: */ + +struct PJONTools { + /* Bus id used as localhost (used by shared mode broadcast and NAT) */ + + static const uint8_t* localhost() + { + static const uint8_t lh[4] = {0, 0, 0, 0}; + return lh; + }; + + /* Unused MAC address value */ + + static const uint8_t* no_mac() + { + static const uint8_t lh[6] = {0, 0, 0, 0, 0, 0}; + return lh; + }; + + /* Calculates the packet's overhead using the header: */ + + static uint8_t packet_overhead(uint8_t header) + { + return ( + ( + (header & PJON_MODE_BIT) ? + (header & PJON_TX_INFO_BIT ? 11 : 6) : + (header & PJON_TX_INFO_BIT ? 2 : 1) + ) + (header & PJON_EXT_LEN_BIT ? 2 : 1) + + (header & PJON_CRC_BIT ? 4 : 1) + + (header & PJON_PORT_BIT ? 2 : 0) + + (header & PJON_PACKET_ID_BIT ? 2 : 0) + + (header & PJON_MAC_BIT ? 12 : 0) + + 2 // header + header's CRC + ); + }; + + /* Calculates the packet's CRC overhead using the header: */ + + static uint8_t crc_overhead(uint8_t header) + { + return (header & PJON_CRC_BIT) ? 4 : 1; + }; + + /* Generates a new unique packet id: */ + + static uint16_t new_packet_id(uint16_t seed) + { + if(!(++seed)) { + seed = 1; + } + return seed; + }; + + /* Copy an id: */ + + static void copy_id(uint8_t dest[], const uint8_t src[], uint8_t length) + { + memcpy(dest, src, length); + }; + + /* Check equality between two ids: */ + + static bool id_equality( + const uint8_t *n_one, + const uint8_t *n_two, + uint8_t length + ) + { + for(uint8_t i = 0; i < length; i++) + if(n_one[i] != n_two[i]) { + return false; + } + return true; + }; + + /* Composes a packet in PJON format: */ + + static uint16_t compose_packet( + PJON_Packet_Info info, + uint8_t *destination, + const void *source, + uint16_t length + ) + { + uint8_t index = 0; + if(length > 255) { + info.header |= PJON_EXT_LEN_BIT; + } +#if(PJON_INCLUDE_PORT) + if(info.port != PJON_BROADCAST) { + info.header |= PJON_PORT_BIT; + } + if((info.header & PJON_PORT_BIT) && (info.port == PJON_BROADCAST)) { + info.header &= ~PJON_PORT_BIT; + } +#endif + if(info.rx.id == PJON_BROADCAST) { + info.header &= ~(PJON_ACK_REQ_BIT); + } + uint16_t new_length = length + packet_overhead(info.header); + bool extended_length = info.header & PJON_EXT_LEN_BIT; + if(new_length > 15 && !(info.header & PJON_CRC_BIT)) { + info.header |= PJON_CRC_BIT; + new_length = (uint16_t)(length + packet_overhead(info.header)); + } + if(new_length > 255 && !extended_length) { + info.header |= PJON_EXT_LEN_BIT; + new_length = (uint16_t)(length + packet_overhead(info.header)); + } + if(new_length >= PJON_PACKET_MAX_LENGTH) { + return new_length; + } + destination[index++] = info.rx.id; + destination[index++] = (uint8_t)info.header; + if(extended_length) { + destination[index++] = (uint8_t)(new_length >> 8); + destination[index++] = (uint8_t)new_length; + destination[index++] = PJON_crc8::compute((uint8_t *)destination, 4); + } else { + destination[index++] = (uint8_t)new_length; + destination[index++] = PJON_crc8::compute((uint8_t *)destination, 3); + } +#ifndef PJON_LOCAL + if(info.header & PJON_MODE_BIT) { + copy_id((uint8_t*) &destination[index], info.rx.bus_id, 4); + index += 4; + if(info.header & PJON_TX_INFO_BIT) { + copy_id((uint8_t*) &destination[index], info.tx.bus_id, 4); + index += 4; + } + destination[index++] = info.hops; + } +#endif + if(info.header & PJON_TX_INFO_BIT) { + destination[index++] = info.tx.id; + } +#if(PJON_INCLUDE_PACKET_ID) + if(info.header & PJON_PACKET_ID_BIT) { + destination[index++] = (uint8_t)(info.id >> 8); + destination[index++] = (uint8_t)info.id; + } +#endif +#if(PJON_INCLUDE_PORT) + if(info.header & PJON_PORT_BIT) { + if(info.port != PJON_BROADCAST) { + destination[index++] = (uint8_t)(info.port >> 8); + destination[index++] = (uint8_t)info.port; + } + } +#endif +#if(PJON_INCLUDE_MAC) + if(info.header & PJON_MAC_BIT) { + copy_id(&destination[index], info.rx.mac, 6); + index += 6; + copy_id(&destination[index], info.tx.mac, 6); + index += 6; + } +#endif + memcpy(destination + index, source, length); + if(info.header & PJON_CRC_BIT) { + uint32_t computed_crc = + PJON_crc32::compute((uint8_t *)destination, new_length - 4); + destination[new_length - 4] = + (uint8_t)((uint32_t)(computed_crc) >> 24); + destination[new_length - 3] = + (uint8_t)((uint32_t)(computed_crc) >> 16); + destination[new_length - 2] = + (uint8_t)((uint32_t)(computed_crc) >> 8); + destination[new_length - 1] = + (uint8_t)((uint32_t)computed_crc); + } else destination[new_length - 1] = + PJON_crc8::compute((uint8_t *)destination, new_length - 1); + return new_length; + }; + + /* Fills a PJON_Packet_Info struct with data parsing a packet: */ + + static void parse_header(const uint8_t *packet, PJON_Packet_Info &info) + { + memset(&info, 0, sizeof info); + uint8_t index = 0; + info.rx.id = packet[index++]; + bool extended_length = packet[index] & PJON_EXT_LEN_BIT; + info.header = packet[index++]; + index += extended_length + 2; // + LENGTH + HEADER CRC +#ifndef PJON_LOCAL + if(info.header & PJON_MODE_BIT) { + copy_id(info.rx.bus_id, packet + index, 4); + index += 4; + if(info.header & PJON_TX_INFO_BIT) { + copy_id(info.tx.bus_id, packet + index, 4); + index += 4; + } + info.hops = packet[index++]; + } +#endif + if(info.header & PJON_TX_INFO_BIT) { + info.tx.id = packet[index++]; + } +#if(PJON_INCLUDE_PACKET_ID) + if(info.header & PJON_PACKET_ID_BIT) { + info.id = (packet[index] << 8) | (packet[index + 1] & 0xFF); + index += 2; + } +#endif +#if(PJON_INCLUDE_PORT) + if(info.header & PJON_PORT_BIT) { + info.port = (packet[index] << 8) | (packet[index + 1] & 0xFF); + index += 2; + } +#endif +#if(PJON_INCLUDE_MAC) + copy_id(info.rx.mac, packet + index, 6); + index += 6; + copy_id(info.tx.mac, packet + index, 6); +#endif + }; +}; diff --git a/hal/transport/PJON/driver/PJONDualUDP.h b/hal/transport/PJON/driver/PJONDualUDP.h new file mode 100644 index 000000000..0477c2b69 --- /dev/null +++ b/hal/transport/PJON/driver/PJONDualUDP.h @@ -0,0 +1,7 @@ + +#pragma once + +#include "PJON.h" +#include "strategies/DualUDP/DualUDP.h" + +#define PJONDualUDP PJON diff --git a/hal/transport/PJON/driver/PJONDynamicRouter.h b/hal/transport/PJON/driver/PJONDynamicRouter.h new file mode 100644 index 000000000..77b164db7 --- /dev/null +++ b/hal/transport/PJON/driver/PJONDynamicRouter.h @@ -0,0 +1,155 @@ + +/*-O//\ __ __ + |-gfo\ |__| | | | |\ | ® + |!y°o:\ | __| |__| | \| 13.0 + |y"s§+`\ multi-master, multi-media bus network protocol + /so+:-..`\ Copyright 2010-2020 by Giovanni Blu Mitolo gioscarab@gmail.com + |+/:ngr-*.`\ + |5/:%&-a3f.:;\ + \+//u/+g%{osv,,\ + \=+&/osw+olds.\\ + \:/+-.-°-:+oss\ + | | \oy\\ + > < +______-| |-__________________________________________________________________ + +PJONDynamicRouter has been contributed by Fred Larsen. + +It performs the same as PJONRouter, but populates the routing table +dynamically based on observed packets from remote buses. +_____________________________________________________________________________ + +This software is experimental and it is distributed "AS IS" without any +warranty, use it at your own risk. + +Copyright 2010-2020 by Giovanni Blu Mitolo gioscarab@gmail.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ + +#pragma once + +#ifndef PJON_ROUTER_TABLE_SIZE +#define PJON_ROUTER_TABLE_SIZE 100 +#endif + +#include "PJONRouter.h" + +class PJONDynamicRouter : public PJONRouter +{ +protected: + + void add_sender_to_routing_table( + const PJON_Packet_Info &packet_info, + uint8_t sender_bus + ) + { + uint8_t start_search = 0; + uint8_t found_bus = find_bus_with_id( + packet_info.tx.bus_id, + packet_info.tx.id, + start_search + ); + // Not found among attached buses or in routing table. Add to table. + if(found_bus == PJON_NOT_ASSIGNED) { + add(packet_info.tx.bus_id, sender_bus); + } + }; + + virtual void dynamic_receiver_function( + uint8_t *payload, + uint16_t length, + const PJON_Packet_Info &packet_info + ) + { + // Do standard routing but also add unknown remote buses to routing table + add_sender_to_routing_table(packet_info, current_bus); + PJONSwitch::dynamic_receiver_function(payload, length, packet_info); + }; + +public: + PJONDynamicRouter() { }; + + PJONDynamicRouter( + uint8_t bus_count, + PJONAny * const buses[], + uint8_t default_gateway = PJON_NOT_ASSIGNED + ) : PJONRouter(bus_count, buses, default_gateway) { }; +}; + +// Specialized class to simplify declaration when using 2 buses +template +class PJONDynamicRouter2 : public PJONDynamicRouter +{ + StrategyLink linkA; + StrategyLink linkB; + PJONAny busA, busB; +public: + PJONDynamicRouter2(uint8_t default_gateway = PJON_NOT_ASSIGNED) + { + PJON* buses[2] = { &busA, &busB }; + PJONSimpleSwitch::connect_buses(2, buses, default_gateway); + busA.set_link(&linkA); + busB.set_link(&linkB); + }; + + PJONAny &get_bus(const uint8_t ix) + { + return ix == 0 ? busA : busB; + } + + A &get_strategy_0() + { + return linkA.strategy; + } + B &get_strategy_1() + { + return linkB.strategy; + } +}; + +// Specialized class to simplify declaration when using 3 buses +template +class PJONDynamicRouter3 : public PJONDynamicRouter +{ + StrategyLink linkA; + StrategyLink linkB; + StrategyLink linkC; + PJONAny busA, busB, busC; +public: + PJONDynamicRouter3(uint8_t default_gateway = PJON_NOT_ASSIGNED) + { + PJON *buses[3] = { &busA, &busB, &busC }; + PJONSimpleSwitch::connect_buses(3, buses, default_gateway); + busA.set_link(&linkA); + busB.set_link(&linkB); + busC.set_link(&linkC); + }; + + PJONAny &get_bus(const uint8_t ix) + { + return ix == 0 ? busA : (ix == 1 ? busB : busC); + } + + A &get_strategy_0() + { + return linkA.strategy; + } + B &get_strategy_1() + { + return linkB.strategy; + } + C &get_strategy_2() + { + return linkC.strategy; + } +}; diff --git a/hal/transport/PJON/driver/PJONESPNOW.h b/hal/transport/PJON/driver/PJONESPNOW.h new file mode 100644 index 000000000..ff12cb0fc --- /dev/null +++ b/hal/transport/PJON/driver/PJONESPNOW.h @@ -0,0 +1,15 @@ + +#pragma once + +/* ESPNOW strategy actively uses the PJON's MAC address feature for this reason + it must be included. */ + +#ifndef PJON_INCLUDE_MAC +#define PJON_INCLUDE_MAC +#endif + +#include "PJON.h" + +#include "strategies/ESPNOW/ESPNOW.h" + +#define PJONESPNOW PJON diff --git a/hal/transport/PJON/driver/PJONEthernetTCP.h b/hal/transport/PJON/driver/PJONEthernetTCP.h new file mode 100644 index 000000000..18ebf62b0 --- /dev/null +++ b/hal/transport/PJON/driver/PJONEthernetTCP.h @@ -0,0 +1,7 @@ + +#pragma once + +#include "PJON.h" +#include "strategies/EthernetTCP/EthernetTCP.h" + +#define PJONEthernetTCP PJON diff --git a/hal/transport/PJON/driver/PJONGlobalUDP.h b/hal/transport/PJON/driver/PJONGlobalUDP.h new file mode 100644 index 000000000..832eb1b2f --- /dev/null +++ b/hal/transport/PJON/driver/PJONGlobalUDP.h @@ -0,0 +1,7 @@ + +#pragma once + +#include "PJON.h" +#include "strategies/GlobalUDP/GlobalUDP.h" + +#define PJONGlobalUDP PJON diff --git a/hal/transport/PJON/driver/PJONInteractiveRouter.h b/hal/transport/PJON/driver/PJONInteractiveRouter.h new file mode 100644 index 000000000..8e73fe66e --- /dev/null +++ b/hal/transport/PJON/driver/PJONInteractiveRouter.h @@ -0,0 +1,233 @@ + +/*-O//\ __ __ + |-gfo\ |__| | | | |\ | ® + |!y°o:\ | __| |__| | \| 13.0 + |y"s§+`\ multi-master, multi-media bus network protocol + /so+:-..`\ Copyright 2010-2020 by Giovanni Blu Mitolo gioscarab@gmail.com + |+/:ngr-*.`\ + |5/:%&-a3f.:;\ + \+//u/+g%{osv,,\ + \=+&/osw+olds.\\ + \:/+-.-°-:+oss\ + | | \oy\\ + > < +______-| |-__________________________________________________________________ + +PJONInteractiveRouter has been contributed by Fred Larsen. + +This class adds functionality to the PJONSwitch, PJONRouter, PJONDynamicRouter +and potential future classes derived from them. This functionality allows a +switch or router to have it's own device id and send and receive packets as a +normal device, but to and from multiple buses. + +It also allows the device to listen to all packets passing through between +buses. + +Probably it is wise to use this functionality only on routers using +strategies that are not timing-critical, for example on buffered media like +serial or Ethernet. If used on timing-critical strategies like SWBB, the +receiver callback should be really fast. +_____________________________________________________________________________ + +This software is experimental and it is distributed "AS IS" without any +warranty, use it at your own risk. + +Copyright 2010-2020 by Giovanni Blu Mitolo gioscarab@gmail.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ + +#pragma once +#include +#include + +typedef void (* PJON_Send_Notification)( + const uint8_t * const payload, + const uint16_t length, + const uint8_t receiver_bus, + const uint8_t sender_bus, + const PJON_Packet_Info &packet_info +); + +template +class PJONInteractiveRouter : public RouterClass +{ +protected: + void *custom_pointer = NULL; + PJON_Receiver receiver = NULL; + PJON_Error error = NULL; + PJON_Send_Notification send_notification = NULL; + bool router = false; + + virtual void dynamic_receiver_function(uint8_t *payload, uint16_t length, + const PJON_Packet_Info &packet_info) + { + // Handle packets to this device, with user-supplied callback and custom ptr + // (If this device has a device id on the source bus, and it is equal to + // the packets receiver_id, the packet is for this device.) + bool packet_is_for_me = ( + RouterClass::buses[RouterClass::current_bus]->tx.id != PJON_NOT_ASSIGNED && + memcmp(RouterClass::buses[RouterClass::current_bus]->tx.bus_id, packet_info.rx.bus_id, 4) == 0 && + RouterClass::buses[RouterClass::current_bus]->tx.id == packet_info.rx.id + ); + + // Take care of other's packets + if(!packet_is_for_me) { + RouterClass::dynamic_receiver_function(payload, length, packet_info); + } else if(packet_info.header & PJON_ACK_REQ_BIT) { + RouterClass::buses[RouterClass::current_bus]->send_acknowledge(); + } + // Call the receive callback _after_ the packet has been delivered + if(router || packet_is_for_me) { + // The packet is for ME :-) + PJON_Packet_Info p_i; + memcpy(&p_i, &packet_info, sizeof(PJON_Packet_Info)); + p_i.custom_pointer = custom_pointer; + if(receiver) { + receiver(payload, length, p_i); + } + } + } + + virtual void dynamic_error_function(uint8_t code, uint16_t data) + { + RouterClass::dynamic_error_function(code, data); + + // Call any user registered error function + if(error) { + error(code, data, custom_pointer); + } + } + + virtual void send_packet(const uint8_t *payload, const uint16_t length, + const uint8_t receiver_bus, const uint8_t sender_bus, + bool &ack_sent, const PJON_Packet_Info &packet_info) + { + RouterClass::send_packet(payload, length, receiver_bus, sender_bus, ack_sent, packet_info); + + // Call any user registered send notification function + if (send_notification) { + send_notification(payload, length, receiver_bus, sender_bus, packet_info); + } + } + +public: + PJONInteractiveRouter() : RouterClass() {} + PJONInteractiveRouter( + uint8_t bus_count, + PJONAny* const buses[], + uint8_t default_gateway = PJON_NOT_ASSIGNED) + : RouterClass(bus_count, buses, default_gateway) {} + + void set_receiver(PJON_Receiver r) + { + receiver = r; + }; + + void set_error(PJON_Error e) + { + error = e; + }; + + void set_send_notification(PJON_Send_Notification s) + { + send_notification = s; + }; + + void set_custom_ptr(void *custom_ptr) + { + custom_pointer = custom_ptr; + }; + + void send_packet( + const uint8_t *payload, + uint16_t length, + const PJON_Packet_Info &packet_info + ) + { + dynamic_receiver_function(payload, length, packet_info); + }; + + // Deliver every packet to receiver callback, or just for this device? + void set_router(bool on) + { + router = on; + }; +}; + +// Specialized class to simplify declaration when using 2 buses +template +class PJONInteractiveRouter2 : public PJONInteractiveRouter +{ + StrategyLink linkA; + StrategyLink linkB; + PJONAny busA, busB; +public: + PJONInteractiveRouter2(uint8_t default_gateway = PJON_NOT_ASSIGNED) + { + PJON* buses[2] = { &busA, &busB }; + PJONSimpleSwitch::connect_buses(2, buses, default_gateway); + busA.set_link(&linkA); + busB.set_link(&linkB); + }; + + PJONAny &get_bus(const uint8_t ix) + { + return ix == 0 ? busA : busB; + } + + A &get_strategy_0() + { + return linkA.strategy; + } + B &get_strategy_1() + { + return linkB.strategy; + } +}; + +// Specialized class to simplify declaration when using 3 buses +template +class PJONInteractiveRouter3 : public PJONInteractiveRouter +{ + StrategyLink linkA; + StrategyLink linkB; + StrategyLink linkC; + PJONAny busA, busB, busC; +public: + PJONInteractiveRouter3(uint8_t default_gateway = PJON_NOT_ASSIGNED) + { + PJON* buses[3] = { &busA, &busB, &busC }; + PJONSimpleSwitch::connect_buses(3, buses, default_gateway); + busA.set_link(&linkA); + busB.set_link(&linkB); + busC.set_link(&linkC); + }; + + PJONAny &get_bus(const uint8_t ix) + { + return ix == 0 ? busA : (ix == 1 ? busB : busC); + } + + A &get_strategy_0() + { + return linkA.strategy; + } + B &get_strategy_1() + { + return linkB.strategy; + } + C &get_strategy_2() + { + return linkC.strategy; + } +}; diff --git a/hal/transport/PJON/driver/PJONLocal.h b/hal/transport/PJON/driver/PJONLocal.h new file mode 100644 index 000000000..9430e5f7b --- /dev/null +++ b/hal/transport/PJON/driver/PJONLocal.h @@ -0,0 +1,493 @@ + +/*-O//\ __ __ + |-gfo\ |__| | | | |\ | ® + |!y°o:\ | __| |__| | \| 13.0 + |y"s§+`\ multi-master, multi-media bus network protocol + /so+:-..`\ Copyright 2010-2020 by Giovanni Blu Mitolo gioscarab@gmail.com + |+/:ngr-*.`\ + |5/:%&-a3f.:;\ + \+//u/+g%{osv,,\ + \=+&/osw+olds.\\ + \:/+-.-°-:+oss\ + | | \oy\\ + > < +______-| |-__________________________________________________________________ + +PJONLocal implements a subset of the PJON protocol's features. It does support +only local mode and does not support packet queueing, infact it can transmit +only one packet at a time without any buffering, although it conserves +interoperability with other classes. + +This class has been developed to enable PJON networking on very limited +microcontrollers, like ATtiny45, ATtiny84 and ATtiny85, where even 1kB +of program memory and 100B of ram make a difference. + + For examples see examples/ARDUINO/Local/SoftwareBitBang/PJONLocal/ + +Thanks to the support, expertise, kindness and talent of the following +contributors, the protocol's documentation, specification and implementation +have been strongly tested, enhanced and verified: + + Fred Larsen, Zbigniew Zasieczny, Matheus Garbelini, sticilface, + Felix Barbalet, Oleh Halitskiy, fotosettore, fabpolli, Adrian Sławiński, + Osman Selçuk Aktepe, Jorgen-VikingGod, drtrigon, Endre Karlson, + Wilfried Klaas, budaics, ibantxo, gonnavis, maxidroms83, Evgeny Dontsov, + zcattacz, Valerii Koval, Ivan Kravets, Esben Soeltoft, Alex Grishin, + Andrew Grande, Michael Teeww, Paolo Paolucci, per1234, Santiago Castro, + pacproduct, elusive-code, Emanuele Iannone, Christian Pointner, + Fabian Gärtner, Mauro Mombelli, Remo Kallio, hyndruide, sigmaeo, filogranaf, + Maximiliano Duarte, Viktor Szépe, Shachar Limor, Andrei Volkau, maniekq, + DetAtHome, Michael Branson, chestwood96, Mattze96 and Steven Bense, + Jack Anderson, callalilychen and Julio Aguirre. + +Compatible tools: + + - ModuleInterface - https://github.com/fredilarsen/ModuleInterface + - PJON-cython - https://github.com/xlfe/PJON-cython + - PJON-piper - https://github.com/Girgitt/PJON-piper + - PJON-python - https://github.com/Girgitt/PJON-python + - PJON-gRPC - https://github.com/Galitskiy/PJON-gRPC +_____________________________________________________________________________ + +This software is experimental and it is distributed "AS IS" without any +warranty, use it at your own risk. + +Copyright 2010-2020 by Giovanni Blu Mitolo gioscarab@gmail.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ + +#pragma once +#define PJON_LOCAL +#include "interfaces/PJON_Interfaces.h" +#include "PJONDefines.h" + +template +class PJONLocal +{ +public: + Strategy strategy; + uint8_t config = PJON_TX_INFO_BIT | PJON_ACK_REQ_BIT; + +#if(PJON_INCLUDE_PACKET_ID) + PJON_Packet_Record recent_packet_ids[PJON_MAX_RECENT_PACKET_IDS]; +#endif + +#if(PJON_INCLUDE_PORT) + uint16_t port = PJON_BROADCAST; +#endif + + /* PJONLocal initialization with no parameters: + PJONLocal bus; */ + + PJONLocal() : strategy(Strategy()) + { + _device_id = PJON_NOT_ASSIGNED; + set_default(); + }; + + /* PJONLocal initialization passing device id: + PJONLocal bus(1); */ + + PJONLocal(uint8_t device_id) : strategy(Strategy()) + { + _device_id = device_id; + set_default(); + }; + + /* Begin function to be called after initialization: */ + + void begin() + { + strategy.begin(_device_id); +#if(PJON_INCLUDE_ASYNC_ACK || PJON_INCLUDE_PACKET_ID) + _packet_id_seed = PJON_RANDOM(65535) + _device_id; +#endif + }; + + /* Compose packet in PJON format: */ + + uint16_t compose_packet( + const uint8_t id, + uint8_t *destination, + const void *source, + uint16_t length, + uint8_t header = PJON_NO_HEADER, + uint16_t packet_id = 0, + uint16_t rx_port = PJON_BROADCAST + ) + { + PJON_Packet_Info info; + info.rx.id = id; + info.tx.id = _device_id; + info.header = (header == PJON_NO_HEADER) ? config : header; +#if(PJON_INCLUDE_PACKET_ID) + if(!packet_id && (info.header & PJON_PACKET_ID_BIT)) { + info.id = PJONTools::new_packet_id(_packet_id_seed++); + } else { + info.id = packet_id; + } +#else + (void)packet_id; +#endif +#if(PJON_INCLUDE_PORT) + info.port = (rx_port == PJON_BROADCAST) ? port : rx_port; +#else + (void)rx_port; +#endif + uint16_t l = + PJONTools::compose_packet(info, destination, source, length); + return l; + }; + + /* Get device id: */ + + uint8_t device_id() const + { + return _device_id; + }; + + /* Returns a pointer to the start of the payload: */ + + uint8_t *get_payload(uint8_t *buffer) + { + return buffer + ( + PJONTools::packet_overhead(buffer[1]) - + PJONTools::crc_overhead(buffer[1]) + ); + } + + /* Calculate packet overhead: */ + + uint8_t packet_overhead(uint8_t header = 0) const + { + return PJONTools::packet_overhead(header); + }; + + /* Fill a PJON_Packet_Info struct with data parsing a packet: */ + + void parse(const uint8_t *packet, PJON_Packet_Info &packet_info) const + { + PJONTools::parse_header(packet, packet_info); + }; + + /* Try to receive data: */ + + uint16_t receive(uint8_t *buffer, PJON_Packet_Info info) + { + uint16_t length = PJON_PACKET_MAX_LENGTH; + uint16_t batch_length = 0; + uint8_t overhead = 0; + bool extended_length = false; + for(uint16_t i = 0; i < length; i++) { + if(!batch_length) { + batch_length = strategy.receive_frame(buffer + i, length - i); + if(batch_length == PJON_FAIL || batch_length == 0) { + return 0; + } + } + batch_length--; + + if(!i && !_router) + if((buffer[i] != _device_id) && (buffer[i] != PJON_BROADCAST)) { + return 0; + } + + if(i == 1) { + if( + (buffer[1] & PJON_MODE_BIT) || + (buffer[1] & PJON_MAC_BIT) || ( + (buffer[0] == PJON_BROADCAST) && (buffer[1] & PJON_ACK_REQ_BIT) + ) || ( + (buffer[1] & PJON_EXT_LEN_BIT) && !(buffer[1] & PJON_CRC_BIT) + ) || ( + !PJON_INCLUDE_PACKET_ID && (buffer[1] & PJON_PACKET_ID_BIT) + ) + ) { + return 0; + } + extended_length = buffer[i] & PJON_EXT_LEN_BIT; + overhead = packet_overhead(buffer[i]); + } + + if((i == 2) && !extended_length) { + length = buffer[i]; + if((length < overhead) || (length >= PJON_PACKET_MAX_LENGTH)) { + return 0; + } + if((length > 15) && !(buffer[1] & PJON_CRC_BIT)) { + return PJON_BUSY; + } + } + + if((i == 3) && extended_length) { + length = (buffer[i - 1] << 8) | (buffer[i] & 0xFF); + if((length < overhead) || (length >= PJON_PACKET_MAX_LENGTH)) { + return 0; + } + if((length > 15) && !(buffer[1] & PJON_CRC_BIT)) { + return PJON_BUSY; + } + } + } + + if( + PJON_crc8::compute(buffer, 3 + extended_length) != + buffer[3 + extended_length] + ) { + return 0; + } + + if(buffer[1] & PJON_CRC_BIT) { + if( + !PJON_crc32::compare( + PJON_crc32::compute(buffer, length - 4), buffer + (length - 4) + ) + ) { + return 0; + } + } else if(PJON_crc8::compute(buffer, length - 1) != buffer[length - 1]) { + return 0; + } + if(buffer[1] & PJON_ACK_REQ_BIT && buffer[0] != PJON_BROADCAST) + if((_mode != PJON_SIMPLEX) && !_router) { + strategy.send_response(PJON_ACK); + } + parse(buffer, info); +#if(PJON_INCLUDE_PACKET_ID) + if( + (info.header & PJON_PACKET_ID_BIT) && + known_packet_id(info) && !_router + ) { + return 0; + } +#endif +#if(PJON_INCLUDE_PORT) + if((port != PJON_BROADCAST) && (port != info.port)) { + return 0; + } +#endif + return length - overhead; + }; + + /* Check if ready to send a packet: */ + + bool ready_to_send() + { + if( + ( + !_retries || ( + (uint32_t)(PJON_MICROS() - _last_send) > + (uint32_t)(strategy.back_off(_retries)) + ) + ) + ) { + return strategy.can_start(); + } + return false; + }; + + /* Transmit an already composed packet: */ + + uint16_t send_packet(const uint8_t *payload, uint16_t length) + { + _last_send = PJON_MICROS(); + strategy.send_frame((uint8_t *)payload, length); + if( + payload[0] == PJON_BROADCAST || !(payload[1] & PJON_ACK_REQ_BIT) || + _mode == PJON_SIMPLEX + ) { + _retries = 0; + return PJON_ACK; + } + uint16_t response = strategy.receive_response(); + if(response == PJON_ACK) { + _retries = 0; + return response; + } + if(_retries < strategy.get_max_attempts()) { + _retries++; + } else { + _retries = 0; + } + if(response == PJON_FAIL) { + return response; + } else { + return PJON_BUSY; + } + }; + + /* Compose and transmit a packet passing its info as parameters: */ + + uint16_t send_packet( + uint8_t id, + uint8_t *buffer, + const void *payload, + uint16_t length, + uint8_t header = PJON_NO_HEADER, + uint16_t packet_id = 0, + uint16_t rx_port = PJON_BROADCAST + ) + { + if(!(length = compose_packet( + id, buffer, payload, length, header, packet_id, rx_port + ))) { + return PJON_FAIL; + } + return send_packet(buffer, length); + }; + + /* In router mode, the receiver function can acknowledge + for selected receiver device ids for which the route is known */ + + void send_acknowledge() + { + strategy.send_response(PJON_ACK); + }; + + /* Set the config bit state: */ + + void set_config_bit(bool new_state, uint8_t bit) + { + if(new_state) { + config |= bit; + } else { + config &= ~bit; + } + }; + + /* Configure synchronous acknowledge presence: + TRUE: Send 8bits synchronous acknowledge when a packet is received + FALSE: Avoid acknowledge transmission */ + + void set_acknowledge(bool state) + { + set_config_bit(state, PJON_ACK_REQ_BIT); + }; + + /* Configure CRC selected for packet checking: + TRUE: CRC32 + FALSE: CRC8 */ + + void set_crc_32(bool state) + { + set_config_bit(state, PJON_CRC_BIT); + }; + + /* Set communication mode: + Passing PJON_SIMPLEX communication is mono-directional + Padding PJON_HALF_DUPLEX communication is bi-directional */ + + void set_communication_mode(uint8_t mode) + { + _mode = mode; + }; + + /* Set default configuration: */ + + void set_default() + { + _mode = PJON_HALF_DUPLEX; + }; + + /* Set the device id passing a single byte (watch out to id collision): */ + + void set_id(uint8_t id) + { + _device_id = id; + }; + + /* Configure sender's information inclusion in the packet. + TRUE: sender's device id (+8bits overhead) + FALSE: No sender's device id inclusion (-8bits overhead) + + If you don't need the sender info disable the inclusion to reduce + overhead and obtain higher communication speed. */ + + void include_sender_info(bool state) + { + set_config_bit(state, PJON_TX_INFO_BIT); + }; + + /* Configure if device acts as a router: + TRUE: device receives messages only for its bus and device id + FALSE: receiver function is always called if data is received */ + + void set_router(bool state) + { + _router = state; + }; + +#if(PJON_INCLUDE_PACKET_ID) + + /* Check if the packet id and its transmitter info are already present in + buffer of recently received packets, if not add it to the buffer. */ + + bool known_packet_id(PJON_Packet_Info info) + { + for(uint8_t i = 0; i < PJON_MAX_RECENT_PACKET_IDS; i++) + if( + info.id == recent_packet_ids[i].id && + info.sender_id == recent_packet_ids[i].sender_id + ) { + return true; + } + save_packet_id(info); + return false; + }; + + /* Save packet id in the buffer: */ + + void save_packet_id(PJON_Packet_Info info) + { + for(uint8_t i = PJON_MAX_RECENT_PACKET_IDS - 1; i > 0; i--) { + recent_packet_ids[i] = recent_packet_ids[i - 1]; + } + recent_packet_ids[0].id = info.id; + recent_packet_ids[0].header = info.header; + recent_packet_ids[0].sender_id = info.sender_id; + }; + + /* Configure packet id presence: + TRUE: include packet id, FALSE: Avoid packet id inclusion */ + + void set_packet_id(bool state) + { + set_config_bit(state, PJON_PACKET_ID_BIT); + }; + +#endif + +#if(PJON_INCLUDE_PORT) + + /* Include the port: + p = 1-65535 -> Include 16 bits port id + p = 0 -> Avoid port id inclusion */ + + void include_port(uint16_t p) + { + set_config_bit((p != 0) ? 1 : 0, PJON_PORT_BIT); + port = p; + }; + +#endif + + +private: + uint32_t _last_send = 0; + uint8_t _mode; + uint16_t _packet_id_seed = 0; + bool _router = false; + uint8_t _retries = 0; +protected: + uint8_t _device_id; +}; diff --git a/hal/transport/PJON/driver/PJONLocalFile.h b/hal/transport/PJON/driver/PJONLocalFile.h new file mode 100644 index 000000000..85f5f6dd0 --- /dev/null +++ b/hal/transport/PJON/driver/PJONLocalFile.h @@ -0,0 +1,7 @@ + +#pragma once + +#include "PJON.h" +#include "strategies/LocalFile/LocalFile.h" + +#define PJONLocalFile PJON diff --git a/hal/transport/PJON/driver/PJONLocalUDP.h b/hal/transport/PJON/driver/PJONLocalUDP.h new file mode 100644 index 000000000..85632a5ed --- /dev/null +++ b/hal/transport/PJON/driver/PJONLocalUDP.h @@ -0,0 +1,7 @@ + +#pragma once + +#include "PJON.h" +#include "strategies/LocalUDP/LocalUDP.h" + +#define PJONLocalUDP PJON diff --git a/hal/transport/PJON/driver/PJONMQTTTranslate.h b/hal/transport/PJON/driver/PJONMQTTTranslate.h new file mode 100644 index 000000000..7546d407b --- /dev/null +++ b/hal/transport/PJON/driver/PJONMQTTTranslate.h @@ -0,0 +1,7 @@ + +#pragma once + +#include "PJON.h" +#include "strategies/MQTTTranslate/MQTTTranslate.h" + +#define PJONMQTTTranslate PJON diff --git a/hal/transport/PJON/driver/PJONOverSampling.h b/hal/transport/PJON/driver/PJONOverSampling.h new file mode 100644 index 000000000..d4fd7e8d8 --- /dev/null +++ b/hal/transport/PJON/driver/PJONOverSampling.h @@ -0,0 +1,7 @@ + +#pragma once + +#include "PJON.h" +#include "strategies/OverSampling/OverSampling.h" + +#define PJONOverSampling PJON diff --git a/hal/transport/PJON/driver/PJONRouter.h b/hal/transport/PJON/driver/PJONRouter.h new file mode 100644 index 000000000..981bee63c --- /dev/null +++ b/hal/transport/PJON/driver/PJONRouter.h @@ -0,0 +1,188 @@ + +/*-O//\ __ __ + |-gfo\ |__| | | | |\ | ® + |!y°o:\ | __| |__| | \| 13.0 + |y"s§+`\ multi-master, multi-media bus network protocol + /so+:-..`\ Copyright 2010-2020 by Giovanni Blu Mitolo gioscarab@gmail.com + |+/:ngr-*.`\ + |5/:%&-a3f.:;\ + \+//u/+g%{osv,,\ + \=+&/osw+olds.\\ + \:/+-.-°-:+oss\ + | | \oy\\ + > < +______-| |-__________________________________________________________________ + +PJONRouter has been contributed by Fred Larsen. + +It performs the same routing as the PJONSwitch for locally attached buses, +but supports a static routing table to enable traversing multiple levels of +buses. +_____________________________________________________________________________ + +This software is experimental and it is distributed "AS IS" without any +warranty, use it at your own risk. + +Copyright 2010-2020 by Giovanni Blu Mitolo gioscarab@gmail.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ + +#pragma once + +// Add virtual keyword to PJONSimpleSwitch functions +#define PJON_ROUTER_NEED_INHERITANCE + +#include "PJONSwitch.h" + +#ifndef PJON_ROUTER_TABLE_SIZE +#define PJON_ROUTER_TABLE_SIZE 10 +#endif + +class PJONRouter : public PJONSwitch +{ +protected: + uint8_t remote_bus_ids[PJON_ROUTER_TABLE_SIZE][4]; + uint8_t remote_bus_via_attached_bus[PJON_ROUTER_TABLE_SIZE]; + uint8_t table_size = 0; + + uint8_t find_bus_in_table( + const uint8_t *bus_id, + const uint8_t /* device_id */, + uint8_t &start_bus + ) + { + uint8_t start = start_bus - bus_count; + for(uint8_t i = start; i < table_size; i++) { + if(memcmp(bus_id, remote_bus_ids[i], 4) == 0) { + start_bus = bus_count + i + 1; // Continue searching for matches + return remote_bus_via_attached_bus[i]; // Explicit bus id match + } + } + start_bus = PJON_NOT_ASSIGNED; + return PJON_NOT_ASSIGNED; + }; + + virtual uint8_t find_bus_with_id( + const uint8_t *bus_id, + const uint8_t device_id, + uint8_t &start_bus + ) + { + // Search for a locally attached bus first + uint8_t receiver_bus = PJON_NOT_ASSIGNED; + if(start_bus < bus_count) { + receiver_bus = find_attached_bus_with_id(bus_id, device_id, start_bus); + if(receiver_bus == PJON_NOT_ASSIGNED) { + start_bus = bus_count; // Not found among attached + } + } + // Search in the routing table + if( + (receiver_bus == PJON_NOT_ASSIGNED) && + (start_bus >= bus_count) && + (start_bus != PJON_NOT_ASSIGNED) + ) { + receiver_bus = find_bus_in_table(bus_id, device_id, start_bus); + } + if(receiver_bus == PJON_NOT_ASSIGNED) { + start_bus = PJON_NOT_ASSIGNED; + } + return receiver_bus; + }; + +public: + PJONRouter() {}; + PJONRouter( + uint8_t bus_count, + PJONAny * const buses[], + uint8_t default_gateway = PJON_NOT_ASSIGNED + ) : PJONSwitch(bus_count, buses, default_gateway) { }; + + void add(const uint8_t bus_id[], uint8_t via_attached_bus) + { + if(table_size < PJON_ROUTER_TABLE_SIZE) { + memcpy(remote_bus_ids[table_size], bus_id, 4); + remote_bus_via_attached_bus[table_size] = via_attached_bus; + table_size++; + } + }; +}; + + +// Specialized class to simplify declaration when using 2 buses +template +class PJONRouter2 : public PJONRouter +{ + StrategyLink linkA; + StrategyLink linkB; + PJONAny busA, busB; +public: + PJONRouter2(uint8_t default_gateway = PJON_NOT_ASSIGNED) + { + PJON* buses[2] = { &busA, &busB }; + PJONSimpleSwitch::connect_buses(2, buses, default_gateway); + busA.set_link(&linkA); + busB.set_link(&linkB); + }; + + PJONAny &get_bus(const uint8_t ix) + { + return ix == 0 ? busA : busB; + } + + A &get_strategy_0() + { + return linkA.strategy; + } + B &get_strategy_1() + { + return linkB.strategy; + } +}; + +// Specialized class to simplify declaration when using 3 buses +template +class PJONRouter3 : public PJONRouter +{ + StrategyLink linkA; + StrategyLink linkB; + StrategyLink linkC; + PJONAny busA, busB, busC; +public: + PJONRouter3(uint8_t default_gateway = PJON_NOT_ASSIGNED) + { + PJON *buses[3] = { &busA, &busB, &busC }; + PJONSimpleSwitch::connect_buses(3, buses, default_gateway); + busA.set_link(&linkA); + busB.set_link(&linkB); + busC.set_link(&linkC); + }; + + PJONAny &get_bus(const uint8_t ix) + { + return ix == 0 ? busA : (ix == 1 ? busB : busC); + } + + A &get_strategy_0() + { + return linkA.strategy; + } + B &get_strategy_1() + { + return linkB.strategy; + } + C &get_strategy_2() + { + return linkC.strategy; + } +}; \ No newline at end of file diff --git a/hal/transport/PJON/driver/PJONSimpleSwitch.h b/hal/transport/PJON/driver/PJONSimpleSwitch.h new file mode 100644 index 000000000..edeaa892d --- /dev/null +++ b/hal/transport/PJON/driver/PJONSimpleSwitch.h @@ -0,0 +1,368 @@ + +/*-O//\ __ __ + |-gfo\ |__| | | | |\ | ® + |!y°o:\ | __| |__| | \| 13.0 + |y"s§+`\ multi-master, multi-media bus network protocol + /so+:-..`\ Copyright 2010-2020 by Giovanni Blu Mitolo gioscarab@gmail.com + |+/:ngr-*.`\ + |5/:%&-a3f.:;\ + \+//u/+g%{osv,,\ + \=+&/osw+olds.\\ + \:/+-.-°-:+oss\ + | | \oy\\ + > < +______-| |-__________________________________________________________________ + +PJONSimpleSwitch has been contributed by Fred Larsen. + +It routes packets between buses with different bus ids, and also between +local buses with no bus ids. It is limited to one single strategy in contrast +to its descendant PJONSwitch. + +A default gateway can be specified, identifying one of the attached buses to +receive packets to other target buses than any of the attached buses. It is +possible to use a PJONSimpleSwitch to handle leaf buses in a tree structure. + +NAT (network address translation) support is present, allowing a local bus +to communicate with shared buses. To do this, the local bus must have the +bus id set to a public/NAT bus id with which it can be reached from other +buses, but be set to local mode and not shared mode. +Any incoming packet to the public bus id will be forwarded into the local bus +with the receiver bus id changed to 0.0.0.0 which is considered equivalent +with a local bus address. +Outgoing packets must be sent in shared mode with a sender bus id of 0.0.0.0, +which will be replaced with the NAT address when forwarded by the switch, +enabling receivers on shared buses to reply back to the local bus. +_____________________________________________________________________________ + +This software is experimental and it is distributed "AS IS" without any +warranty, use it at your own risk. + +Copyright 2010-2020 by Giovanni Blu Mitolo gioscarab@gmail.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ + +#pragma once +#include "PJON.h" + +#ifndef PJON_ROUTER_MAX_BUSES +#define PJON_ROUTER_MAX_BUSES 5 +#endif + +template +class PJONSimpleSwitch +{ +protected: + uint8_t bus_count = 0; + uint8_t default_gateway = PJON_NOT_ASSIGNED; + uint8_t current_bus = PJON_NOT_ASSIGNED; + PJON *buses[PJON_ROUTER_MAX_BUSES]; + + void connect( + uint8_t bus_count_in, + PJON * const buses_in[], + uint8_t default_gateway_in, + void *custom_pointer, + PJON_Receiver receiver, + PJON_Error error + ) + { + bus_count = (bus_count_in > PJON_ROUTER_MAX_BUSES) ? + PJON_ROUTER_MAX_BUSES : bus_count_in; + default_gateway = default_gateway_in; + for(uint8_t i = 0; i < bus_count; i++) { + buses[i] = buses_in[i]; + buses[i]->set_receiver(receiver); + buses[i]->set_error(error); + buses[i]->set_custom_pointer(custom_pointer); + buses[i]->set_router(true); + } + }; + + uint8_t find_attached_bus_with_id( + const uint8_t *bus_id, + const uint8_t /*device_id*/, + uint8_t &start_bus + ) + { + for(uint8_t i=start_bus; itx.bus_id, 4) == 0) { + start_bus = i + 1; // Continue searching for more matches after this + return i; // Explicit bus id match + } + } + start_bus = PJON_NOT_ASSIGNED; + return PJON_NOT_ASSIGNED; + }; + +#ifdef PJON_ROUTER_NEED_INHERITANCE + virtual +#endif + void send_packet(const uint8_t *payload, const uint16_t length, + const uint8_t receiver_bus, const uint8_t sender_bus, + bool &ack_sent, const PJON_Packet_Info &packet_info) + { + // Send an ACK once to notify that the packet will be delivered + if( + !ack_sent && + (packet_info.header & PJON_ACK_REQ_BIT) && + (packet_info.rx.id != PJON_BROADCAST) + ) { + buses[sender_bus]->strategy.send_response(PJON_ACK); + ack_sent = true; + } + + /* Set current_bus to receiver bus before potentially calling error + callback for that bus */ + uint8_t send_bus = current_bus; + current_bus = receiver_bus; + + /* NAT support: If a shared packet comes from a local bus destined to a + non-local receiver, then put the NAT address of the bus as the sender + bus id so that replies can find the route back via NAT. */ + PJON_Packet_Info p_info = packet_info; + if ((packet_info.header & PJON_MODE_BIT) && + !(buses[sender_bus]->config & PJON_MODE_BIT) && + memcmp(buses[sender_bus]->tx.bus_id, PJONTools::localhost(), 4)!=0 && + memcmp(packet_info.tx.bus_id, PJONTools::localhost(), 4)==0) { + // Replace sender bus id with public/NAT bus id in the packet + memcpy(&p_info.tx.bus_id, buses[sender_bus]->tx.bus_id, 4); + } + + /* NAT support: If a shared packet comes with receiver bus id matching the + NAT address of a local bus, then change the receiver bus id to 0.0.0.0 + before forwarding the shared packet to the local bus. */ + if ((packet_info.header & PJON_MODE_BIT) && + !(buses[receiver_bus]->config & PJON_MODE_BIT) && + memcmp(buses[receiver_bus]->tx.bus_id, PJONTools::localhost(), 4)!=0 && + memcmp(packet_info.rx.bus_id, buses[receiver_bus]->tx.bus_id, 4)==0) { + // Replace receiver bus id with 0.0.0.0 when sending to local bus + memcpy(p_info.rx.bus_id, PJONTools::localhost(), 4); + } + + uint16_t result = +#if PJON_MAX_PACKETS == 0 + buses[receiver_bus]->forward_blocking( + p_info, + (const uint8_t *)payload, + length + ); + /* Call error function explicitly, because it is not called by + forward_blocking */ + if(result == PJON_FAIL) { + dynamic_error_function(PJON_CONNECTION_LOST, 0); + } +#else + buses[receiver_bus]->forward( + p_info, + (const uint8_t *)payload, + length + ); +#endif + current_bus = send_bus; + } + + void forward_packet( + const uint8_t *payload, + const uint16_t length, + const uint8_t receiver_bus, + const uint8_t sender_bus, + bool &ack_sent, + const PJON_Packet_Info &packet_info + ) + { + // If receiving bus matches and not equal to sending bus, then route packet + if(receiver_bus != PJON_NOT_ASSIGNED && receiver_bus != sender_bus) + send_packet( + payload, + length, + receiver_bus, + sender_bus, + ack_sent, + packet_info + ); + } + +#ifdef PJON_ROUTER_NEED_INHERITANCE + virtual +#endif + uint8_t find_bus_with_id( + const uint8_t bus_id[], + const uint8_t device_id, + uint8_t &start_bus + ) + { + return find_attached_bus_with_id(bus_id, device_id, start_bus); + }; + +#ifdef PJON_ROUTER_NEED_INHERITANCE + virtual +#endif + void dynamic_receiver_function( + uint8_t *payload, + uint16_t length, + const PJON_Packet_Info &packet_info + ) + { + uint8_t start_search = 0; + bool ack_sent = false; // Send ACK once even if delivering to multiple buses + do { + uint8_t receiver_bus = find_bus_with_id((const uint8_t*) + ((packet_info.header & PJON_MODE_BIT) != 0 ? + packet_info.rx.bus_id : PJONTools::localhost()), + packet_info.rx.id, start_search + ); + + /* The NAT case: + A. A shared packet comes in destined to the "public" bus id registered + on the local bus. It will be found normally, and send_packet will + modify the receiver bus id to 0.0.0.0 before sending. + B. A shared packet comes in from the local bus, with sender bus id + 0.0.0.0 and a valid receiver bus id. The receiver bus id will be + found normally, and send_packet will modify the sender bus id + from 0.0.0.0 to the registered public/NAT bus id of the local bus. + */ + + if(receiver_bus == PJON_NOT_ASSIGNED) { + receiver_bus = default_gateway; + } + + forward_packet( + payload, + length, + receiver_bus, + current_bus, + ack_sent, + packet_info + ); + + } while(start_search != PJON_NOT_ASSIGNED); + }; + +#ifdef PJON_ROUTER_NEED_INHERITANCE + virtual +#endif + void dynamic_error_function(uint8_t code, uint16_t data) { } + +public: + + PJONSimpleSwitch() {}; + + PJONSimpleSwitch( + uint8_t bus_count, + PJON * const buses[], + uint8_t default_gateway = PJON_NOT_ASSIGNED + ) + { + connect_buses(bus_count, buses, default_gateway); + }; + + // Specialized constructor to simplify syntax when using 2 buses + PJONSimpleSwitch( + PJON &bus0, + PJON &bus1, + uint8_t default_gateway = PJON_NOT_ASSIGNED + ) + { + PJON *buses[2] = { &bus0, &bus1 }; + connect_buses(2, buses, default_gateway); + }; + + // Specialized constructor to simplify syntax when using 3 buses + PJONSimpleSwitch( + PJON &bus0, + PJON &bus1, + PJON &bus2, + uint8_t default_gateway = PJON_NOT_ASSIGNED + ) + { + PJON *buses[3] = { &bus0, &bus1, &bus2 }; + connect_buses(3, buses, default_gateway); + }; + + void begin() + { + for(uint8_t i = 0; i < bus_count; i++) { + buses[i]->begin(); + } + }; + + void loop() + { + for(current_bus = 0; current_bus < bus_count; current_bus++) { + uint16_t code = + buses[current_bus]->receive( + buses[current_bus]->strategy.get_receive_time() + ); + if(PJON_MAX_PACKETS < bus_count && code == PJON_ACK) { + break; + } + } + for(current_bus = 0; current_bus < bus_count; current_bus++) { + buses[current_bus]->update(); + } + current_bus = PJON_NOT_ASSIGNED; + }; + + void connect_buses( + uint8_t bus_count_in, + PJON * const buses_in[], + uint8_t default_gateway_in = PJON_NOT_ASSIGNED + ) + { + connect( + bus_count_in, + buses_in, + default_gateway_in, + this, + PJONSimpleSwitch::receiver_function, + PJONSimpleSwitch::error_function + ); + }; + + // Return the position of the bus currently calling a callback. + // (It may return PJON_NOT_ASSIGNED if not doing a callback.) + uint8_t get_callback_bus() const + { + return current_bus; + } + + // Return one of the buses, in the same order as sent to the constructor + PJON &get_bus(const uint8_t ix) + { + return *(buses[ix]); + } + + static void receiver_function( + uint8_t *payload, + uint16_t length, + const PJON_Packet_Info &packet_info + ) + { + ( + (PJONSimpleSwitch*)packet_info.custom_pointer + )->dynamic_receiver_function( + payload, + length, + packet_info + ); + } + + static void error_function(uint8_t code, uint16_t data, void *custom_pointer) + { + ((PJONSimpleSwitch*)custom_pointer)->dynamic_error_function( + code, + data + ); + } +}; diff --git a/hal/transport/PJON/driver/PJONSoftwareBitBang.h b/hal/transport/PJON/driver/PJONSoftwareBitBang.h new file mode 100644 index 000000000..3964b7a1c --- /dev/null +++ b/hal/transport/PJON/driver/PJONSoftwareBitBang.h @@ -0,0 +1,7 @@ + +#pragma once + +#include "PJON.h" +#include "strategies/SoftwareBitBang/SoftwareBitBang.h" + +#define PJONSoftwareBitBang PJON diff --git a/hal/transport/PJON/driver/PJONSwitch.h b/hal/transport/PJON/driver/PJONSwitch.h new file mode 100644 index 000000000..94bc30d56 --- /dev/null +++ b/hal/transport/PJON/driver/PJONSwitch.h @@ -0,0 +1,155 @@ + +/*-O//\ __ __ + |-gfo\ |__| | | | |\ | ® + |!y°o:\ | __| |__| | \| 13.0 + |y"s§+`\ multi-master, multi-media bus network protocol + /so+:-..`\ Copyright 2010-2020 by Giovanni Blu Mitolo gioscarab@gmail.com + |+/:ngr-*.`\ + |5/:%&-a3f.:;\ + \+//u/+g%{osv,,\ + \=+&/osw+olds.\\ + \:/+-.-°-:+oss\ + | | \oy\\ + > < +______-| |-__________________________________________________________________ + +PJONSwitch has been contributed by Fred Larsen. + +This class does the same as the PJONSimpleSwitch but supports routing packets +between buses of different strategies using the Any strategy. +_____________________________________________________________________________ + +This software is experimental and it is distributed "AS IS" without any +warranty, use it at your own risk. + +Copyright 2010-2020 by Giovanni Blu Mitolo gioscarab@gmail.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ + +#pragma once + +#include "PJONSimpleSwitch.h" +#include "strategies/Any/Any.h" + +class PJONAny : public PJON +{ +public: + PJONAny( + const uint8_t id = PJON_NOT_ASSIGNED + ) : PJON(id) {}; + + PJONAny( + StrategyLinkBase *link, + const uint8_t id = PJON_NOT_ASSIGNED + ) : PJON(id) + { + strategy.set_link(link); + }; + + PJONAny( + StrategyLinkBase *link, + const uint8_t bus_id[], + const uint8_t id = PJON_NOT_ASSIGNED + ) : PJON( + bus_id, + id + ) + { + strategy.set_link(link); + } + + void set_link(StrategyLinkBase *link) + { + strategy.set_link(link); + } +}; + +class PJONSwitch : public PJONSimpleSwitch +{ +public: + PJONSwitch() : PJONSimpleSwitch() {}; + + PJONSwitch( + uint8_t bus_count, + PJONAny * const bus_list[], + uint8_t default_gateway = PJON_NOT_ASSIGNED + ) : PJONSimpleSwitch(bus_count, (PJON* const *)bus_list, default_gateway) { }; +}; + +// Specialized class to simplify declaration when using 2 buses +template +class PJONSwitch2 : public PJONSwitch +{ + StrategyLink linkA; + StrategyLink linkB; + PJONAny busA, busB; +public: + PJONSwitch2(uint8_t default_gateway = PJON_NOT_ASSIGNED) + { + PJON* buses[2] = { &busA, &busB }; + PJONSimpleSwitch::connect_buses(2, buses, default_gateway); + busA.set_link(&linkA); + busB.set_link(&linkB); + }; + + PJONAny &get_bus(const uint8_t ix) + { + return ix == 0 ? busA : busB; + } + + A &get_strategy_0() + { + return linkA.strategy; + } + B &get_strategy_1() + { + return linkB.strategy; + } +}; + +// Specialized class to simplify declaration when using 3 buses +template +class PJONSwitch3 : public PJONSwitch +{ + StrategyLink linkA; + StrategyLink linkB; + StrategyLink linkC; + PJONAny busA, busB, busC; +public: + PJONSwitch3(uint8_t default_gateway = PJON_NOT_ASSIGNED) + { + PJON *buses[3] = { &busA, &busB, &busC }; + PJONSimpleSwitch::connect_buses(3, buses, default_gateway); + busA.set_link(&linkA); + busB.set_link(&linkB); + busC.set_link(&linkC); + }; + + PJONAny &get_bus(const uint8_t ix) + { + return ix == 0 ? busA : (ix == 1 ? busB : busC); + } + + A &get_strategy_0() + { + return linkA.strategy; + } + B &get_strategy_1() + { + return linkB.strategy; + } + C &get_strategy_2() + { + return linkC.strategy; + } +}; \ No newline at end of file diff --git a/hal/transport/PJON/driver/PJONThroughLora.h b/hal/transport/PJON/driver/PJONThroughLora.h new file mode 100644 index 000000000..b9af20952 --- /dev/null +++ b/hal/transport/PJON/driver/PJONThroughLora.h @@ -0,0 +1,7 @@ + +#pragma once + +#include "PJON.h" +#include "strategies/ThroughLoRa/ThroughLora.h" + +#define PJONThroughLora PJON diff --git a/hal/transport/PJON/driver/PJONThroughSerial.h b/hal/transport/PJON/driver/PJONThroughSerial.h new file mode 100644 index 000000000..66f020dd1 --- /dev/null +++ b/hal/transport/PJON/driver/PJONThroughSerial.h @@ -0,0 +1,7 @@ + +#pragma once + +#include "PJON.h" +#include "strategies/ThroughSerial/ThroughSerial.h" + +#define PJONThroughSerial PJON diff --git a/hal/transport/PJON/driver/PJONVirtualBusRouter.h b/hal/transport/PJON/driver/PJONVirtualBusRouter.h new file mode 100644 index 000000000..822ce43ac --- /dev/null +++ b/hal/transport/PJON/driver/PJONVirtualBusRouter.h @@ -0,0 +1,311 @@ + +/*-O//\ __ __ + |-gfo\ |__| | | | |\ | ® + |!y°o:\ | __| |__| | \| 13.0 + |y"s§+`\ multi-master, multi-media bus network protocol + /so+:-..`\ Copyright 2010-2020 by Giovanni Blu Mitolo gioscarab@gmail.com + |+/:ngr-*.`\ + |5/:%&-a3f.:;\ + \+//u/+g%{osv,,\ + \=+&/osw+olds.\\ + \:/+-.-°-:+oss\ + | | \oy\\ + > < +______-| |-__________________________________________________________________ + +PJONVirtualBusRouter has been contributed by Fred Larsen. + +This class adds functionality to the PJONSwitch, PJONRouter, PJONDynamicRouter +and potential future classes derived from them. This functionality allows a +switch or router to treat multiple attached buses with the same bus id as a +"virtual" bus, where devices can be placed anywhere independent of device id. + +It will start in promiscuous mode, delivering every packet to all attached +buses except the one where the packet comes from. As it learns by looking at +the sender ids of observed packets, it will deliver each packet only to the +attached bus where the receiver device can be found, increasing precision +and reducing traffic. +_____________________________________________________________________________ + +This software is experimental and it is distributed "AS IS" without any +warranty, use it at your own risk. + +Copyright 2010-2020 by Giovanni Blu Mitolo gioscarab@gmail.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ + +#pragma once +#include "PJONDynamicRouter.h" + +// If none of your devices have a device id above a specific number, +// you can save some bytes of RAM by lowering this constant. +// The constant must be higher than the highest device id that +// can be encountered, and will limit the router to this range. +#ifndef PJON_VIRTUALBUS_MAX_DEVICES +#define PJON_VIRTUALBUS_MAX_DEVICES 255 +#endif + +// After having observed a device on one bus it is associated with that bus +// and packets will not be forwarded to other buses. +// This setting can activate a timeout so that if there is no traffic from a +// device for a certain time, its position is cleared and packets will be +// forwared to all buses. This allows a device to be moved from behind one +// router to behind another without restarting routers. The value is in seconds. +#ifndef PJON_VIRTUALBUS_ROUTE_TIMEOUT_S +#define PJON_VIRTUALBUS_ROUTE_TIMEOUT_S 0 +#endif + +template +class PJONVirtualBusRouter : public RouterClass +{ +protected: + // The array position of one (any) of the parts of the virtual bus. + uint8_t virtual_bus = PJON_NOT_ASSIGNED; + uint8_t device_via_attached_bus[PJON_VIRTUALBUS_MAX_DEVICES]; +#if (PJON_VIRTUALBUS_ROUTE_TIMEOUT != 0) + uint32_t device_last_activity_time[PJON_VIRTUALBUS_MAX_DEVICES]; +#endif + + // Support for disabling ACK for devices with unknown location + bool unknown_device_location = false; + + void init_vbus() + { + for (uint8_t i=0; i= PJON_VIRTUALBUS_MAX_DEVICES) { + return PJON_NOT_ASSIGNED; + } +#if (PJON_VIRTUALBUS_ROUTE_TIMEOUT != 0) + // Forget the device location if no packet from it has been observed for some time + if ((uint32_t)(PJON_MILLIS() - device_last_activity_time[device_id]) + > (uint32_t)PJON_VIRTUALBUS_ROUTE_TIMEOUT*1000) { + device_via_attached_bus[device_id] = PJON_NOT_ASSIGNED; + } +#endif + return device_via_attached_bus[device_id]; + } + + bool is_vbus(const uint8_t bus_id[]) + { + return virtual_bus < RouterClass::bus_count && + memcmp(RouterClass::buses[virtual_bus]->tx.bus_id, bus_id, 4)==0; + } + + void register_device_on_vbus(const uint8_t device_id, const uint8_t attached_bus) + { + if (device_id < PJON_VIRTUALBUS_MAX_DEVICES) { +#ifdef DEBUG_PRINT + if (attached_bus != device_via_attached_bus[device_id]) { + Serial.print(F("Device ")); + Serial.print(device_id); + Serial.print(F(" on bus ")); + Serial.println(attached_bus); + } +#endif + device_via_attached_bus[device_id] = attached_bus; +#if (PJON_VIRTUALBUS_ROUTE_TIMEOUT != 0) + device_last_activity_time[device_id] = PJON_MILLIS(); +#endif + } + } + + virtual void send_packet(const uint8_t *payload, const uint16_t length, + const uint8_t receiver_bus, const uint8_t sender_bus, + bool &ack_sent, const PJON_Packet_Info &packet_info) + { + // Override the base class send_packet to disable requesting and sending ACK + // if the receiver's location is not registered. + bool disable_ack = unknown_device_location && is_vbus(RouterClass::buses[receiver_bus]->tx.bus_id); + if (disable_ack) { + PJON_Packet_Info info = packet_info; + info.header &= ~PJON_ACK_REQ_BIT; + + bool disable_ack = true; + RouterClass::send_packet(payload, length, receiver_bus, sender_bus, disable_ack, info); +#ifdef DEBUG_PRINT_PACKETS + Serial.print(F("FORWARD NOACK ")); + Serial.print(info.receiver_id); + Serial.print(F(" to bus ")); + Serial.println(receiver_bus); +#endif + } else { + RouterClass::send_packet(payload, length, receiver_bus, sender_bus, ack_sent, packet_info); +#ifdef DEBUG_PRINT_PACKETS + Serial.print(F("FORWARD ")); + Serial.print(packet_info.rx.id); + Serial.print(F(" to bus ")); + Serial.println(receiver_bus); +#endif + } + } + + void handle_send_error(uint8_t code, uint8_t packet) + { + // Find out which device id does not receive + if (PJON_CONNECTION_LOST == code && + is_vbus(RouterClass::buses[RouterClass::current_bus]->tx.bus_id) && + (packet < PJON_MAX_PACKETS || PJON_MAX_PACKETS == 0)) { + PJON_Packet_Info info; +#if PJON_MAX_PACKETS == 0 + RouterClass::buses[RouterClass::current_bus]-> + parse((RouterClass::buses[RouterClass::current_bus]->data), info); +#else + RouterClass::buses[RouterClass::current_bus]-> + parse((RouterClass::buses[RouterClass::current_bus]->packets[packet].content), info); +#endif + if (info.rx.id < PJON_VIRTUALBUS_MAX_DEVICES && is_vbus(info.rx.bus_id)) { + // Unregister the device if we got an error trying to deliver to the attached + // bus on which it is registered. This will step back from pointed delivery + // to duplication on all parts of the virtual bus for this device. + if (device_via_attached_bus[info.rx.id] == RouterClass::current_bus) { + device_via_attached_bus[info.rx.id] = PJON_NOT_ASSIGNED; +#ifdef DEBUG_PRINT + Serial.print("Unregister "); + Serial.print(info.rx.id); + Serial.print(" from bus "); + Serial.println(RouterClass::current_bus); +#endif + } + } + } + } + + virtual void dynamic_receiver_function(uint8_t *payload, uint16_t length, + const PJON_Packet_Info &packet_info) + { + // First register the sender device if it belongs to a virtual bus. + // If a packet is sent to this device later, it is possible to fall back + // from delivering a copy to each part of the virtual bus, to just + // delivering to the part where the device is actually present. + if (is_vbus(packet_info.tx.bus_id)) { + register_device_on_vbus(packet_info.tx.id, RouterClass::current_bus); + } + + // Search for the device on the virtual bus + uint8_t receiver_bus = find_vbus_with_device(packet_info.rx.bus_id, packet_info.rx.id); + + // If found on part of a virtual bus, do not deliver copies to others + if (receiver_bus != PJON_NOT_ASSIGNED) { + bool ack_sent = false; + if (receiver_bus != RouterClass::current_bus) { + RouterClass::forward_packet(payload, length, receiver_bus, RouterClass::current_bus, ack_sent, + packet_info); + } + } else { + unknown_device_location = true; + RouterClass::dynamic_receiver_function(payload, length, packet_info); + unknown_device_location = false; + } + } + + virtual void dynamic_error_function(uint8_t code, uint16_t data) + { + handle_send_error(code, data); + } + +public: + PJONVirtualBusRouter() : RouterClass() + { + init_vbus(); + } + PJONVirtualBusRouter( + uint8_t bus_count, + PJONAny* const *buses, + uint8_t default_gateway = PJON_NOT_ASSIGNED) + : RouterClass(bus_count, buses, default_gateway) + { + init_vbus(); + } + + /* Support multiple of the attached physical buses to have the same bus id, + / forming a "virtual bus" where devices can be in any part independent of + / device id. Specify the array position of one of the bus parts. */ + void set_virtual_bus(uint8_t first_bus) + { + virtual_bus = first_bus; + } +}; + +// Specialized class to simplify declaration when using 2 buses +template +class PJONVirtualBusRouter2 : public PJONVirtualBusRouter +{ + StrategyLink linkA; + StrategyLink linkB; + PJONAny busA, busB; +public: + PJONVirtualBusRouter2(uint8_t default_gateway = PJON_NOT_ASSIGNED) + { + PJON* buses[2] = { &busA, &busB }; + PJONSimpleSwitch::connect_buses(2, buses, default_gateway); + busA.set_link(&linkA); + busB.set_link(&linkB); + }; + + PJONAny &get_bus(const uint8_t ix) + { + return ix == 0 ? busA : busB; + } + + A &get_strategy_0() + { + return linkA.strategy; + } + B &get_strategy_1() + { + return linkB.strategy; + } +}; + +// Specialized class to simplify declaration when using 3 buses +template +class PJONVirtualBusRouter3 : public PJONVirtualBusRouter +{ + StrategyLink linkA; + StrategyLink linkB; + StrategyLink linkC; + PJONAny busA, busB, busC; +public: + PJONVirtualBusRouter3(uint8_t default_gateway = PJON_NOT_ASSIGNED) + { + PJON* buses[3] = { &busA, &busB, &busC }; + PJONSimpleSwitch::connect_buses(3, buses, default_gateway); + busA.set_link(&linkA); + busB.set_link(&linkB); + busC.set_link(&linkC); + }; + + PJONAny &get_bus(const uint8_t ix) + { + return ix == 0 ? busA : (ix == 1 ? busB : busC); + } + + A &get_strategy_0() + { + return linkA.strategy; + } + B &get_strategy_1() + { + return linkB.strategy; + } + C &get_strategy_2() + { + return linkC.strategy; + } +}; \ No newline at end of file diff --git a/hal/transport/PJON/driver/README.md b/hal/transport/PJON/driver/README.md new file mode 100644 index 000000000..9f5eabd0a --- /dev/null +++ b/hal/transport/PJON/driver/README.md @@ -0,0 +1,54 @@ + +![PJON](http://www.gioblu.com/PJON/PJON-github-header-tiny.png) +## PJON 13.0 +PJON® (Padded Jittering Operative Network) is an arduino-compatible, multi-master, multi-media network protocol. It proposes a new Open Standard, it is designed as a framework and implements a totally software-defined network protocol stack that can be easily cross-compiled on many microcontrollers and real-time operative systems like ATtiny, ATmega, SAMD, ESP8266, ESP32, STM32, Teensy, Raspberry Pi, Zephyr, Linux, Windows x86, Apple and Android. PJON operates on a wide range of media and protocols like PJDL, PJDLR, PJDLS, Serial, RS485, USB, ASK/FSK, LoRa, UDP, TCP, MQTT and ESPNOW. For more information visit the [documentation](documentation/README.md), the [specification](specification/PJON-protocol-specification-v4.0.md) or the [wiki](https://github.com/gioblu/PJON/wiki). + +[![Get PJON bus id](https://img.shields.io/badge/get-PJON%20bus%20id-lightgrey.svg)](http://www.pjon.org/get-bus-id.php) +[![Join the chat at https://gitter.im/gioblu/PJON](https://badges.gitter.im/gioblu/PJON.svg)](https://gitter.im/gioblu/PJON?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + +PJON is used in thousands of devices and its community has spread worldwide because of the following 5 key factors: +- **New technology**: [PJON](specification/PJON-protocol-specification-v4.0.md) is an experimental network protocol stack crafted in 10 years of research and experimentation. It was originally developed as an open-source alternative to i2c and 1-Wire but during development its scope and features have been extended to cover use cases where IP is generally applied. PJON has been engineered to have a variable footprint (4.2-8.2 kB program memory) and overhead (5-35 bytes per packet) depending on its configuration. +- **Multi-media support**: PJON operates upon a wide range of protocols like TCP, UDP, MQTT, ESPNOW, USB, Serial, RS485 and LoRa. The PJON network protocol stack specifies and implements also [PJDL](src/strategies/SoftwareBitBang/specification/PJDL-specification-v5.0.md) that operates over a single wire of up to 2000m shared by up to 255 devices, [PJDLR](src/strategies/OverSampling/specification/PJDLR-specification-v3.0.md) that operates with many ASK/FSK/OOK radio modules, and also [PJDLS](src/strategies/AnalogSampling/specification/PJDLS-specification-v2.0.md) that operates wirelessly with light pulses using off-the-shelf LEDs and laser diodes. +- **Increased reliability**: Many protocols massively applied worldwide expose dangerous vulnerabilities, have weak error detection algorithms and are not resilient to interference. PJON is based on years of analysis and study not to make the same mistakes present in most alternatives and provide with a simpler and more efficient solution. +- **High flexibility**: PJON is totally software-defined and it is designed to be easily extensible. it builds out-of-the-box in all supported devices and operates transparently on top of any supported protocol or medium. +- **Low cost**: Without any additional hardware needed to operate, minimal network wiring requirements and direct pin-to-pin or LED-to-LED communication, PJON is extremely energy efficient, cheap to be implemented and maintained. This implementation is kept updated and meticulously tested thanks to the strong commitment of its growing community of end users, testers and developers. + +### Features +- Cross-compilation support with the [interfaces](src/interfaces) system calls abstraction +- Multi-media support with the [strategies](src/strategies) data link layer abstraction +- Modular packet format that includes only the field used (overhead 5-35 bytes) +- Hot-swap support, no need of system reset or shut down when replacing or adding devices +- Flexible local (device id) and shared (bus id) network identification +- Safe error detection done with modern CRC8 and CRC32 polynomials +- Optional inclusion of MAC addresses +- Optional acknowledgement +- Error handling + +### Publications +- [PJON protocol handbook](https://www.pjon-technologies.com/collections/frontpage/products/pjon-protocol-hand-book) by Giovanni Blu Mitolo - Distributed by [PJON Technologies srl](https://www.pjon-technologies.com) +- [PJON 12.0 big box](https://www.pjon-technologies.com/collections/frontpage/products/pjon-protocol-12-0-big-box) by Giovanni Blu Mitolo - Distributed by [PJON Technologies srl](https://www.pjon-technologies.com) + +### Academic studies +Researchers are active in many universities worldwide using PJON in different environments. The following list contains all the known published academic studies about PJON: +- [Definition and Application of PJON-PLC for sensor networks](https://repositorio.unican.es/xmlui/bitstream/handle/10902/14012/408952.pdf?sequence=1) by Jorge Gómez Segurola, Ingeniería de Tecnologías de +Telecomunicación - [Universidad de Cantabria](https://web.unican.es/) (ES) +- [Biomimetic electronics](http://c.harl.ie/biomimetic.html) by Charlie Williams with scientific input from researchers Vítor Martins dos Santos, Diana Machado de Sousa and Sabine Vreeburg - Artist in Residency at [Wageningen University](https://www.wur.nl/en.htm) (NL) +- [LANC Video Camera Control](http://jda.tel/pdf/lanc_video_camera_control.pdf) by [Jack Anderson](https://github.com/jdaandersj) - Department of Computer Science [Loughborough University](https://www.lboro.ac.uk/departments/compsci/) (UK) + +### Contribute +Feel free to send a pull request sharing something you have made that could help, if you want to support this project you can also try to solve an [issue](https://github.com/gioblu/PJON/issues). Thanks to support, expertise, kindness and talent of the following contributors, the protocol's documentation, specification and implementation have been strongly tested, enhanced and verified: + +[Fred Larsen](https://github.com/fredilarsen), [Zbigniew Zasieczny](https://github.com/girgitt), [Matheus Garbelini](https://github.com/Matheus-Garbelini), [sticilface](https://github.com/sticilface), [Felix Barbalet](https://github.com/xlfe), [Oleh Halitskiy](https://github.com/Halytskyi), [fotosettore](https://github.com/fotosettore), [fabpolli](https://github.com/fabpolli), [Adrian Sławiński](https://github.com/4ib3r), [Osman Selçuk Aktepe](https://github.com/osman-aktepe), [Jorgen-VikingGod](https://github.com/Jorgen-VikingGod), [drtrigon](https://github.com/drtrigon), [Endre Karlson](https://github.com/ekarlso), [Wilfried Klaas](https://github.com/willie68), [budaics](https://github.com/budaics), [ibantxo](https://github.com/ibantxo), [gonnavis](https://github.com/gonnavis), [maxidroms83](https://github.com/maxidroms83), [Evgeny Dontsov](https://github.com/dontsovcmc), [zcattacz](https://github.com/zcattacz), [Valerii Koval](https://github.com/valeros), [Ivan Kravets](https://github.com/ivankravets), [Esben Soeltoft](https://github.com/EsbenSoeltoft), [Alex Grishin](https://github.com/240974a), [Andrew Grande](https://github.com/aperepel), [Michael Teeww](https://github.com/MichMich), [Paolo Paolucci](https://github.com/PaoloP74), [per1234](https://github.com/per1234), [Santiago Castro](https://github.com/bryant1410), [pacproduct](https://github.com/pacproduct), [elusive-code](https://github.com/elusive-code), [Emanuele Iannone](https://github.com/eiannone), [Christian Pointner](https://github.com/equinox0815), [Fabian Gärtner](https://github.com/TeeTrizZz), [Mauro Mombelli](https://github.com/MauroMombelli), [Remo Kallio](https://github.com/shacal), [hyndruide](https://github.com/hyndruide), [sigmaeo](https://github.com/sigmaeo), [filogranaf](https://github.com/filogranaf), [Maximiliano Duarte](https://github.com/domonetic), [Viktor Szépe](https://github.com/szepeviktor), [Shachar Limor](), [Pantovich](), [Mauro Zancarlin](), [Franketto](), [jzobac](), [DanRoad](), [fcastrovilli](https://github.com/fcastrovilli), [Andrei Volkau](https://github.com/andrei-volkau), [maniekq](https://github.com/maniekq), [DetAtHome](https://github.com/DetAtHome), [Michael Branson](https://github.com/mxbranson), [chestwood96](https://github.com/chestwood96), [Mattze96](https://github.com/Mattze96), [Steven Bense](https://github.com/justoke), [Jack Anderson](https://github.com/jdaandersj), [callalilychen](https://github.com/callalilychen), [Julio Aguirre](https://github.com/jcallano), [Cimex97](https://github.com/Cimex97), [der-schne](https://github.com/der-schne) and [porkyneal](https://github.com/porkyneal). + +### Compliant tools +- [ModuleInterface](https://github.com/fredilarsen/ModuleInterface) - easy config and value sync between IoT modules by Fred Larsen +- [PJON-cython](https://github.com/xlfe/PJON-cython) - cython PJON wrapper by xlfe github user +- [PJON-piper](https://github.com/Girgitt/PJON-piper) - command line wrapper by Zbigniew Zasieczny +- [PJON-python](https://github.com/Girgitt/PJON-python) - python interface by Zbigniew Zasieczny +- [PJON-gRPC](https://github.com/Halytskyi/PJON-gRPC) - gRPC server-client by Oleh Halytskyi + +### License +All the software included in this project is experimental and it is distributed "AS IS" without any warranty, use it at your own risk. [Licensed](https://github.com/gioblu/PJON/blob/master/LICENSE.md) under the Apache License, Version 2.0. PJON® and its brand are registered trademarks, property of Giovanni Blu Mitolo gioscarab@gmail.com + +### Safety warning +When installing or maintaining a PJON network, extreme care must be taken to avoid any danger. If devices are connected to AC power you are exposed to a high chance of being electrocuted if hardware is not installed carefully and properly. If you are not experienced enough ask the support of a skilled technician and consider that many countries prohibit uncertified installations. When a [SoftwareBitBang](/src/strategies/SoftwareBitBang) bus is installed [interference mitigation](https://github.com/gioblu/PJON/wiki/Mitigate-interference) and [protective circuitry](https://github.com/gioblu/PJON/wiki/Protective-circuitry) guidelines must be followed. When working with an [AnalogSampling](/src/strategies/AnalogSampling) LED or laser based setup safety glasses must be worn and transceivers must be operated cautiously to avoid potential eye injuries. Before any practical test or a hardware purchase for a wireless [OverSampling](/src/strategies/OverSampling), [ThroughSerial](/src/strategies/ThroughSerial) or [ThroughLoRa](/src/strategies/ThroughLoRa) radio setup, compliance with government requirements and regulations must be ensured. When connecting a local bus to the internet all devices must be considered potentially compromised, manipulated or remotely actuated against your will. It should be considered a good practice not to connect to the internet systems that may create a damage (fire, flood, data-leak) if hacked. diff --git a/hal/transport/PJON/driver/SECURITY.md b/hal/transport/PJON/driver/SECURITY.md new file mode 100644 index 000000000..9054f1c04 --- /dev/null +++ b/hal/transport/PJON/driver/SECURITY.md @@ -0,0 +1,10 @@ +## Security Policy +PJON is still in experimental phase and it distributed "AS IS" without any warranty, although a lot of work is iteratively done to make it more secure and reliable. Its implementation and specification are publicly available and are constantly reviewed worldwide by the community, the developers team and more recently by academics. + +The security of a system that uses PJON for communication mostly depends on the vulnerabilities exposed by the hardware and by the physical layer used. Consider that **only air-gapped wired local buses are undoubtely secure**. When connecting a local bus to the internet using [ESPNOW](/src/strategies/ESPNOW), [EthernetTCP](/src/strategies/EthernetTCP) or [LocalUDP](/src/strategies/LocalUDP), [GlobalUDP](/src/strategies/GlobalUDP) or [DualUDP](/src/strategies/DualUDP), all connected devices must be considered potentially compromised. It should be considered a good practice not to connect to the internet systems that may cause damage (fire, flood, data-leak) if hacked. + +### Safety warning +When installing or maintaining a PJON network, extreme care must be taken to avoid any danger. If devices are connected to AC power you are exposed to a high chance of being electrocuted if hardware is not installed carefully and properly. If you are not experienced enough ask the support of a skilled technician and consider that many countries prohibit uncertified installations. When a [SoftwareBitBang](/src/strategies/SoftwareBitBang) bus is installed [interference mitigation](https://github.com/gioblu/PJON/wiki/Mitigate-interference) and [protective circuitry](https://github.com/gioblu/PJON/wiki/Protective-circuitry) guidelines must be followed. When working with an [AnalogSampling](/src/strategies/AnalogSampling) LED or laser based setup safety glasses must be worn and transceivers must be operated cautiously to avoid potential eye injuries. Before any practical test or a hardware purchase for a wireless [OverSampling](/src/strategies/OverSampling), [ThroughSerial](/src/strategies/ThroughSerial) or [ThroughLoRa](/src/strategies/ThroughLoRa) radio setup, compliance with government requirements and regulations must be ensured. + +### Reporting a Vulnerability +If you discover a vulnerability in the specification or in the implementation please report it as soon as possible opening an [issue](https://github.com/gioblu/PJON/issues). If you have developed a fix, feel free to open a [pull request](https://github.com/gioblu/PJON/pulls). diff --git a/hal/transport/PJON/driver/interfaces/ARDUINO/ESPNOWHelper.h b/hal/transport/PJON/driver/interfaces/ARDUINO/ESPNOWHelper.h new file mode 100644 index 000000000..9198736d2 --- /dev/null +++ b/hal/transport/PJON/driver/interfaces/ARDUINO/ESPNOWHelper.h @@ -0,0 +1,268 @@ +#pragma once + +#ifndef ESP32 +#error "ESP32 constant is not defined." +#endif + +// ESP includes +#include +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "freertos/timers.h" +#include "nvs_flash.h" +#include "esp_event_loop.h" +#include "tcpip_adapter.h" +#include "esp_wifi.h" +#include "esp_log.h" +#include "esp_system.h" +#include "esp_now.h" +#include "rom/ets_sys.h" +#include "rom/crc.h" +#include "esp_wifi_types.h" + +static const char *TAG = "espnow"; + +/* ESPNOW can work in both station and softap mode. + It is configured in menuconfig. */ +#if CONFIG_STATION_MODE +#define ESPNOW_WIFI_MODE WIFI_MODE_STA +#define ESPNOW_WIFI_IF ESP_IF_WIFI_STA +#else +#define ESPNOW_WIFI_MODE WIFI_MODE_AP +#define ESPNOW_WIFI_IF ESP_IF_WIFI_AP +#endif + +#define ESPNOW_MAX_PACKET 250 +#define ESPNOW_QUEUE_SIZE 6 + +static uint8_t espnow_broadcast_mac[ESP_NOW_ETH_ALEN] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +#define IS_BROADCAST_ADDR(addr) (memcmp(addr, espnow_broadcast_mac, ESP_NOW_ETH_ALEN) == 0) + +typedef struct { + uint8_t mac_addr[ESP_NOW_ETH_ALEN]; + esp_now_send_status_t status; +} espnow_event_send_cb_t; + +typedef struct { + uint8_t mac_addr[ESP_NOW_ETH_ALEN]; + uint8_t *data; + int data_len; +} espnow_packet_t; + +enum { + ESPNOW_DATA_BROADCAST, + ESPNOW_DATA_UNICAST, + ESPNOW_DATA_MAX, +}; + +static uint8_t last_mac[ESP_NOW_ETH_ALEN]; +static TaskHandle_t pjon_task_h = NULL; +static xQueueHandle espnow_recv_queue = NULL; + +static void espnow_send_cb(const uint8_t *mac_addr, esp_now_send_status_t status) +{ + // The only thing we do in the send callback is unblock the + // other thread which blocks after posting data to the MAC + xTaskNotifyGive(pjon_task_h); + if(mac_addr == NULL) { + ESP_LOGE(TAG, "Send cb arg error"); + } + return; +}; + +static void espnow_recv_cb(const uint8_t *mac_addr, const uint8_t *data, int len) +{ + espnow_packet_t packet; + if(mac_addr == NULL || data == NULL || len <= 0) { + ESP_LOGE(TAG, "Receive cb arg error"); + return; + } + memcpy(packet.mac_addr, mac_addr, ESP_NOW_ETH_ALEN); + packet.data = (uint8_t *)malloc(len); + if(packet.data == NULL) { + ESP_LOGE(TAG, "Malloc receive data fail"); + return; + } + memcpy(packet.data, data, len); + packet.data_len = len; + // Post to the queue, but don't wait + if(xQueueSend(espnow_recv_queue, &packet, 0) != pdTRUE) { + ESP_LOGW(TAG, "Send receive queue fail"); + free(packet.data); + } +}; + +class ENHelper +{ + uint8_t _magic_header[4]; + uint8_t _channel = 14; + uint8_t _esp_pmk[16]; + +public: + void add_node_mac(uint8_t mac_addr[ESP_NOW_ETH_ALEN]) + { + ESP_ERROR_CHECK(add_peer(mac_addr)); + }; + + esp_err_t add_peer(uint8_t mac_addr[ESP_NOW_ETH_ALEN]) + { + if(esp_now_is_peer_exist(mac_addr)) { + return ESP_OK; + } + /* Add broadcast peer information to peer list. */ + esp_now_peer_info_t *peer = + (esp_now_peer_info_t *)malloc(sizeof(esp_now_peer_info_t)); + if(peer == NULL) { + ESP_LOGE(TAG, "Malloc peer information fail"); + vSemaphoreDelete(espnow_recv_queue); + esp_now_deinit(); + return ESP_FAIL; + } + memset(peer, 0, sizeof(esp_now_peer_info_t)); + peer->channel = _channel; + peer->ifidx = ESPNOW_WIFI_IF; + if(IS_BROADCAST_ADDR(mac_addr)) { + peer->encrypt = false; + } + // else { + // peer->encrypt = true; + // memcpy(peer->lmk, _esp_pmk, 16); + // } + memcpy(peer->peer_addr, mac_addr, ESP_NOW_ETH_ALEN); + ESP_ERROR_CHECK(esp_now_add_peer(peer)); + free(peer); + return ESP_OK; + }; + + bool begin(uint8_t channel, uint8_t *espnow_pmk) + { + esp_err_t ret = nvs_flash_init(); + if( + ret == ESP_ERR_NVS_NO_FREE_PAGES // || + // ret == ESP_ERR_NVS_NEW_VERSION_FOUND + // error: ESP_ERR_NVS_NEW_VERSION_FOUND was not declared in this scope + ) { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + pjon_task_h = xTaskGetCurrentTaskHandle(); + _channel = channel; + memcpy(_esp_pmk, espnow_pmk, 16); + if(espnow_recv_queue != NULL) { + return ESP_FAIL; + } + espnow_recv_queue = + xQueueCreate(ESPNOW_QUEUE_SIZE, sizeof(espnow_packet_t)); + if(espnow_recv_queue == NULL) { + ESP_LOGE(TAG, "Create mutex fail"); + return ESP_FAIL; + } + tcpip_adapter_init(); + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + ESP_ERROR_CHECK(esp_wifi_set_country(&wifi_country)); + ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM)); + ESP_ERROR_CHECK(esp_wifi_set_mode(ESPNOW_WIFI_MODE)); + /* These two steps are required BEFORE the channel can be set + As per the documentation from Espressif: + docs.espressif.com/projects/esp-idf/en/latest/api-reference/network/ + esp_wifi.html#_CPPv420esp_wifi_set_channel7uint8_t18wifi_second_chan_t */ + ESP_ERROR_CHECK(esp_wifi_start()); + ESP_ERROR_CHECK(esp_wifi_set_promiscuous(true)); + ESP_ERROR_CHECK(esp_wifi_set_channel(_channel, WIFI_SECOND_CHAN_NONE)); + // Initialize ESPNOW and register sending & receiving callback function + ESP_ERROR_CHECK(esp_now_init()); + ESP_ERROR_CHECK(esp_now_register_send_cb(espnow_send_cb)); + ESP_ERROR_CHECK(esp_now_register_recv_cb(espnow_recv_cb)); + // Set primary master key + ESP_ERROR_CHECK(esp_now_set_pmk(_esp_pmk)); + // Add broadcast peer information to peer list + add_peer(espnow_broadcast_mac); + return true; + }; + + uint16_t receive_frame(uint8_t *data, uint16_t max_length) + { + // see if there's any received data waiting + espnow_packet_t packet; + if(xQueueReceive(espnow_recv_queue, &packet, 0) == pdTRUE) { + if(packet.data_len >= 4) { + uint8_t len = packet.data_len - 4; + if( + (packet.data[0] ^ len) != _magic_header[0] || + (packet.data[1] ^ len) != _magic_header[1] || + (packet.data[packet.data_len - 2] ^ len) != _magic_header[2] || + (packet.data[packet.data_len - 1] ^ len) != _magic_header[3] + ) { + ESP_LOGE(TAG, "magic mismatch"); + free(packet.data); + return PJON_FAIL; + } + if(len > max_length) { + free(packet.data); + ESP_LOGE(TAG, "buffer overflow - %d bytes but max is %d", len, max_length); + return PJON_FAIL; + } + memcpy(data, packet.data + 2, len); + free(packet.data); + // Update last mac received from + memcpy(last_mac, packet.mac_addr, ESP_NOW_ETH_ALEN); + return len; + } else { + ESP_LOGE(TAG, "packet < 4 received"); + free(packet.data); + return PJON_FAIL; // No data waiting + } + } + return PJON_FAIL; + }; + + void send_frame( + uint8_t *data, + uint16_t length, + uint8_t dest_mac[ESP_NOW_ETH_ALEN] + ) + { + uint8_t packet[ESPNOW_MAX_PACKET]; + if(length + 4 > ESPNOW_MAX_PACKET) { + ESP_LOGE(TAG, "Packet send error - too long :%d", length + 4); + return; + } + uint8_t len = length; + packet[0] = _magic_header[0] ^ len; + packet[1] = _magic_header[1] ^ len; + memcpy(packet + 2, data, len); + packet[len + 2] = _magic_header[2] ^ len; + packet[len + 3] = _magic_header[3] ^ len; + if(esp_now_send(dest_mac, packet, len + 4) != ESP_OK) { + ESP_LOGE(TAG, "Send error"); + } else { // Wait for notification that the data has been received by the MAC + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + } + }; + + void send_response(uint8_t response) + { + send_frame(&response, 1, last_mac); + }; + + void send_frame(uint8_t *data, uint16_t length) + { + // Broadcast + send_frame(data, length, espnow_broadcast_mac); + }; + + void set_magic_header(uint8_t *magic_header) + { + memcpy(_magic_header, magic_header, 4); + }; + + void get_sender(uint8_t *ip) + { + memcpy(ip, last_mac, ESP_NOW_ETH_ALEN); + }; +}; diff --git a/hal/transport/PJON/driver/interfaces/ARDUINO/PJON_ARDUINO_Interface.h b/hal/transport/PJON/driver/interfaces/ARDUINO/PJON_ARDUINO_Interface.h new file mode 100644 index 000000000..7db03da65 --- /dev/null +++ b/hal/transport/PJON/driver/interfaces/ARDUINO/PJON_ARDUINO_Interface.h @@ -0,0 +1,177 @@ + +/* PJON Arduino Interface + ___________________________________________________________________________ + + Copyright 2018 Giovanni Blu Mitolo gioscarab@gmail.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + +#pragma once + +#if defined(ARDUINO) +#ifdef ARDUINO_ESP8266_WEMOS_D1MINI +#define PJON_ESP // WeMos mini and D1 R2 +#elif ARDUINO_ESP8266_ESP01 +#define PJON_ESP // Generic ESP's use for 01's +#elif ARDUINO_ESP8266_NODEMCU +#define PJON_ESP // Wio Link and NodeMCU 1.0 (also 0.9), use for ESP12 +#elif ESP8266 +#define PJON_ESP +#elif defined(ESP32) +#define PJON_ESP +#endif + +#include +#include "PJON_IO.h" + +#ifdef __STM32F1__ +#ifndef A0 +#define A0 0 +#endif +#endif + +/* Generic constants ---------------------------------------------------- */ + +#ifndef LED_BUILTIN +#define LED_BUILTIN -1 +#endif + +/* Arduino IO system calls ---------------------------------------------- */ + +#ifndef PJON_ANALOG_READ +#define PJON_ANALOG_READ analogRead +#endif + +#ifndef PJON_IO_WRITE +#define PJON_IO_WRITE digitalWrite +#endif + +#ifndef PJON_IO_READ +#define PJON_IO_READ digitalRead +#endif + +#ifndef PJON_IO_MODE +#define PJON_IO_MODE pinMode +#endif + +#ifndef PJON_IO_PULL_DOWN +#define PJON_IO_PULL_DOWN(P) \ + do { \ + digitalWrite(P, LOW); \ + pinMode(P, INPUT); \ + } while(0) +#endif + +/* Random --------------------------------------------------------------- */ + +#ifdef __STM32F1__ +#ifndef PJON_RANDOM +#define PJON_RANDOM(R) random(R ? R : R + 1) +#endif +#else +#ifndef PJON_RANDOM +#define PJON_RANDOM random +#endif +#endif + +#ifndef PJON_RANDOM_SEED +#define PJON_RANDOM_SEED randomSeed +#endif + +/* Serial --------------------------------------------------------------- */ + +#ifndef PJON_SERIAL_TYPE +#define PJON_SERIAL_TYPE Stream * +#endif + +#ifndef PJON_SERIAL_AVAILABLE +#define PJON_SERIAL_AVAILABLE(S) S->available() +#endif + +#ifndef PJON_SERIAL_WRITE +#define PJON_SERIAL_WRITE(S, C) S->write(C) +#endif + +#ifndef PJON_SERIAL_READ +#define PJON_SERIAL_READ(S) S->read() +#endif + +#ifndef PJON_SERIAL_FLUSH +#define PJON_SERIAL_FLUSH(S) S->flush() +#endif + +/* Timing --------------------------------------------------------------- */ + +#ifndef PJON_DELAY +#define PJON_DELAY delay +#endif + +#ifndef PJON_DELAY_MICROSECONDS +#define PJON_DELAY_MICROSECONDS delayMicroseconds +#endif + +#ifndef PJON_MICROS +#define PJON_MICROS micros +#endif + +#ifndef PJON_MILLIS +#define PJON_MILLIS millis +#endif + +/* Set ADC prescale functions ------------------------------------------- */ + +#ifndef cbi +#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit)) +#endif + +#ifndef sbi +#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit)) +#endif + +/* Sockets -------------------------------------------------------------- */ + +#define HAS_ETHERNETUDP + +/* Byte order translation functions ------------------------------------- */ + +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ && !defined(PJON_ESP) + +#ifndef htons // Host to network short +#define htons(x) ( ((uint16_t)(x) << 8 & 0xFF00) | \ + ((uint16_t)(x) >> 8 & 0x00FF) ) +#endif + +#ifndef ntohs // Network to host short +#define ntohs(x) htons(x) +#endif + +#ifndef htonl // Host to network long +#define htonl(x) ( ((uint32_t)(x) << 24 & 0xFF000000UL) | \ + ((uint32_t)(x) << 8 & 0x00FF0000UL) | \ + ((uint32_t)(x) >> 8 & 0x0000FF00UL) | \ + ((uint32_t)(x) >> 24 & 0x000000FFUL) ) +#endif + +#ifndef ntohl // Network to host long +#define ntohl(x) htonl(x) +#endif +#endif + +#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ && !defined(PJON_ESP) +#define htons(x) (x) +#define htonl(x) (x) +#define ntohs(x) (x) +#define ntohl(x) (x) +#endif + +#endif diff --git a/hal/transport/PJON/driver/interfaces/ARDUINO/PJON_IO.h b/hal/transport/PJON/driver/interfaces/ARDUINO/PJON_IO.h new file mode 100644 index 000000000..3255f2d6e --- /dev/null +++ b/hal/transport/PJON/driver/interfaces/ARDUINO/PJON_IO.h @@ -0,0 +1,319 @@ + +/* Faster digitalWrite, digitalRead, pinMode for Arduino + https://codebender.cc/library/digitalWriteFast#bonus%2FdigitalWrite%2Fdigital_write_macros.h + - Basic scheme was developed by Paul Stoffregen with digitalWrite. + - Extended to pinMode by John Raines + - Extended to digitalRead by John Raines with considerable assistance + by William Westfield + Copyright (c) 2008-2010 PJRC.COM, LLC + + List of supported MCUs: + - ATmega8/88/168/328/1280/1284P/2560 (Duemilanove, Uno, Nano, Mini, Pro, Mega) + - ATmega16U4/32U4 (Leonardo, Micro) + - ATtiny44/84/44A/84A Added by Wilfried Klaas + - ATtiny45/85 (Trinket, Digispark) + - SAMD21G18A (Arduino Zero) Added by Esben Soeltoft 03/09/2016 + +Renamed since v7.0 to avoid naming collisions and so subtle bugs and anomalies +if used along with third-party software using older/different versions of +digitalWriteFast. All methods are defined in uppercase style to implicitly +inform the reader of their definition as macros in the global scope. + ______________________________________________________________________________ + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. */ + +#pragma once + +/* AVR ATmega1280/2560 - Arduino Mega ------------------------------------- */ + +#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) +#define PJON_IO_PIN_TO_PORT_REG(P) \ + ((P >= 22 && P <= 29) ? &PORTA : \ + (((P >= 10 && P <= 13) || (P >= 50 && P <= 53)) ? &PORTB : \ + ((P >= 30 && P <= 37) ? &PORTC : \ + (((P >= 18 && P <= 21) || P == 38) ? &PORTD : \ + (((P <= 3) || P == 5) ? &PORTE : \ + ((P >= 54 && P <= 61) ? &PORTF : \ + (((P >= 39 && P <= 41) || P == 4) ? &PORTG : \ + (((P >= 6 && P <= 9) || P == 16 || P == 17) ? &PORTH : \ + ((P == 14 || P == 15) ? &PORTJ : \ + ((P >= 62 && P <= 69) ? &PORTK : &PORTL)))))))))) + +#define PJON_IO_PIN_TO_DDR_REG(P) \ + ((P >= 22 && P <= 29) ? &DDRA : \ + (((P >= 10 && P <= 13) || (P >= 50 && P <= 53)) ? &DDRB : \ + ((P >= 30 && P <= 37) ? &DDRC : \ + (((P >= 18 && P <= 21) || P == 38) ? &DDRD : \ + (((P <= 3) || P == 5) ? &DDRE : \ + ((P >= 54 && P <= 61) ? &DDRF : \ + (((P >= 39 && P <= 41) || P == 4) ? &DDRG : \ + (((P >= 6 && P <= 9) || P == 16 || P == 17) ? &DDRH : \ + ((P == 14 || P == 15) ? &DDRJ : \ + ((P >= 62 && P <= 69) ? &DDRK : &DDRL)))))))))) + +#define PJON_IO_PIN_TO_PIN_REG(P) \ + ((P >= 22 && P <= 29) ? &PINA : \ + (((P >= 10 && P <= 13) || (P >= 50 && P <= 53)) ? &PINB : \ + ((P >= 30 && P <= 37) ? &PINC : \ + (((P >= 18 && P <= 21) || P == 38) ? &PIND : \ + (((P <= 3) || P == 5) ? &PINE : \ + ((P >= 54 && P <= 61) ? &PINF : \ + (((P >= 39 && P <= 41) || P == 4) ? &PING : \ + (((P >= 6 && P <= 9) || P == 16 || P == 17) ? &PINH : \ + ((P == 14 || P == 15) ? &PINJ : \ + ((P >= 62 && P <= 69) ? &PINK : &PINL)))))))))) + +#ifndef PJON_IO_PIN_TO_BIT +#define PJON_IO_PIN_TO_BIT(P) \ + ((P >= 7 && P <= 9) ? P - 3 : ((P >= 10 && P <= 13) ? P - 6 : \ + ((P >= 22 && P <= 29) ? P - 22 : ((P >= 30 && P <= 37) ? 37 - P : \ + ((P >= 39 && P <= 41) ? 41 - P : ((P >= 42 && P <= 49) ? 49 - P : \ + ((P >= 50 && P <= 53) ? 53 - P : ((P >= 54 && P <= 61) ? P - 54 : \ + ((P >= 62 && P <= 69) ? P - 62 : ((P == 0 || P == 15 || P == 17 || P == 21) \ + ? 0 : ((P == 1 || P == 14 || P == 16 || P == 20) ? 1 : ((P == 19) ? 2 : \ + ((P == 5 || P == 6 || P == 18) ? 3 : ((P == 2) ? 4 : \ + ((P == 3 || P == 4) ? 5 : 7))))))))))))))) +#endif +#endif + +/* AVR ATmega88/168/328/328P - Arduino Duemilanove, Uno, Nano, Mini, Pro -- */ + +#if defined(__AVR_ATmega88__) || defined(__AVR_ATmega168__) || \ + defined(__AVR_ATmega168P__)|| \ + defined(__AVR_ATmega328__) || defined(__AVR_ATmega328P__) || \ + defined(__AVR_ATmega328PB__) +#define PJON_IO_PIN_TO_PORT_REG(P) \ + ((P <= 7) ? &PORTD : ((P >= 8 && P <= 13) ? &PORTB : &PORTC)) +#define PJON_IO_PIN_TO_DDR_REG(P) \ + ((P <= 7) ? &DDRD : ((P >= 8 && P <= 13) ? &DDRB : &DDRC)) +#define PJON_IO_PIN_TO_PIN_REG(P) \ + ((P <= 7) ? &PIND : ((P >= 8 && P <= 13) ? &PINB : &PINC)) +#ifndef PJON_IO_PIN_TO_BIT +#define PJON_IO_PIN_TO_BIT(P) \ + ((P <= 7) ? P : ((P >= 8 && P <= 13) ? P - 8 : P - 14)) +#endif +#endif + +/* AVR ATmega16U4/32U4 - Arduino Leonardo, Micro -------------------------- */ + +#if defined(__AVR_ATmega16U4__) || defined(__AVR_ATmega32U4__) +#define PJON_IO_PIN_TO_PORT_REG(P) \ + (((P <= 4) || P == 6 || P == 12 || P == 24 || P == 25 || P == 29) \ + ? &PORTD : ((P == 5 || P == 13) ? &PORTC : ((P >= 18 && P <= 23)) ? &PORTF : \ + ((P == 7) ? &PORTE : &PORTB))) +#define PJON_IO_PIN_TO_DDR_REG(P) \ + (((P <= 4) || P == 6 || P == 12 || P == 24 || P == 25 || P == 29) \ + ? &DDRD : ((P == 5 || P == 13) ? &DDRC : ((P >= 18 && P <= 23)) ? \ + &DDRF : ((P == 7) ? &DDRE : &DDRB))) +#define PJON_IO_PIN_TO_PIN_REG(P) \ + (((P <= 4) || P == 6 || P == 12 || P == 24 || P == 25 || P == 29) \ + ? &PIND : ((P == 5 || P == 13) ? &PINC : ((P >= 18 && P <= 23)) ? \ + &PINF : ((P == 7) ? &PINE : &PINB))) +#ifndef PJON_IO_PIN_TO_BIT +#define PJON_IO_PIN_TO_BIT(P) \ + ((P >= 8 && P <= 11) ? P - 4 : ((P >= 18 && P <= 21) ? 25 - P : \ + ((P == 0) ? 2 : ((P == 1) ? 3 : ((P == 2) ? 1 : ((P == 3) ? 0 : \ + ((P == 4) ? 4 : ((P == 6) ? 7 : ((P == 13) ? 7 : ((P == 14) ? 3 : \ + ((P == 15) ? 1 : ((P == 16) ? 2 : ((P == 17) ? 0 : ((P == 22) ? 1 : \ + ((P == 23) ? 0 : ((P == 24) ? 4 : ((P == 25) ? 7 : ((P == 26) ? 4 : \ + ((P == 27) ? 5 : 6 ))))))))))))))))))) +#endif +#endif + +/* AVR ATtiny45/85 - Trinket, Digispark ----------------------------------- */ + +#if defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__) +#define PJON_IO_PIN_TO_PORT_REG(P) &PORTB +#define PJON_IO_PIN_TO_DDR_REG(P) &DDRB +#define PJON_IO_PIN_TO_PIN_REG(P) &PINB +#ifndef PJON_IO_PIN_TO_BIT +#define PJON_IO_PIN_TO_BIT(P) P +#endif +#endif + +/* AVR ATMEGA1284P -------------------------------------------------------- */ + +#if defined(__AVR_ATmega1284P__) +#define PJON_IO_PIN_TO_PORT_REG(P) \ + (P >= 24 ? &PORTA : (P <= 7 ? &PORTB : ((P >= 8 && P <=15) ? &PORTD : &PORTC))) +#define PJON_IO_PIN_TO_DDR_REG(P) \ + (P >= 24 ? &DDRA : (P <= 7 ? &DDRB : ((P >= 8 && P <=15) ? &DDRD : &DDRC))) +#define PJON_IO_PIN_TO_PIN_REG(P) \ + (P >= 24 ? &PINA : (P <= 7 ? &PINB : ((P >= 8 && P <=15) ? &PIND : &PINC))) +#ifndef PJON_IO_PIN_TO_BIT +#define PJON_IO_PIN_TO_BIT(P) \ + (P >= 24 ? P-24 : (P <= 7 ? P : ((P >= 8 && P <=15) ? P-8 : P-16 ) ) ) +#endif +#endif + +/* AVR ATtiny44/84/44A/84A ------------------------------------------------ */ +#if defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__) || \ + defined(__AVR_ATtiny44A__) || defined(__AVR_ATtiny84A__) +#define PJON_IO_PIN_TO_PORT_REG(P) \ + ((P <= 7) ? &PORTA : &PORTB ) +#define PJON_IO_PIN_TO_DDR_REG(P) \ + ((P <= 7) ? &DDRA : &DDRB ) +#define PJON_IO_PIN_TO_PIN_REG(P) \ + ((P <= 7) ? &PINA : &PINB) +#ifndef PJON_IO_PIN_TO_BIT +#define PJON_IO_PIN_TO_BIT(P) \ + ((P <= 7) ? P : ((P == 8) ? 2 : ( (P == 9) ? 3 : ( (P == 10) ? 1 : 0)))) +#endif +#endif + +/* SAMD21G18A - Arduino Zero ---------------------------------------------- */ + +#if defined(__SAMD21G18A__) || defined(ARDUINO_SAM_ZERO) // Arduino Zero pins +#define PJON_IO_PIN_TO_PORT_BIT(P) \ + ((P == 0) ? PORT_PA11 : ((P == 1) ? PORT_PA10 : ((P == 2) ? PORT_PA14 : \ + ((P == 3) ? PORT_PA09 : ((P == 4) ? PORT_PA08 : ((P == 5) ? PORT_PA15 : \ + ((P == 6) ? PORT_PA20 : ((P == 7) ? PORT_PA21 : ((P == 8) ? PORT_PA06 : \ + ((P == 9) ? PORT_PA07 : ((P == 10) ? PORT_PA18 : ((P == 11) ? PORT_PA16 : \ + ((P == 12) ? PORT_PA19 : ((P == 13) ? PORT_PA17 : ((P == A0) ? PORT_PA02 : \ + ((P == A1) ? PORT_PB08 : ((P == A2) ? PORT_PB09 : ((P == A3) ? PORT_PA04 : \ + ((P == A4) ? PORT_PA05 : ((P == A5) ? PORT_PB02 : ((P == SCK) ? PORT_PB11 : \ + ((P == MISO) ? PORT_PA12 : ((P == MOSI) ? PORT_PB10 : ((P == PIN_WIRE_SCL) ? \ + PORT_PA23 : ((P == PIN_WIRE_SDA) ? PORT_PA22 : PORT_PA13 \ + ))))))))))))))))))))))))) + +#define PJON_IO_PIN_TO_PORT_REG(P) \ + ((P >= 15 && P <= 16) || (P == 19) || (P >= 23 && P <= 24) ? PORTB : PORTA ) +#define PJON_IO_PIN_TO_PORT_REGOUT(P) \ + ((P >= 15 && P <= 16) || (P == 19) || (P >= 23 && P <= 24) ? \ + REG_PORT_OUT1 : REG_PORT_OUT0) +#define PJON_IO_PIN_TO_PORT_REGOUTSET(P) \ + ((P >= 15 && P <= 16) || (P == 19) || (P >= 23 && P <= 24) ? \ + REG_PORT_OUTSET1 : REG_PORT_OUTSET0) +#define PJON_IO_PIN_TO_PORT_REGOUTCLR(P) \ + ((P >= 15 && P <= 16) || (P == 19) || (P >= 23 && P <= 24) ? \ + REG_PORT_OUTCLR1 : REG_PORT_OUTCLR0) +#define PJON_IO_PIN_TO_PORT_REGPINCFG(P) \ + ((P >= 15 && P <= 16) || (P == 19) || (P >= 23 && P <= 24) ? \ + REG_PORT_PINCFG1 : REG_PORT_PINCFG0) +#define PJON_IO_PIN_TO_PORT_REGDIRSET(P) \ + ((P >= 15 && P <= 16) || (P == 19) || (P >= 23 && P <= 24) ? \ + REG_PORT_DIRSET1 : REG_PORT_DIRSET0) +#define PJON_IO_PIN_TO_PORT_REGDIRCLR(P) \ + ((P >= 15 && P <= 16) || (P == 19) || (P >= 23 && P <= 24) ? \ + REG_PORT_DIRCLR1 : REG_PORT_DIRCLR0) +#define PJON_IO_PIN_TO_PORT_IN_REG(P) \ + ((P >= 15 && P <= 16) || (P == 19) || (P >= 23 && P <= 24) ? \ + REG_PORT_IN1 : REG_PORT_IN0) + +/* functions for read/write/pinmode fast for SAMD chips + PWM pins 3, 4, 5, 6, 8, 9, 10, 11, 12, 13 provide 8-bit PWM output with the + analogWrite() function analogWrite works on all analog pins and all digital + PWM pins. You can supply it any value between 0 and 255. analog pins provide + 12bits resolution and provide a value between 0-4096. + + INPUT (0x0), + OUTPUT (0x1), + INPUT_PULLUP (0x2) Set pin to input mode with pull-up resistor enabled, + INPUT_PULLDOWN (0x3) */ + +#define PJON_IO_MODE(P, V) \ + do { if (__builtin_constant_p(P) && __builtin_constant_p(V)) { \ + if(V == 0x0) { \ + PJON_IO_PIN_TO_PORT_REGPINCFG(P) = (uint8_t)(PORT_PINCFG_INEN); \ + PJON_IO_PIN_TO_PORT_REGDIRCLR(P) = (uint32_t)PJON_IO_PIN_TO_PORT_BIT(P); \ + } else if(V == 0x2) { \ + PORT->Group[PJON_IO_PIN_TO_PORT_REG(11)].PINCFG[g_APinDescription[11].ulPin].reg = \ + (uint8_t)(PORT_PINCFG_INEN|PORT_PINCFG_PULLEN); \ + PJON_IO_PIN_TO_PORT_REGDIRCLR(P) = (uint32_t)PJON_IO_PIN_TO_PORT_BIT(P); \ + PJON_IO_PIN_TO_PORT_REGOUTSET(P) = (uint32_t)PJON_IO_PIN_TO_PORT_BIT(P); \ + } else if(V == 0x3) { \ + PORT->Group[g_APinDescription[11].ulPort].PINCFG[g_APinDescription[11].ulPin].reg = \ + (uint8_t)(PORT_PINCFG_INEN|PORT_PINCFG_PULLEN) ; \ + PJON_IO_PIN_TO_PORT_REGDIRCLR(P) = (uint32_t)PJON_IO_PIN_TO_PORT_BIT(P); \ + PJON_IO_PIN_TO_PORT_REGOUTCLR(P) = (uint32_t)PJON_IO_PIN_TO_PORT_BIT(P); \ + } else { \ + PJON_IO_PIN_TO_PORT_REGPINCFG(P) &= ~(uint8_t)(PORT_PINCFG_INEN); \ + PJON_IO_PIN_TO_PORT_REGDIRSET(P) = (uint32_t)PJON_IO_PIN_TO_PORT_BIT(P); \ + } \ + } else pinMode(P, V); \ + } while (0) + +#define PJON_IO_READ(P) ( (int) _PJON_IO_READ_(P) ) +#define _PJON_IO_READ_(P) \ + (__builtin_constant_p(P)) ? \ + (PJON_IO_PIN_TO_PORT_IN_REG(P) & PJON_IO_PIN_TO_PORT_BIT(P)) : digitalRead(P) + +#define PJON_IO_WRITE(P, V) \ + do { if (__builtin_constant_p(P) && __builtin_constant_p(V)) { \ + if(V) PJON_IO_PIN_TO_PORT_REGOUTSET(P) = PJON_IO_PIN_TO_PORT_BIT(P); \ + else PJON_IO_PIN_TO_PORT_REGOUTCLR(P) = PJON_IO_PIN_TO_PORT_BIT(P); \ + } else digitalWrite(P, V); \ + } while(0) + +#define PJON_IO_PULL_DOWN(P) \ + do { if(__builtin_constant_p(P)) { \ + PJON_IO_MODE(P, INPUT_PULLDOWN); \ + } else pinMode(P, INPUT_PULLDOWN); \ + } while(0) +#endif + +/* AVR related ------------------------------------------------------------ */ + +#ifdef __AVR__ +#define PJON_IO_ATOMIC_WRITE(A, P, V) \ + if ((int)(A) < 0x40) { bitWrite(*(A), PJON_IO_PIN_TO_BIT(P), V); } \ + else { \ + uint8_t register saveSreg = SREG; \ + cli(); \ + bitWrite(*(A), PJON_IO_PIN_TO_BIT(P), V); \ + sei(); \ + SREG = saveSreg; \ + } + +#ifndef PJON_IO_WRITE +#define PJON_IO_WRITE(P, V) \ + do { \ + if(__builtin_constant_p(P) && __builtin_constant_p(V)) \ + PJON_IO_ATOMIC_WRITE((uint8_t*) PJON_IO_PIN_TO_PORT_REG(P), P, V) \ + else digitalWrite(P, V); \ + } while(0) +#endif + +#ifndef PJON_IO_MODE +#define PJON_IO_MODE(P, V) \ + do { \ + if(__builtin_constant_p(P) && __builtin_constant_p(V)) \ + PJON_IO_ATOMIC_WRITE((uint8_t*) PJON_IO_PIN_TO_DDR_REG(P), P, V) \ + else pinMode(P, V); \ + } while(0) +#endif + +#ifndef PJON_IO_READ +#define PJON_IO_READ(P) ((int) _PJON_IO_READ_(P)) +#define _PJON_IO_READ_(P) \ + (__builtin_constant_p(P)) ? ( \ + ((((*PJON_IO_PIN_TO_PIN_REG(P)) >> (PJON_IO_PIN_TO_BIT(P))) & 0x01))) : \ + digitalRead(P) +#endif + +#define PJON_IO_PULL_DOWN(P) \ + do { if(__builtin_constant_p(P)) { \ + PJON_IO_WRITE(P, LOW); \ + PJON_IO_MODE(P, INPUT); \ + } else { \ + digitalWrite(P, LOW); \ + pinMode(P, INPUT); \ + } \ + } while(0) +#endif diff --git a/hal/transport/PJON/driver/interfaces/ARDUINO/TCPHelper_ARDUINO.h b/hal/transport/PJON/driver/interfaces/ARDUINO/TCPHelper_ARDUINO.h new file mode 100644 index 000000000..ac5d8174b --- /dev/null +++ b/hal/transport/PJON/driver/interfaces/ARDUINO/TCPHelper_ARDUINO.h @@ -0,0 +1,89 @@ +#pragma once + +#ifdef PJON_ESP +#if defined(ESP32) +#include +#else +#include +#endif +typedef WiFiServer TCPHelperServer; +typedef WiFiClient TCPHelperClient; +#define min(a,b) (a +#else +#include +#endif +// #include // May be required if using outdated IDE +typedef EthernetServer TCPHelperServer; +typedef EthernetClient TCPHelperClient; +#define PJON_GET_MAC EthernetClass::MACAddress +#endif + +/* +// The following classes make it posssible to use the same non-blocking connect logic +// on Arduino as on POSIX. It works, allowing better communication with POSIX devices, +// but requires that the official EthernetClient and EthernetServer classes are changed +// so that private members are made protected, making it possible to add +// functionality to them. + +class TCPHelperClient : public EthernetClient { +public: + bool prepare_connect(IPAddress ip, uint16_t port) { + if (_sock != MAX_SOCK_NUM) + stop(); + + if (_sock != MAX_SOCK_NUM) + return false; + + for (int i = 0; i < MAX_SOCK_NUM; i++) { + uint8_t s = socketStatus(i); + if (s == SnSR::CLOSED || s == SnSR::FIN_WAIT || s == SnSR::CLOSE_WAIT) { + _sock = i; + break; + } + } + + if (_sock == MAX_SOCK_NUM) + return false; + + _srcport++; + if (_srcport == 0) _srcport = 49152; //Use IANA recommended ephemeral port range 49152-65535 + socket(_sock, SnMR::TCP, _srcport, 0); + + if (!::connect(_sock, rawIPAddress(ip), port)) { + _sock = MAX_SOCK_NUM; + return false; + } + + return true; + } + + int try_connect() { + if (status() == SnSR::ESTABLISHED) return 1; + if (status() == SnSR::CLOSED) { + _sock = MAX_SOCK_NUM; + return -1; + } + return 0; + } + + TCPHelperClient() : EthernetClient() { } + + TCPHelperClient(uint16_t sock) : EthernetClient(sock) { } + + TCPHelperClient(EthernetClient client) : EthernetClient(client ){ } +}; + +class TCPHelperServer : public EthernetServer { +public: + TCPHelperServer(uint16_t sock) : EthernetServer(sock) { } + + TCPHelperClient available() { + EthernetClient client = EthernetServer::available(); + return TCPHelperClient(client); + } +}; +*/ diff --git a/hal/transport/PJON/driver/interfaces/ARDUINO/UDPHelper_ARDUINO.h b/hal/transport/PJON/driver/interfaces/ARDUINO/UDPHelper_ARDUINO.h new file mode 100644 index 000000000..d1ef544c3 --- /dev/null +++ b/hal/transport/PJON/driver/interfaces/ARDUINO/UDPHelper_ARDUINO.h @@ -0,0 +1,130 @@ +#pragma once + +#ifdef PJON_ESP +#if defined(ESP32) +#include +#else +#include +#endif +#include +#define PJON_ESP +#else +#ifdef PJON_ETHERNET2 +#include +#include +#else +#include +#include +#endif +#endif + +class UDPHelper +{ + uint16_t _port; + uint32_t _magic_header; + +#ifdef PJON_ESP + WiFiUDP udp; +#else + EthernetUDP udp; +#endif + uint8_t _broadcast[4] = { 0xFF, 0xFF, 0xFF, 0xFF }; + +public: + + bool begin(uint16_t port) + { + _port = port; + return udp.begin(_port); + } + + uint16_t receive_frame(uint8_t *data, uint16_t max_length) + { + uint16_t result = PJON_FAIL; + int16_t packet_size = udp.parsePacket(); + if(packet_size > 0) { + uint32_t header = 0; + int16_t len = udp.read((char *) &header, 4), remaining = packet_size - len; + if (len == -1) { + return result; + } + if(len == 4 && header == _magic_header && (unsigned)packet_size <= 4 + max_length) { + len = udp.read(data, packet_size - 4); + if (len == -1) { + return result; + } + if (len == packet_size - 4) { + result = packet_size - 4; + } + remaining -= len; + } + if (remaining > 0) { + // We must still read every byte because some implementations + // (like ESP32) will not clear the receive buffer and therefore will + // not receive any more packets otherwise. + while (remaining > 0 && len > 0) { + len = udp.read(data, (unsigned)remaining <= max_length ? remaining : max_length); + if (len > 0) { + remaining -= len; + } + } + } + } + return result; + } + + void send_frame(uint8_t *data, uint16_t length, IPAddress remote_ip, uint16_t remote_port) + { + if(length > 0) { + udp.beginPacket(remote_ip, remote_port); +#if defined(ESP32) + udp.write((const unsigned char*) &_magic_header, 4); +#else + udp.write((const char*) &_magic_header, 4); +#endif + udp.write(data, length); + udp.endPacket(); + } + } + + void send_response(uint8_t *data, uint16_t length) + { + send_frame(data, length, udp.remoteIP(), udp.remotePort()); + } + + void send_response(uint8_t response) + { + send_frame(&response, 1, udp.remoteIP(), udp.remotePort()); + } + + void send_frame(uint8_t *data, uint16_t length, uint8_t remote_ip[], uint16_t remote_port) + { + IPAddress address(remote_ip); + send_frame(data, length, address, remote_port); + } + + void send_frame(uint8_t *data, uint16_t length) + { + // Broadcast on local subnet, global broadcast may not be accepted + IPAddress broadcastIp; +#ifdef PJON_ESP + // 255.255.255.255 may not work, be more specific + broadcastIp = ~(uint32_t)WiFi.subnetMask() | (uint32_t)WiFi.localIP(); +#else + broadcastIp = _broadcast; +#endif + send_frame(data, length, broadcastIp, _port); + } + + void set_magic_header(uint32_t magic_header) + { + _magic_header = magic_header; + } + + void get_sender(uint8_t *ip, uint16_t &port) + { + uint32_t ip_address = udp.remoteIP(); + memcpy(ip, &ip_address, 4); + port = udp.remotePort(); + } +}; diff --git a/hal/transport/PJON/driver/interfaces/LINUX/PJON_LINUX_Interface.h b/hal/transport/PJON/driver/interfaces/LINUX/PJON_LINUX_Interface.h new file mode 100644 index 000000000..c898d2076 --- /dev/null +++ b/hal/transport/PJON/driver/interfaces/LINUX/PJON_LINUX_Interface.h @@ -0,0 +1,160 @@ + +/* PJON Linux Interface + 13/04/2020 - callalilychen, Use termios2 for generic baudrate support + ___________________________________________________________________________ + + Copyright 2018 Fred Larsen + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + +#pragma once + +#if defined(LINUX) || defined(ANDROID) + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { + extern int tcflush (int __fd, int __queue_selector); +} + +#include +#include +#include +#include + +#define OUTPUT 1 +#define INPUT 0 +#define HIGH 1 +#define LOW 0 +#define INPUT_PULLUP 0x2 +#define LSBFIRST 1 +#define MSBFIRST 2 + +uint32_t micros(); + +uint32_t millis(); + +void delayMicroseconds(uint32_t delay_value); + +void delay(uint32_t delay_value_ms); + +/* Open serial port ----------------------------------------------------- */ + +int serialOpen(const char *device, const int baud); + +/* Returns the number of bytes of data available to be read in the buffer */ + +int serialDataAvailable(const int fd); + +/* Reads a character from the serial buffer ------------------------------- */ + +int serialGetCharacter(const int fd); + +#ifndef PJON_LINUX_SEPARATE_DEFINITION +#include "PJON_LINUX_Interface.inl" +#endif + +/* Generic constants ---------------------------------------------------- */ + +#ifndef A0 +#define A0 0 +#endif + +#ifndef LED_BUILTIN +#define LED_BUILTIN -1 +#endif + +/* LINUX IO system calls ------------------------------------------------ */ + +#if !defined(PJON_ANALOG_READ) +#define PJON_ANALOG_READ(P) 0 +#endif + +#if !defined(PJON_IO_WRITE) +#define PJON_IO_WRITE(P, V) +#endif + +#if !defined(PJON_IO_READ) +#define PJON_IO_READ(P) 0 +#endif + +#if !defined(PJON_IO_MODE) +#define PJON_IO_MODE(P, V) +#endif + +#if !defined(PJON_IO_PULL_DOWN) +#define PJON_IO_PULL_DOWN(P) +#endif + +/* Random --------------------------------------------------------------- */ + +#ifndef PJON_RANDOM +#define PJON_RANDOM(randMax) (int)((1.0 + randMax) * rand() / ( RAND_MAX + 1.0 ) ) +/* Scale rand()'s return value against RAND_MAX using doubles instead of + a pure modulus to have a more distributed result */ +#endif + +#ifndef PJON_RANDOM_SEED +#define PJON_RANDOM_SEED srand +#endif + +/* Serial --------------------------------------------------------------- */ + +#ifndef PJON_SERIAL_TYPE +#define PJON_SERIAL_TYPE int16_t +#endif + +#ifndef PJON_SERIAL_AVAILABLE +#define PJON_SERIAL_AVAILABLE(S) serialDataAvailable(S) +#endif + +#ifndef PJON_SERIAL_WRITE +#define PJON_SERIAL_WRITE(S, C) write(S, &C, 1) +#endif + +#ifndef PJON_SERIAL_READ +#define PJON_SERIAL_READ(S) serialGetCharacter(S) +#endif + +#ifndef PJON_SERIAL_FLUSH +#define PJON_SERIAL_FLUSH(S) tcflush(S, TCIOFLUSH) +#endif + +/* Timing --------------------------------------------------------------- */ + +#ifndef PJON_DELAY +#define PJON_DELAY delay +#endif + +#ifndef PJON_DELAY_MICROSECONDS +#define PJON_DELAY_MICROSECONDS delayMicroseconds +#endif + +#ifndef PJON_MICROS +#define PJON_MICROS micros +#endif + +#ifndef PJON_MILLIS +#define PJON_MILLIS millis +#endif +#endif diff --git a/hal/transport/PJON/driver/interfaces/LINUX/PJON_LINUX_Interface.inl b/hal/transport/PJON/driver/interfaces/LINUX/PJON_LINUX_Interface.inl new file mode 100644 index 000000000..642d0d2d2 --- /dev/null +++ b/hal/transport/PJON/driver/interfaces/LINUX/PJON_LINUX_Interface.inl @@ -0,0 +1,113 @@ +#ifndef LINUX + #define LINUX +#endif +#ifndef PJON_LINUX_SEPARATE_DEFINITION + #define PJON_LINUX_SEPARATE_DEFINITION +#endif + +#include "PJON_LINUX_Interface.h" +#include "/usr/include/asm-generic/termbits.h" +#include "/usr/include/asm-generic/ioctls.h" + +auto start_ts = std::chrono::high_resolution_clock::now(); +auto start_ts_ms = std::chrono::high_resolution_clock::now(); + +uint32_t micros() { + auto elapsed_usec = + std::chrono::duration_cast( + std::chrono::high_resolution_clock::now() - start_ts + ).count(); + + if(elapsed_usec >= UINT32_MAX) { + start_ts = std::chrono::high_resolution_clock::now(); + return 0; + } else return elapsed_usec; +}; + +uint32_t millis() { + return (uint32_t) + std::chrono::duration_cast( + std::chrono::high_resolution_clock::now() - start_ts_ms + ).count(); +}; + +void delayMicroseconds(uint32_t delay_value) { + struct timeval tv; + if (delay_value < 1000000){ + tv.tv_sec = 0; + tv.tv_usec = delay_value; + } + else{ + tv.tv_sec = floor(delay_value / 1000000); + tv.tv_usec = delay_value - tv.tv_sec * 1000000; + } + + select(0, NULL, NULL, NULL, &tv); +}; + +void delay(uint32_t delay_value_ms) { + std::this_thread::sleep_for(std::chrono::milliseconds(delay_value_ms)); +}; + +/* Open serial port ----------------------------------------------------- */ + +int serialOpen(const char *device, const int baud) { + speed_t bd; + int fd; + + if((fd = open(device, O_NDELAY | O_NOCTTY | O_NONBLOCK | O_RDWR)) == -1) + return -1; + + fcntl(fd, F_SETFL, O_RDWR); + + struct termios2 config; + int state; + int r = ioctl(fd, TCGETS2, &config); + if(r) return -1; + + // sets the terminal to something like the "raw" mode of the old Version 7 terminal driver + config.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP + | INLCR | IGNCR | ICRNL | IXON); + config.c_cflag &= ~(CSTOPB | CSIZE | PARENB); + config.c_cflag |= CS8; + config.c_lflag &= ~(ECHO | ECHOE | ECHONL | ICANON | ISIG | IEXTEN); + config.c_oflag &= ~OPOST; + + // sets the baudrate + config.c_ispeed = config.c_ospeed = baud; + config.c_cflag &= ~CBAUD; + config.c_cflag |= BOTHER; + + config.c_cflag |= (CLOCAL | CREAD); + config.c_cc [VMIN] = 0; + config.c_cc [VTIME] = 50; // 5 seconds reception timeout + + r = ioctl(fd, TCSETS2, &config); + if(r) return -1; + + r = ioctl(fd, TIOCMGET, &state); + if(r) return -1; + + state |= (TIOCM_DTR | TIOCM_RTS); + r = ioctl(fd, TIOCMSET, &state); + if(r) return -1; + + delayMicroseconds(10000); // Sleep for 10 milliseconds + return fd; +}; + +/* Returns the number of bytes of data available to be read in the buffer */ + +int serialDataAvailable(const int fd) { + int result = 0; + ioctl(fd, FIONREAD, &result); + return result; +}; + +/* Reads a character from the serial buffer ------------------------------- */ + +int serialGetCharacter(const int fd) { + uint8_t result; + if(read(fd, &result, 1) != 1) return -1; + return ((int)result) & 0xFF; +}; diff --git a/hal/transport/PJON/driver/interfaces/LINUX/TCPHelper_POSIX.h b/hal/transport/PJON/driver/interfaces/LINUX/TCPHelper_POSIX.h new file mode 100644 index 000000000..2d038f3d4 --- /dev/null +++ b/hal/transport/PJON/driver/interfaces/LINUX/TCPHelper_POSIX.h @@ -0,0 +1,459 @@ +#pragma once + +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#include + +#define close(fd) closesocket(fd) +#define ssize_t int +#else +#include +#include +#include +#include +#include +#include +#include +#include +#include +#endif + +#ifndef constrain +#define constrain(x, l, u) (xu?u:x)) +#endif + +class TCPHelperClient +{ +#ifdef _WIN32 + SOCKET _fd = -1; +#else + int _fd = -1; +#endif + sockaddr_in _remote_addr; + +public: + TCPHelperClient() {} + TCPHelperClient(int fd) + { + _fd = fd; + } + TCPHelperClient(const TCPHelperClient &another) + { + *this = another; + } + + /* + ~TCPHelperClient() { + #ifdef _WIN32 + if (_fd != INVALID_SOCKET) closesocket(_fd); + WSACleanup(); + #else + if (_fd != INVALID_SOCKET) close(_fd); + #endif + } + */ + int available() + { +#ifdef _WIN32 + WSAPOLLFD pfs; + pfs.fd = _fd; + pfs.events = POLLIN; + pfs.revents = 0; + int rv = WSAPoll(&pfs, 1, 0); + if ((rv == -1 && WSAGetLastError() == WSAENETDOWN) || (rv > 0 && (pfs.revents & POLLNVAL) != 0)) { +#ifdef ETCP_ERROR_PRINT + printf("poll triggered stop, rv=%d: %s\n", rv, strerror(errno)); +#endif + stop(); // Socket problem, close it + } else if (rv > 0) { + return (pfs.revents & POLLRDNORM) != 0 || (pfs.revents & POLLHUP) != 0; + } +#else + struct pollfd pfs; + pfs.fd = _fd; + pfs.events = POLLIN; + pfs.revents = 0; + int rv = ::poll(&pfs, 1, 0); + if (rv == -1 || (rv > 0 && (pfs.revents & POLLNVAL) != 0)) { +#ifdef ETCP_ERROR_PRINT + printf("poll triggered stop, rv=%d: %s\n", rv, strerror(errno)); +#endif + stop(); // Socket problem, close it + } else if (rv > 0) { + return (pfs.revents & POLLIN) != 0 || (pfs.revents & POLLHUP) != 0; + } +#endif + return 0; + } + + bool connect(const uint8_t *address, uint16_t port) + { + if (_fd != -1) { +#ifdef ETCP_ERROR_PRINT + printf("connect triggered stop, _fd=%d\n", _fd); +#endif + stop(); + } + _fd = ::socket(AF_INET,SOCK_STREAM,0); + if (_fd == -1) { + return false; + } + memset(&_remote_addr, 0, sizeof(_remote_addr)); + _remote_addr.sin_family = AF_INET; + _remote_addr.sin_port = htons(port); + memcpy(&_remote_addr.sin_addr.s_addr, address, 4); + + // Shorten timeout for connecting + struct timeval read_timeout; + read_timeout.tv_sec = 0; + read_timeout.tv_usec = 2000000; +#ifndef __ZEPHYR__ + setsockopt(_fd, SOL_SOCKET, SO_RCVTIMEO, (char*)&read_timeout, sizeof read_timeout); + setsockopt(_fd, SOL_SOCKET, SO_SNDTIMEO, (char*)&read_timeout, sizeof read_timeout); +#endif + + bool connected = ::connect(_fd, (struct sockaddr *) &_remote_addr,sizeof(_remote_addr)) == 0; + if (connected) { +#ifndef __ZEPHYR__ + // Shorten timeouts for reading and writing + struct timeval read_timeout; + read_timeout.tv_sec = 0; + read_timeout.tv_usec = 1000; + setsockopt(_fd, SOL_SOCKET, SO_RCVTIMEO, (char*)&read_timeout, sizeof read_timeout); + read_timeout.tv_usec = 2000000; + setsockopt(_fd, SOL_SOCKET, SO_SNDTIMEO, (char*)&read_timeout, sizeof read_timeout); +#endif + // Disable Nagles algorith because we are sending small packets and waiting for reply + set_nodelay(true); + } + return connected; + } + + // Prepare a socket for non-blocking connect + bool prepare_connect(const uint8_t *address, uint16_t port) + { + if (_fd != -1) { +#ifdef ETCP_ERROR_PRINT + printf("prepare_connect triggered stop, _fd=%d\n", _fd); + stop(); +#endif + } + _fd = ::socket(AF_INET,SOCK_STREAM,0); + if (_fd == -1) { + return false; + } + memset(&_remote_addr, 0, sizeof(_remote_addr)); + _remote_addr.sin_family = AF_INET; + _remote_addr.sin_port = htons(port); + memcpy(&_remote_addr.sin_addr.s_addr, address, 4); +#ifdef _WIN32 + unsigned long ul = 1; + int flags = ioctlsocket(_fd, FIONBIO, (unsigned long *)&ul); //Set into non blocking mode. + if (flags != SOCKET_ERROR) ; // Failed to set? +#else + int flags = fcntl(_fd, F_GETFL, 0); + if (flags!=-1) { + fcntl(_fd, F_SETFL, flags | O_NONBLOCK); + } +#endif + else { +#ifdef ETCP_ERROR_PRINT + printf("prepare_connect triggered stop, _fd=%d: %s\n", _fd, strerror(errno)); +#endif + } + return flags != -1; + } + + // Try a non-blocking connect, it may succeed at once or after some calls + // Returns -1 if failure, 0 if waiting to connect, 1 when connected + int8_t try_connect() + { + int8_t connected = 0; + int status = ::connect(_fd, (struct sockaddr *) &_remote_addr,sizeof(_remote_addr)); + if (status == -1) { +#ifdef _WIN32 + int errno2 = WSAGetLastError(); + if (errno2 == WSAEISCONN); // Already connected, proceed with setting timeouts + else if (errno2 == WSAEWOULDBLOCK || errno2 == WSAEINPROGRESS || errno2 == WSAEALREADY) { + return 0; // Normal, we are waiting for connect to succeed + } +#else + if (errno == EISCONN); // Already connected, proceed with setting timeouts + else if (errno == EINPROGRESS || errno == EALREADY) { + return 0; // Normal, we are waiting for connect to succeed + } +#endif + else { +#ifdef ETCP_ERROR_PRINT +#ifdef _WIN32 + printf("try_connect triggered stop, _fd=%d: %d\n", _fd, errno2); +#else + printf("try_connect triggered stop, _fd=%d: %d %s\n", _fd, errno, strerror(errno)); +#endif +#endif + stop(); + return -1; // Some other error + } + } + + // Enable blocking operations +#ifdef _WIN32 + unsigned long ul = 0; + int flags = ioctlsocket(_fd, FIONBIO, (unsigned long *)&ul); // Set into non blocking mode. + //if (flags != SOCKET_ERROR) ; // Failed to set. +#else + int flags = fcntl(_fd, F_GETFL, 0); + if (flags != -1) { + fcntl(_fd, F_SETFL, flags & ~O_NONBLOCK); + } +#endif + + // Set timeouts for reading and writing +#ifndef __ZEPHYR__ + struct timeval read_timeout; + read_timeout.tv_sec = 0; + read_timeout.tv_usec = 1000; + setsockopt(_fd, SOL_SOCKET, SO_RCVTIMEO, (char*)&read_timeout, sizeof read_timeout); + read_timeout.tv_usec = 2000000; + setsockopt(_fd, SOL_SOCKET, SO_SNDTIMEO, (char*)&read_timeout, sizeof read_timeout); +#endif + // Disable Nagles algorith because we are sending small packets and waiting for reply + set_nodelay(true); + + return 1; + } + + // Enable or disable Nagles algorithm to send unfilled packets immediately or not + void set_nodelay(bool nodelay) + { + int flag = nodelay; + if (_fd != -1) { + setsockopt(_fd, IPPROTO_TCP, TCP_NODELAY, (char*)&flag, sizeof flag); + } + } + + bool connected() + { + return _fd != -1; + } + + int read(uint8_t *buffer, int buffer_size) + { + if (_fd == -1) { + return -1; + } + int r = ::recv(_fd, (char*)buffer, buffer_size, 0); //MSG_DONTWAIT); + if (r == -1) { +#ifdef _WIN32 + int errno2 = WSAGetLastError(); + if (errno2 == WSAEWOULDBLOCK) { + return 0; // Waiting for more + } +#ifdef ETCP_ERROR_PRINT + printf("read triggered stop, r=%d: %d\n", r, errno2); +#endif +#else + if (errno == EAGAIN) { + return 0; // Waiting for more + } +#ifdef ETCP_ERROR_PRINT + printf("read triggered stop, r=%d: %s\n", r, strerror(errno)); +#endif +#endif + stop(); + } + return r; + } + + int write(const uint8_t *buffer, int size) + { + if (_fd == -1) { + return -1; + } +#if defined(_WIN32) || defined(__ZEPHYR__) + int w = ::send(_fd, (char*)buffer, size, 0); +#else + int w = ::send(_fd, (char*)buffer, size, MSG_NOSIGNAL); +#endif + if (w == -1) { +#ifdef ETCP_ERROR_PRINT +#ifdef _WIN32 + int errno2 = WSAGetLastError(); + printf("write triggered stop, w=%d: %d\n", w, errno2); +#else + if (errno != EPIPE) { + printf("write triggered stop, w=%d: %s\n", w, strerror(errno)); + } +#endif +#endif + stop(); + } + return w; + } + + void flush() { } + + void stop() + { + if (_fd != -1) { +#ifndef _WIN32 + ::shutdown(_fd, SHUT_RDWR); +#endif + ::close(_fd); + _fd = -1; + } + } + + void operator=(const TCPHelperClient &another) + { + _fd = another._fd; + } + + operator bool() + { + return connected(); + } + bool operator==(const bool value) + { + return bool() == value; + } + bool operator!=(const bool value) + { + return bool() != value; + } + bool operator==(const TCPHelperClient& rhs) + { + return _fd == rhs._fd && _fd != -1 && rhs._fd != -1; + } + bool operator!=(const TCPHelperClient& rhs) + { + return !this->operator==(rhs); + } + uint8_t getSocketNumber() + { + return _fd; + } + + int print(const char *msg) + { + return write((const uint8_t*) msg, strlen(msg)); + } +}; + + +class TCPHelperServer +{ + uint16_t _port = 0; + int _fd = -1; + sockaddr_in _localaddr, _remote_sender_addr; + +public: + TCPHelperServer(uint16_t listening_port) + { + _port = listening_port; + } + ~TCPHelperServer() + { +#ifdef ETCP_DEBUG_PRINT + printf("destructor triggered stop, _fd=%d\n", _fd); +#endif + stop(); + } + + TCPHelperClient available() + { + socklen_t len = sizeof(_remote_sender_addr); + memset(&_remote_sender_addr, 0, len); + int connected_fd = _fd == -1 ? -1 : ::accept(_fd, (struct sockaddr *) &_remote_sender_addr, &len); + if (connected_fd != -1) { +#ifndef __ZEPHYR__ + // Shorten timeout for reading and writing + struct timeval read_timeout; + read_timeout.tv_sec = 0; + read_timeout.tv_usec = 1000; + setsockopt(connected_fd, SOL_SOCKET, SO_RCVTIMEO, (char*)&read_timeout, sizeof read_timeout); + read_timeout.tv_usec = 2000000; + setsockopt(connected_fd, SOL_SOCKET, SO_SNDTIMEO, (char*)&read_timeout, sizeof read_timeout); +#endif + // Disable Nagles algorith because we are sending small packets and waiting for reply + int flag = 1; + setsockopt(connected_fd, IPPROTO_TCP, TCP_NODELAY, (char*)&flag, sizeof flag); + } + return TCPHelperClient(connected_fd); + } + + bool begin() + { + // Close if open after previous init attempt + if (_fd != -1) { + close(_fd); + _fd = -1; + } + + // Prepare socket + _fd=socket(AF_INET,SOCK_STREAM,0); + if (_fd==-1) { +#ifdef ETCP_ERROR_PRINT + printf("failure creating socket: %s\n", strerror(errno)); +#endif + return false; + } + + int yes = 1; + setsockopt(_fd, SOL_SOCKET, SO_REUSEADDR, (char*)&yes, sizeof(yes)); + + // Bind to specific local port + memset(&_localaddr, 0, sizeof(_localaddr)); + _localaddr.sin_family = AF_INET; + _localaddr.sin_port = htons(_port); + _localaddr.sin_addr.s_addr = htonl(INADDR_ANY); + if (bind(_fd,(struct sockaddr *) &_localaddr,sizeof(_localaddr))==-1) { +#ifdef ETCP_ERROR_PRINT + printf("bind failed: %s\n", strerror(errno)); +#endif + return false; + } + + // Start listening + if (listen(_fd, 5) == -1) { +#ifdef ETCP_ERROR_PRINT + printf("listen failed: %s\n", strerror(errno)); +#endif + return false; + } + + // Set the socket to nonblocking so that accept will not block +#ifdef _WIN32 + unsigned long ul = 1; + int flags = ioctlsocket(_fd, FIONBIO, (unsigned long *)&ul); //Set into non blocking mode. + if (flags != NO_ERROR) { // Failed to set. +#ifdef ETCP_ERROR_PRINT + printf("ioctlsocket FIONBIO failed."); +#endif + } +#else + int flags = fcntl(_fd, F_GETFL, 0); + if (flags != -1) { + fcntl(_fd, F_SETFL, flags | O_NONBLOCK); + } +#endif + memset(&_remote_sender_addr, 0, sizeof(_remote_sender_addr)); + return true; + } + + void stop() + { + if (_fd != -1) { + ::close(_fd); + _fd = -1; + } + } +}; + +#undef close diff --git a/hal/transport/PJON/driver/interfaces/LINUX/UDPHelper_POSIX.h b/hal/transport/PJON/driver/interfaces/LINUX/UDPHelper_POSIX.h new file mode 100644 index 000000000..54c1733c0 --- /dev/null +++ b/hal/transport/PJON/driver/interfaces/LINUX/UDPHelper_POSIX.h @@ -0,0 +1,226 @@ +#pragma once + +#include +#include + +#ifdef _WIN32 +#include +#include + +#define close(fd) closesocket(fd) +#define ssize_t int +#else +#include +#include +#include +#include +#include +#include +#include +#endif + +#ifdef __ZEPHYR__ +#include +#define INADDR_BROADCAST ((u32_t) 0xffffffff) +#endif + +class UDPHelper +{ + uint16_t _port; + uint32_t _magic_header; + sockaddr_in _localaddr, _remote_receiver_addr, _remote_sender_addr; + int _fd = -1; +public: + ~UDPHelper() + { + if (_fd != -1) +#ifdef _WIN32 + closesocket(_fd); + WSACleanup(); +#else + close(_fd); +#endif + } + + bool begin(uint16_t port) + { +#ifdef _WIN32 + // Initialize Winsock + WSAData wsaData; + int iResult; + iResult = WSAStartup(MAKEWORD(2, 2), &wsaData); + if (iResult != 0) { + return false; + } +#endif + _port = port; + + // Close if open after previous init attempt + if (_fd != -1) { +#ifdef _WIN32 + closesocket(_fd); +#else + close(_fd); +#endif + _fd = -1; + } + + // Prepare socket + _fd=socket(AF_INET,SOCK_DGRAM, IPPROTO_UDP); + if (_fd==-1) { + //printf("INIT listening socket %s\n", strerror(errno)); + return false; + } + + // Make it non-blocking +#ifdef _WIN32 + DWORD read_timeout = 10; // millis +#else + struct timeval read_timeout; + read_timeout.tv_sec = 0; + read_timeout.tv_usec = 1000; +#endif +#ifdef __ZEPHYR__ + fcntl(_fd, F_SETFL, O_NONBLOCK); +#else + setsockopt(_fd, SOL_SOCKET, SO_RCVTIMEO, (char *)&read_timeout, sizeof read_timeout); +#endif + + // Bind to specific local port + memset(&_localaddr, 0, sizeof(_localaddr)); + _localaddr.sin_family = AF_INET; + _localaddr.sin_port = htons(_port); + _localaddr.sin_addr.s_addr = INADDR_ANY; + if (bind(_fd,(struct sockaddr *) &_localaddr,sizeof(_localaddr))==-1) { + //printf("INIT listening bind %s\n", strerror(errno)); + return false; + } + memset(&_remote_sender_addr, 0, sizeof(_remote_sender_addr)); + + // Prepare receiver address as broadcast + memset(&_remote_receiver_addr, 0, sizeof(_remote_receiver_addr)); + _remote_receiver_addr.sin_family = AF_INET; + _remote_receiver_addr.sin_port = htons(_port); + _remote_receiver_addr.sin_addr.s_addr = INADDR_BROADCAST; + +#ifndef __ZEPHYR__ + // Allow broadcasts + int broadcast=1; + if (setsockopt(_fd,SOL_SOCKET,SO_BROADCAST,(const char*)&broadcast,sizeof(broadcast))==-1) { + //printf("INIT send setsockopt %s\n", strerror(errno)); + return false; + } +#endif + return true; + } + + uint16_t receive_frame(uint8_t *string, uint16_t max_length) + { + struct sockaddr_storage src_addr; + socklen_t src_addr_len=sizeof(src_addr); + ssize_t count=recvfrom(_fd,(char*)string,max_length,0,(struct sockaddr*)&src_addr,&src_addr_len); + if (count==-1) { +#ifdef _WIN32 + //int error = WSAGetLastError(); + //if (error != WSAETIMEDOUT) printf("recvfrom error %d, bufsize=%d\n", error, max_length); +#endif + return false; // Reception failed + } else if (count==max_length || count < 4) { + //printf("FAIL receive_frame recvfrom %d\n", count); + return false; // Too large packet + } else { + //printf("OK receive_frame recvfrom %d, maxize %d\n", count, max_length); + // Remember sender's address + memcpy(&_remote_sender_addr, &src_addr, sizeof(_remote_sender_addr)); + + // Get header + uint32_t header = 0; + memcpy(&header, string, 4); + if(header != _magic_header) { + return false; // Not a LocalUDP packet + } + + // Shift contents to remove header + for (uint16_t i=0; i 0) { + Buf buffer(4 + length); + memcpy(buffer(), &_magic_header, 4); + memcpy(&(buffer()[4]), string, length); + int res = sendto(_fd,buffer(),buffer.size(),0,(const sockaddr *)&remote_addr,sizeof(remote_addr)); + //printf("send_frame %d sendto %d\n", length, res); + } + } + + void send_response(uint8_t *string, uint16_t length) + { + send_frame((const uint8_t *)string, length, _remote_sender_addr); + } + + void send_response(uint8_t response) + { + send_frame((const uint8_t *)&response, 1, _remote_sender_addr); + } + + void send_frame(const uint8_t *string, uint16_t length) + { + _remote_receiver_addr.sin_port = htons(_port); + _remote_receiver_addr.sin_addr.s_addr = INADDR_BROADCAST; + send_frame(string, length, _remote_receiver_addr); + } + + void send_frame(const uint8_t *string, uint16_t length, uint8_t *remote_ip, uint16_t remote_port) + { + _remote_receiver_addr.sin_port = htons(remote_port); + _remote_receiver_addr.sin_addr.s_addr = *(uint32_t*)remote_ip; + send_frame(string, length, _remote_receiver_addr); + } + + void set_magic_header(uint32_t magic_header) + { + _magic_header = magic_header; + } + + void get_sender(uint8_t *ip, uint16_t &port) + { + memcpy(ip, &_remote_sender_addr.sin_addr.s_addr, 4); + port = ntohs(_remote_sender_addr.sin_port); + } +}; + +#undef close diff --git a/hal/transport/PJON/driver/interfaces/PJON_Interfaces.h b/hal/transport/PJON/driver/interfaces/PJON_Interfaces.h new file mode 100644 index 000000000..4db6c7203 --- /dev/null +++ b/hal/transport/PJON/driver/interfaces/PJON_Interfaces.h @@ -0,0 +1,8 @@ + +#pragma once + +#include "ARDUINO/PJON_ARDUINO_Interface.h" +#include "RPI/PJON_RPI_Interface.h" +#include "WINX86/PJON_WINX86_Interface.h" +#include "LINUX/PJON_LINUX_Interface.h" +#include "ZEPHYR/PJON_ZEPHYR_Interface.h" diff --git a/hal/transport/PJON/driver/interfaces/README.md b/hal/transport/PJON/driver/interfaces/README.md new file mode 100644 index 000000000..665796cf0 --- /dev/null +++ b/hal/transport/PJON/driver/interfaces/README.md @@ -0,0 +1,77 @@ + +### What is an interface? +PJON uses interfaces to handle low level system calls using the pre-processor, abstracting all architecture related implementation and enhancing portability without affecting the memory footprint. Thanks to the interfaces abstraction the PJON source is released in one unified implementation that compiles in all supported devices, architectures or operative systems. An interface is simply a file containing a set of common macros pointing to dedicated system calls. Interfaces provide users with an easy way to port PJON on virtually every architecture able to execute a compiled C++ program, simply filling a list of method definitions. + +Here as an example, the Arduino interface already present in PJON: +```cpp +// Use an architecture related constant to conditionally include the interface +#if defined(ARDUINO) + #include + #include "PJON_IO.h" + // Architecture related necessary files inclusion + + /* IO methods definition ------------------------------------------------- */ + + #if !defined(PJON_ANALOG_READ) + #define PJON_ANALOG_READ analogRead + #endif + + #if !defined(PJON_IO_WRITE) + #define PJON_IO_WRITE digitalWrite + #endif + + #if !defined(PJON_IO_READ) + #define PJON_IO_READ digitalRead + #endif + + #if !defined(PJON_IO_MODE) + #define PJON_IO_MODE pinMode + #endif + + #if !defined(PJON_IO_PULL_DOWN) + #define PJON_IO_PULL_DOWN(P) \ + do { \ + digitalWrite(P, LOW); \ + pinMode(P, INPUT); \ + } while(0) + #endif + + /* Random ----------------------------------------------------------------- */ + + #ifndef PJON_RANDOM + #define PJON_RANDOM random + #endif + + #ifndef PJON_RANDOM_SEED + #define PJON_RANDOM_SEED randomSeed + #endif + + /* Serial ----------------------------------------------------------------- */ + + #ifndef PJON_SERIAL_AVAILABLE + #define PJON_SERIAL_AVAILABLE(S) S->available() + #endif + + #ifndef PJON_SERIAL_WRITE + #define PJON_SERIAL_WRITE(S, C) S->write(C) + #endif + + #ifndef PJON_SERIAL_READ + #define PJON_SERIAL_READ(S) S->read() + #endif + + #ifndef PJON_SERIAL_FLUSH + #define PJON_SERIAL_FLUSH(S) S->flush() + #endif + + /* Timing ----------------------------------------------------------------- */ + + #ifndef PJON_DELAY_MICROSECONDS + #define PJON_DELAY_MICROSECONDS delayMicroseconds + #endif + + #ifndef PJON_MICROS + #define PJON_MICROS micros + #endif +#endif +``` diff --git a/hal/transport/PJON/driver/interfaces/RPI/PJON_RPI_Interface.h b/hal/transport/PJON/driver/interfaces/RPI/PJON_RPI_Interface.h new file mode 100644 index 000000000..7c16d195c --- /dev/null +++ b/hal/transport/PJON/driver/interfaces/RPI/PJON_RPI_Interface.h @@ -0,0 +1,121 @@ + +/* PJON RaspeberryPi Interface + _____________________________________________________________________________ + + Copyright 2018 Giovanni Blu Mitolo gioscarab@gmail.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + +#pragma once + +#if defined(RPI) +#include +#include +#include +#include +#include + +/* Generic constants ---------------------------------------------------- */ + +#ifndef A0 +#define A0 0 +#endif + +#ifndef LED_BUILTIN +#define LED_BUILTIN -1 +#endif + +/* Fallback to WiringPi core functions ---------------------------------- */ + +#if !defined(PJON_ANALOG_READ) +#define PJON_ANALOG_READ(P) analogRead(P) +#endif + +#if !defined(PJON_IO_WRITE) +#define PJON_IO_WRITE digitalWrite +#endif + +#if !defined(PJON_IO_READ) +#define PJON_IO_READ digitalRead +#endif + +#if !defined(PJON_IO_MODE) +#define PJON_IO_MODE pinMode +#endif + +#if !defined(PJON_IO_PULL_DOWN) +#define PJON_IO_PULL_DOWN(P) { \ + PJON_IO_MODE(P, INPUT); \ + pullUpDnControl(P, PUD_DOWN); \ + } +#endif + +/* Random ----------------------------------------------------------------- */ + +#ifndef PJON_RANDOM +#define PJON_RANDOM(randMax) (int)((1.0 + randMax) * rand() / ( RAND_MAX + 1.0 ) ) +/* Scale rand()'s return value against RAND_MAX using doubles instead of + a pure modulus to have a more distributed result */ +#endif + +#ifndef PJON_RANDOM_SEED +#define PJON_RANDOM_SEED srand +#endif + +/* Serial ----------------------------------------------------------------- */ + +#ifndef PJON_SERIAL_TYPE +#define PJON_SERIAL_TYPE int16_t +#endif + +#ifndef PJON_SERIAL_AVAILABLE +#define PJON_SERIAL_AVAILABLE(S) serialDataAvail(S) +#endif + +#ifndef PJON_SERIAL_WRITE +#define PJON_SERIAL_WRITE(S, C) write(S, &C, 1) +#endif + +#ifndef PJON_SERIAL_READ +#define PJON_SERIAL_READ(S) serialGetchar(S) +#endif + +#ifndef PJON_SERIAL_FLUSH +#define PJON_SERIAL_FLUSH(S) serialFlush(S) +#endif + +/* Timing offset in microseconds between expected and real serial + byte transmission: */ + +#ifndef TS_FLUSH_OFFSET +#define TS_FLUSH_OFFSET 152 +#endif + +/* Timing ----------------------------------------------------------------- */ + +#ifndef PJON_DELAY +#define PJON_DELAY delay +#endif + +#ifndef PJON_DELAY_MICROSECONDS +#define PJON_DELAY_MICROSECONDS delayMicroseconds +#endif + +#ifndef PJON_MICROS +#define PJON_MICROS micros +#endif + +#ifndef PJON_MILLIS +#define PJON_MILLIS millis +#endif +#endif diff --git a/hal/transport/PJON/driver/interfaces/WINX86/PJON_WINX86_Interface.h b/hal/transport/PJON/driver/interfaces/WINX86/PJON_WINX86_Interface.h new file mode 100644 index 000000000..8584f4a2b --- /dev/null +++ b/hal/transport/PJON/driver/interfaces/WINX86/PJON_WINX86_Interface.h @@ -0,0 +1,171 @@ + +/* PJON Windows x86 Interface + _____________________________________________________________________________ + + Copyright 2018 Zbigniew Zasieczny z.zasieczny@gmail.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + +#pragma once + +#if defined(_WIN32) + +#include +#include +#include +#include "Serial/Serial.h" + +#define OUTPUT 1 +#define INPUT 0 +#define HIGH 1 +#define LOW 0 +#define INPUT_PULLUP 0x2 +#define LSBFIRST 1 +#define MSBFIRST 2 + +auto start_ts = std::chrono::high_resolution_clock::now(); +auto start_ts_ms = std::chrono::high_resolution_clock::now(); + +uint32_t micros() +{ + auto elapsed_usec = + std::chrono::duration_cast( + std::chrono::high_resolution_clock::now() - start_ts + ).count(); + + if(elapsed_usec >= UINT32_MAX) { + start_ts = std::chrono::high_resolution_clock::now(); + return 0; + } else { + return (uint32_t) elapsed_usec; + } +}; + +uint32_t millis() +{ + return (uint32_t) + std::chrono::duration_cast( + std::chrono::high_resolution_clock::now() - start_ts_ms + ).count(); +}; + + +void delayMicroseconds(uint32_t delay_value) +{ + auto begin_ts = std::chrono::high_resolution_clock::now(); + while(true) { + auto elapsed_usec = + std::chrono::duration_cast( + std::chrono::high_resolution_clock::now() - begin_ts + ).count(); + if(elapsed_usec >= delay_value) { + break; + } + std::this_thread::sleep_for(std::chrono::microseconds(50)); + } +}; + +void delay(uint32_t delay_value_ms) +{ + std::this_thread::sleep_for(std::chrono::milliseconds(delay_value_ms)); +} + +/* Generic constants ---------------------------------------------------- */ + +#ifndef A0 +#define A0 0 +#endif + +#ifndef LED_BUILTIN +#define LED_BUILTIN -1 +#endif + +/* Fallback to WINDOWS-specific functions -------------------------------- */ + +#if !defined(PJON_ANALOG_READ) +#define PJON_ANALOG_READ(P) 0 +#endif + +#if !defined(PJON_IO_WRITE) +// Avoid warning C4390 (empty controlled statement) +#define PJON_IO_WRITE(P, V) (void)0 +#endif + +#if !defined(PJON_IO_READ) +#define PJON_IO_READ(P) 0 +#endif + +#if !defined(PJON_IO_MODE) +// Avoid warning C4390 (empty controlled statement) +#define PJON_IO_MODE(P, V) (void)0 +#endif + +#if !defined(PJON_IO_PULL_DOWN) +// Avoid warning C4390 (empty controlled statement) +#define PJON_IO_PULL_DOWN(P) (void)0 +#endif + +/* Random ----------------------------------------------------------------- */ + +#ifndef PJON_RANDOM +#define PJON_RANDOM(randMax) (int)((1.0 + randMax) * rand() / ( RAND_MAX + 1.0 ) ) +/* Scale rand()'s return value against RAND_MAX using doubles instead of + a pure modulus to have a more distributed result */ +#endif + +#ifndef PJON_RANDOM_SEED +#define PJON_RANDOM_SEED srand +#endif + + +/* Serial ----------------------------------------------------------------- */ + +#ifndef PJON_SERIAL_TYPE +#define PJON_SERIAL_TYPE Serial * +#endif + +#ifndef PJON_SERIAL_AVAILABLE +#define PJON_SERIAL_AVAILABLE(S) S->serialDataAvail() +#endif + +#ifndef PJON_SERIAL_WRITE +#define PJON_SERIAL_WRITE(S, C) S->writeByte(&C) +#endif + +#ifndef PJON_SERIAL_READ +#define PJON_SERIAL_READ(S) S->getByte() +#endif + +#ifndef PJON_SERIAL_FLUSH +#define PJON_SERIAL_FLUSH(S) S->flush() +#endif + + +/* Timing ----------------------------------------------------------------- */ + +#ifndef PJON_DELAY +#define PJON_DELAY delay +#endif + +#ifndef PJON_DELAY_MICROSECONDS +#define PJON_DELAY_MICROSECONDS delayMicroseconds +#endif + +#ifndef PJON_MICROS +#define PJON_MICROS micros +#endif + +#ifndef PJON_MILLIS +#define PJON_MILLIS millis +#endif +#endif diff --git a/hal/transport/PJON/driver/interfaces/WINX86/Serial/Serial.cpp b/hal/transport/PJON/driver/interfaces/WINX86/Serial/Serial.cpp new file mode 100644 index 000000000..7829f8621 --- /dev/null +++ b/hal/transport/PJON/driver/interfaces/WINX86/Serial/Serial.cpp @@ -0,0 +1,150 @@ +/** Serial.cpp + * + * A very simple serial port control class that does NOT require MFC/AFX. + * + * @author Hans de Ruiter, Zbigniew Zasieczny + * + * @version 0.1 -- 28 October 2008 + * @version 0.2 -- 20 April 2017 (Zbigniew Zasieczny) + * - added methods needed by PJON to handle WINX86 abstraction interface + * - modified flush method to use WIN API + * - added initial tests + * - changed timeouts to 1s + * + * + * 11/05/2020 - getByte function simplified, now returns -1 in case of failure + */ + +#if defined(_WIN32) + +#include +using namespace std; + +#include "Serial.h" +#include + +std::wstring s2ws(const std::string& s) +{ + int len; + int slength = (int)s.length() + 1; + len = MultiByteToWideChar(CP_ACP, 0, s.c_str(), slength, 0, 0); + wchar_t* buf = new wchar_t[len]; + MultiByteToWideChar(CP_ACP, 0, s.c_str(), slength, buf, len); + std::wstring r(buf); + delete[] buf; + return r; +}; + + +Serial::Serial(std::string &commPortName, int bd) +{ + std::wstring com_name_ws = s2ws(commPortName); + + commHandle = + CreateFileW( + com_name_ws.c_str(), + GENERIC_READ | GENERIC_WRITE, + 0, + NULL, + OPEN_EXISTING, + 0, + NULL + ); + + if(commHandle == INVALID_HANDLE_VALUE) { + throw("ERROR: Could not open com port"); + } else { + // set timeouts + COMMTIMEOUTS timeouts; + + /* Blocking: + timeouts.ReadIntervalTimeout = MAXDWORD; + timeouts.ReadTotalTimeoutConstant = 0; + timeouts.ReadTotalTimeoutMultiplier = 0; + Non-blocking: + timeouts = { MAXDWORD, 0, 0, 0, 0}; */ + + // Non-blocking with short timeouts + timeouts.ReadIntervalTimeout = 1; + timeouts.ReadTotalTimeoutMultiplier = 1; + timeouts.ReadTotalTimeoutConstant = 1; + timeouts.WriteTotalTimeoutMultiplier = 1; + timeouts.WriteTotalTimeoutConstant = 1; + + DCB dcb; + if(!SetCommTimeouts(commHandle, &timeouts)) { + Serial::~Serial(); + throw("ERROR: Could not set com port time-outs"); + } + + // set DCB; disabling harware flow control; setting 1N8 mode + memset(&dcb, 0, sizeof(dcb)); + dcb.DCBlength = sizeof(dcb); + dcb.BaudRate = bd; + dcb.fBinary = 1; + dcb.fDtrControl = DTR_CONTROL_DISABLE; + dcb.fRtsControl = RTS_CONTROL_DISABLE; + dcb.Parity = NOPARITY; + dcb.StopBits = ONESTOPBIT; + dcb.ByteSize = 8; + + if(!SetCommState(commHandle, &dcb)) { + Serial::~Serial(); + throw("ERROR: Could not set com port parameters"); + } + } +}; + + +Serial::~Serial() +{ + CloseHandle(commHandle); +}; + +int Serial::writeByte(uint8_t *buffer) +{ + DWORD numWritten; + WriteFile(commHandle, buffer, 1, &numWritten, NULL); + return numWritten; +}; + +int16_t Serial::getByte() +{ + uint8_t buff; + if(read(&buff, 1) == -1) { + return -1; + } + return buff; +}; + + +int Serial::read(uint8_t *buffer, int buffLen) +{ + DWORD numRead; + BOOL ret = ReadFile(commHandle, buffer, buffLen, &numRead, NULL); + if(!ret) { + return -1; + } + return numRead; +}; + + +bool Serial::serialDataAvail() +{ + COMSTAT stats; + DWORD error; + ClearCommError(commHandle, &error, &stats); + if(stats.cbInQue > 0) { + return true; + } + return false; +}; + + +void Serial::flush() +{ + PurgeComm(commHandle, PURGE_RXCLEAR | PURGE_TXCLEAR); + return; +}; + +#endif diff --git a/hal/transport/PJON/driver/interfaces/WINX86/Serial/Serial.h b/hal/transport/PJON/driver/interfaces/WINX86/Serial/Serial.h new file mode 100644 index 000000000..a4760527c --- /dev/null +++ b/hal/transport/PJON/driver/interfaces/WINX86/Serial/Serial.h @@ -0,0 +1,74 @@ +/** Serial.h + * + * A very simple serial port control class that does NOT require MFC/AFX. + * + * License: This source code can be used and/or modified without restrictions. + * It is provided as is and the author disclaims all warranties, expressed + * or implied, including, without limitation, the warranties of + * merchantability and of fitness for any purpose. The user must assume the + * entire risk of using the Software. + * + * @author Hans de Ruiter, Zbigniew Zasieczny + * + * @version 0.1 -- 28 October 2008 + * @version 0.2 -- 20 April 2017 (Zbigniew Zasieczny) + * - added methods needed by PJON to handle WINX86 abstraction interface + * + * 11/05/2020 getByte function simplified, now returns -1 in case of failure + */ + +#pragma once + +#if defined(_WIN32) +#include +#include + +typedef std::basic_string tstring; + +class Serial +{ + +private: + HANDLE commHandle; + +public: + Serial( + std::string &commPortName, + int bitRate = 115200 + ); + + virtual ~Serial(); + + /** Writes a single byte (uint8_t) to the serial port. + * + * @param buffer pointer to the buffer containing the bytes + * + * @return int the number of bytes written + */ + int writeByte(uint8_t *buffer); + + /** Reads a string of bytes from the serial port. + * + * @param buffer pointer to the buffer to be written to + * @param buffLen the size of the buffer + * + * @return int the number of bytes read + */ + int read(uint8_t *buffer, int buffLen); + + /** Returns an integer (uint16_t) + * + * @return a value < 256 or -1 in case of failure + */ + int16_t getByte(); + + /** Returns true if there is data available in receive buffer + */ + bool serialDataAvail(); + + /** Flushes everything from the serial port's read buffer + */ + void flush(); +}; + +#endif diff --git a/hal/transport/PJON/driver/interfaces/ZEPHYR/PJON_ZEPHYR_Interface.h b/hal/transport/PJON/driver/interfaces/ZEPHYR/PJON_ZEPHYR_Interface.h new file mode 100644 index 000000000..bc3905a1c --- /dev/null +++ b/hal/transport/PJON/driver/interfaces/ZEPHYR/PJON_ZEPHYR_Interface.h @@ -0,0 +1,104 @@ +#pragma once + +#if defined(__ZEPHYR__) + +#define OUTPUT GPIO_OUTPUT + +#include +#include +#include +#include + +int serial_get_char(struct device* dev); +bool serial_char_available(struct device* dev); +void uart_irq_callback(struct device *dev); +int serial_put_char(struct device* dev, uint8_t byte); +struct device* serial_open(const char* dt_label); +void serial_close(const char* dt_label); +void digitalWrite(int pin, bool state); +void pinMode(int pin, int mode); +void serial_flush(struct device* dev); + +#if defined CONFIG_PJON_STRATEGY_THROUGHSERIAL +#ifndef PJON_ZEPHYR_SEPARATE_DEFINITION +#include "PJON_ZEPHYR_Interface.inl" +#endif +#endif +// deal with randomness + +#ifndef PJON_RANDOM +#define PJON_RANDOM(randMax) (int)((1.0 + randMax) * rand() / ( RAND_MAX + 1.0 )) +#endif + +#ifndef PJON_RANDOM_SEED +#define PJON_RANDOM_SEED srand +#endif + +#ifndef A0 +#define A0 0 +#endif + +#ifndef PJON_ANALOG_READ +#define PJON_ANALOG_READ(P) 0 +#endif + +// delay and timing functions + +#ifndef PJON_DELAY +#define PJON_DELAY(x) (k_sleep(K_MSEC(x))) +#endif + +#ifndef PJON_DELAY_MICROSECONDS +#define PJON_DELAY_MICROSECONDS(x) (k_sleep(K_USEC(x))) +#endif + +#ifndef PJON_MILLIS +#define PJON_MILLIS k_uptime_get +#endif + +#ifndef PJON_MICROS +#define PJON_MICROS 1000*k_uptime_get +#endif + +// serial port handling + +#ifndef PJON_SERIAL_TYPE +#define PJON_SERIAL_TYPE struct device* +#endif + +#ifndef PJON_SERIAL_AVAILABLE +#define PJON_SERIAL_AVAILABLE(S) serial_char_available(S) +#endif + +#ifndef PJON_SERIAL_READ +#define PJON_SERIAL_READ(S) serial_get_char(S) +#endif + +#ifndef PJON_SERIAL_WRITE +#define PJON_SERIAL_WRITE(S, C) serial_put_char(S, C) +#endif + +#ifndef PJON_SERIAL_FLUSH +#define PJON_SERIAL_FLUSH(S) serial_flush(S) +#endif + +// io pin handling (for setting the rs485 txe pin) is not needed since we use the auto-direction +// feature of the rs485 transceiver + +#ifndef PJON_IO_WRITE +#define PJON_IO_WRITE digitalWrite +#endif + +#ifndef PJON_IO_MODE +#define PJON_IO_MODE pinMode +#endif + +#ifndef LOW +#define LOW 0 +#endif + +#ifndef HIGH +#define HIGH 1 +#endif + +#endif diff --git a/hal/transport/PJON/driver/interfaces/ZEPHYR/PJON_ZEPHYR_Interface.inl b/hal/transport/PJON/driver/interfaces/ZEPHYR/PJON_ZEPHYR_Interface.inl new file mode 100644 index 000000000..8158b7712 --- /dev/null +++ b/hal/transport/PJON/driver/interfaces/ZEPHYR/PJON_ZEPHYR_Interface.inl @@ -0,0 +1,128 @@ +#ifndef ZEPHYR + #define ZEPHYR +#endif +#ifndef PJON_ZEPHYR_SEPARATE_DEFINITION + #define PJON_ZEPHYR_SEPARATE_DEFINITION +#endif + +#include "PJON_ZEPHYR_Interface.h" +#include + +#include +#include +#include +#include + +#include +#include +#include + +static struct device* pin_dev = NULL; + +static std::map ringbuffers; + +static struct ring_buf* _get_ring_buf(struct device* dev) +{ + auto search = ringbuffers.find(dev); + struct ring_buf* tmp = nullptr; + if (search != ringbuffers.end()) { + tmp = search->second; + } + return tmp; +} + +struct device* serial_open(const char* dt_label) +{ + struct device* uart = device_get_binding(dt_label); ///@todo multiple uarts for this layer --SA + struct uart_config cfg; + uart_config_get(uart, &cfg); + cfg.data_bits = UART_CFG_DATA_BITS_8; + cfg.stop_bits = UART_CFG_STOP_BITS_1; + uart_configure(uart, &cfg); + + uart_irq_callback_set(uart, uart_irq_callback); + uart_irq_rx_enable(uart); + + uint8_t* buf = (uint8_t*)calloc(1, CONFIG_PJON_MTU); + struct ring_buf* r = (struct ring_buf*)calloc(1, sizeof(struct ring_buf)); + r->size = CONFIG_PJON_MTU, + r->buf.buf8 = buf; + + auto ret = ringbuffers.insert(std::make_pair(uart, r)); + + if (ret.second != true) { + uart = nullptr; + } + + return uart; +} + +void serial_close(const char* dt_label) +{ + struct ring_buf* r = _get_ring_buf(device_get_binding(dt_label)); + free(r->buf.buf8); + free(r); +} + +int serial_get_char(struct device* dev) +{ + size_t ret; + uint8_t b; + struct ring_buf* r = _get_ring_buf(dev); + if (r) { + ret = ring_buf_get(r, &b, sizeof(b)); + } + return b; +} + +bool serial_char_available(struct device* dev) +{ + struct ring_buf* r = _get_ring_buf(dev); + bool b = false; + if (r) { + b = !ring_buf_is_empty(r); + } + return b; +} + +void uart_irq_callback(struct device* dev) +{ + uart_irq_update(dev); + + if (uart_irq_rx_ready(dev)) { + uint8_t tmp; + while (1) { + if (uart_fifo_read(dev, &tmp, 1) == 0) { + break; + } + struct ring_buf* r = _get_ring_buf(dev); + + if (r) { + ring_buf_put(r, &tmp, sizeof(tmp)); + } + } + } +} + +int serial_put_char(struct device* dev, uint8_t b) +{ + uart_poll_out(dev, b); + return 1; +} + +void digitalWrite(int pin, bool state) +{ + gpio_pin_set(pin_dev, pin, state); +} + +void pinMode(int pin, int mode) +{ + pin_dev = device_get_binding(DT_GPIO_LABEL(DT_ALIAS(pjon_txe_pin), gpios)); + + gpio_pin_configure(pin_dev, pin, mode); +} + +void serial_flush(struct device* dev) +{ + k_usleep(1000); +} diff --git a/hal/transport/PJON/driver/strategies/AnalogSampling/AnalogSampling.h b/hal/transport/PJON/driver/strategies/AnalogSampling/AnalogSampling.h new file mode 100644 index 000000000..180baccf1 --- /dev/null +++ b/hal/transport/PJON/driver/strategies/AnalogSampling/AnalogSampling.h @@ -0,0 +1,435 @@ + +/* AnalogSampling data link layer 18/02/2017 + used as a Strategy by the PJON framework + It complies with PJDLS (Padded Jittering Data Link byte Stuffed) + specification v2.0 + + AnalogSampling is designed to sample digital data using analog readings. + It can be effectively used to communicate data wirelessly through any + sort of radiation transceiver. + + The most basic example is to use a couple of visible light LEDs as wireless + transceivers connecting to the A0 pin of 2 Arduino boards. + + Leveraging the interesting characteristics of LEDs: + - Emit photons if electrons are travelling through the junction + - Emit electrons if photons are hitting the junction, thanks to the + photo-electric effect + + It is possibile to use them as wireless (bidirectional) transceivers! + + The obtained range is related to: + - Analog reference (voltage reading resolution) + - LED sensitivity to the signal + - Available current for transmitter + + It is possible to use this strategy with a couple of LEDs and an optic + fiber cable to have a safe EM interference free data link. + + It is possible to use this strategy to also communicate long range + wirelessly using a couple of photodiodes and laser emitters. It may be + necessary to tweak timing constants in Timing.h. + ___________________________________________________________________________ + + Copyright 2010-2020 Giovanni Blu Mitolo gioscarab@gmail.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + +#pragma once + +/* Default reading state threshold: */ + +#ifndef AS_THRESHOLD +#define AS_THRESHOLD 1 +#endif + +/* MODE 1 performance (default): + Transfer speed: 1024Bb or 128B/s + + MODE 2 performance: + Transfer speed: 1361Bb or 170B/s + + MODE 3 performance: + Transfer speed: 3352Bb or 419B/s + + MODE 4 performance: + Transfer speed: 5069Bb or 633B/s + + MODE 5 performance: + Transfer speed: 12658Bb or 1582B/s + ADC prescale 8 (Caution out of specification) + + Set default transmission mode */ +#ifndef AS_MODE +#define AS_MODE 1 +#endif + +// Used to signal communication failure +#define AS_FAIL 65535 +// Used for pin handling +#define AS_NOT_ASSIGNED 255 +// START frame symbol 10010101 - 0x95 - • +#define AS_START 149 +// END frame symbol 11101010 - 0xea - ê +#define AS_END 234 +// ESCAPE symbol 10111011 - 0xBB - » +#define AS_ESC 187 + +#include "Timing.h" + +// Recommended receive time for this strategy, in microseconds +#ifndef AS_RECEIVE_TIME +#define AS_RECEIVE_TIME 1000 +#endif + +class AnalogSampling +{ +public: + + /* Returns the suggested delay related to attempts passed as parameter: */ + + uint32_t back_off(uint8_t attempts) + { + uint32_t result = attempts; + for(uint8_t d = 0; d < AS_BACK_OFF_DEGREE; d++) { + result *= (uint32_t)(attempts); + } + return result; + }; + + + /* Begin method, to be called on initialization: + (returns always true) */ + + bool begin(uint8_t did = 0) + { +#ifdef ARDUINO + // Set ADC clock prescale +#if AS_PRESCALE == 8 + cbi(ADCSRA, ADPS2); + sbi(ADCSRA, ADPS1); + sbi(ADCSRA, ADPS0); +#elif AS_PRESCALE == 16 + sbi(ADCSRA, ADPS2); + cbi(ADCSRA, ADPS1); + cbi(ADCSRA, ADPS0); +#elif AS_PRESCALE == 32 + sbi(ADCSRA, ADPS2); + cbi(ADCSRA, ADPS1); + sbi(ADCSRA, ADPS0); +#endif +#endif + PJON_DELAY(PJON_RANDOM(AS_INITIAL_DELAY) + did); + compute_analog_read_duration(); + _last_byte = receive_byte(); + return true; + }; + + + /* Check if the channel is free for transmission: + If receiving 10 bits no 1s are detected + there is no active transmission */ + + bool can_start() + { + uint32_t time = PJON_MICROS(); + while( + (uint32_t)(PJON_MICROS() - time) <= + (AS_BIT_SPACER + (AS_BIT_WIDTH * 9)) + ) if(receive_byte() != PJON_FAIL) { + return false; + } + PJON_DELAY_MICROSECONDS(PJON_RANDOM(AS_COLLISION_DELAY)); + if((uint16_t)PJON_ANALOG_READ(_input_pin) > threshold) { + return false; + } + return true; + }; + + + /* compute PJON_ANALOG_READ duration: */ + + void compute_analog_read_duration() + { + uint32_t time = PJON_MICROS(); + PJON_ANALOG_READ(_input_pin); + _analog_read_time = (uint32_t)(PJON_MICROS() - time); + }; + + + /* Returns the maximum number of attempts for each transmission: */ + + static uint8_t get_max_attempts() + { + return AS_MAX_ATTEMPTS; + }; + + + /* Returns the recommended receive time for this strategy: */ + + static uint16_t get_receive_time() + { + return AS_RECEIVE_TIME; + }; + + + /* Handle a collision: */ + + void handle_collision() + { + PJON_DELAY_MICROSECONDS(PJON_RANDOM(AS_COLLISION_DELAY)); + }; + + + /* Read a byte from the pin */ + + uint8_t read_byte() + { + uint16_t bit_value = 0; + uint16_t high_bit = 0; + uint16_t low_bit = 0; + uint8_t byte_value = 0; + for(int i = 0; i < 8; i++) { + long time = PJON_MICROS(); + PJON_DELAY_MICROSECONDS((AS_BIT_WIDTH / 2) - AS_READ_DELAY); + bit_value = PJON_ANALOG_READ(_input_pin); + byte_value += (bit_value > threshold) << i; + + high_bit = + (((bit_value > threshold) ? bit_value : high_bit) + high_bit) / 2; + + low_bit = + (((bit_value < threshold) ? bit_value : low_bit) + low_bit) / 2; + + PJON_DELAY_MICROSECONDS( + AS_BIT_WIDTH - (uint32_t)(PJON_MICROS() - time) + ); + } + threshold = (high_bit + low_bit) / 2; + _last_update = PJON_MICROS(); + return byte_value; + }; + + + /* Check if a byte is coming from the pin: + This function is looking for padding bits before a byte. + If value is 1 for more than ACCEPTANCE and after + that comes a 0 probably a byte is coming: + ________ + | Init | + |--------| + |_____ | + | | | | + |1 | |0 | + |__|__|__| + | + ACCEPTANCE */ + + + uint16_t receive_byte() + { + PJON_IO_PULL_DOWN(_input_pin); + if(_output_pin != AS_NOT_ASSIGNED && _output_pin != _input_pin) { + PJON_IO_PULL_DOWN(_output_pin); + } + uint32_t time = PJON_MICROS(); + + if( + (uint32_t)(PJON_MICROS() - _last_update) > + AS_THRESHOLD_DECREASE_INTERVAL + ) { + threshold *= 0.9; + _last_update = PJON_MICROS(); + } + + while( + ((uint16_t)(PJON_ANALOG_READ(_input_pin)) > threshold) && + ((uint32_t)(PJON_MICROS() - time) <= AS_BIT_SPACER) + ); // Do nothing + + time = PJON_MICROS() - time; + if(time < AS_BIT_SPACER * 0.75) { + return AS_FAIL; + } + if(time <= AS_BIT_SPACER * 1.25) { + PJON_DELAY_MICROSECONDS(AS_BIT_WIDTH); + return read_byte(); + } + return AS_FAIL; + }; + + + /* Receive byte response */ + + uint16_t receive_response() + { + PJON_IO_PULL_DOWN(_input_pin); + if(_output_pin != AS_NOT_ASSIGNED && _output_pin != _input_pin) { + PJON_IO_WRITE(_output_pin, LOW); + } + uint16_t response = AS_FAIL; + uint32_t time = PJON_MICROS(); + while( + (response != PJON_ACK) && + (uint32_t)(PJON_MICROS() - AS_RESPONSE_TIMEOUT) <= time + ) { + response = receive_byte(); + } + return response; + }; + + + /* Receive a frame: */ + + uint16_t receive_frame(uint8_t *data, uint16_t max_length) + { + uint16_t result; + // No initial flag, byte-stuffing violation + if(max_length == PJON_PACKET_MAX_LENGTH) + if( + (receive_byte() != AS_START) || + (_last_byte == AS_ESC) + ) { + return AS_FAIL; + } + result = receive_byte(); + if(result == AS_FAIL) { + return AS_FAIL; + } + // Unescaped START byte stuffing violation + if(result == AS_START) { + return AS_FAIL; + } + if(result == AS_ESC) { + result = receive_byte(); + // Escaping byte-stuffing violation + if((result != AS_START) && (result != AS_ESC) && (result != AS_END)) { + return AS_FAIL; + } + result ^= AS_ESC; + } + // No end flag, byte-stuffing violation + if(max_length == 1 && receive_byte() != AS_END) { + return AS_FAIL; + } + *data = result; + return 1; + }; + + + /* Every byte is prepended with 2 synchronization padding bits. The first + is a longer than standard logic 1 followed by a standard logic 0. + __________ ___________________________ + | SyncPad | Byte | + |______ |___ ___ _____ | + | | | | | | | | | | + | | 1 | 0 | 1 | 0 0 | 1 | 0 | 1 1 | 0 | + |_|____|___|___|_____|___|___|_____|___| + | + ACCEPTANCE + + The reception tecnique is based on finding a logic 1 as long as the + first padding bit within a certain threshold, synchronizing to its + falling edge and checking if it is followed by a logic 0. If this + pattern is recognised, reception starts, if not, interference, + synchronization loss or simply absence of communication is + detected at byte level. */ + + void send_byte(uint8_t b) + { + PJON_IO_WRITE(_output_pin, HIGH); + PJON_DELAY_MICROSECONDS(AS_BIT_SPACER); + PJON_IO_WRITE(_output_pin, LOW); + PJON_DELAY_MICROSECONDS(AS_BIT_WIDTH); + for(uint8_t mask = 0x01; mask; mask <<= 1) { + PJON_IO_WRITE(_output_pin, b & mask); + PJON_DELAY_MICROSECONDS(AS_BIT_WIDTH); + } + }; + + + /* Send byte response to package transmitter */ + + void send_response(uint8_t response) + { + PJON_DELAY_MICROSECONDS(AS_BIT_WIDTH); + PJON_IO_PULL_DOWN(_input_pin); + PJON_IO_MODE(_output_pin, OUTPUT); + send_byte(response); + PJON_IO_PULL_DOWN(_output_pin); + }; + + + /* Send a frame: */ + + void send_frame(uint8_t *data, uint16_t length) + { + PJON_IO_MODE(_output_pin, OUTPUT); + // Add frame flag + send_byte(AS_START); + for(uint16_t b = 0; b < length; b++) + if( // Byte-stuffing + (data[b] == AS_START) || + (data[b] == AS_ESC) || + (data[b] == AS_END) + ) { + send_byte(AS_ESC); + send_byte(data[b] ^ AS_ESC); + } else { + send_byte(data[b]); + } + send_byte(AS_END); + PJON_IO_PULL_DOWN(_output_pin); + }; + + + /* Set the communicaton pin: */ + + void set_pin(uint8_t pin) + { + PJON_IO_PULL_DOWN(pin); + _input_pin = pin; + _output_pin = pin; + }; + + + /* Set a pair of communication pins: */ + + void set_pins( + uint8_t input_pin = AS_NOT_ASSIGNED, + uint8_t output_pin = AS_NOT_ASSIGNED + ) + { + PJON_IO_PULL_DOWN(input_pin); + PJON_IO_PULL_DOWN(output_pin); + _input_pin = input_pin; + _output_pin = output_pin; + }; + + + /* Set the threshold analog value between a LOW and a HIGH read: */ + + void set_threshold(uint16_t value) + { + threshold = value; + }; + + + uint16_t threshold = AS_THRESHOLD; +private: + uint8_t _last_byte; + uint16_t _analog_read_time; + uint8_t _input_pin; + uint8_t _output_pin; + uint32_t _last_update; +}; diff --git a/hal/transport/PJON/driver/strategies/AnalogSampling/README.md b/hal/transport/PJON/driver/strategies/AnalogSampling/README.md new file mode 100644 index 000000000..2d009f07e --- /dev/null +++ b/hal/transport/PJON/driver/strategies/AnalogSampling/README.md @@ -0,0 +1,78 @@ +## AnalogSampling + +| Medium | Pins used | Inclusion | +|--------|-----------|--------------------| +| Light pulses over air or optic-fibre | 1 or 2 | `#include `| + +The `AnalogSampling` strategy is a software implementation of [PJDLS](/src/strategies/AnalogSampling/specification/PJDLS-specification-v2.0.md), it is designed to communicate data wirelessly using light impulses and its sampling technique is based on analog readings. This strategy is able to use a single LED for both photo-emission and photo-reception phases providing with wireless half-duplex connectivity between devices with a range of up to 5 meters. Most appliances have at least a useless energy consuming LED on board, right? Thanks to this strategy that can be used for communication. + +`AnalogSampling` can also be used with separate emitter and receiver pins enabling cheap long range wireless or optic-fibre communication using standard photo-diodes, light-emitting diodes or laser diodes. The proposed circuit, technique and codebase were originally implemented in 2011, see the first [video documented experiment](https://www.youtube.com/watch?v=-Ul2j6ixbmE). Take a look at the [video introduction](https://www.youtube.com/watch?v=1BeGYMjg-DI) for a brief showcase of its features. + +### Compatibility +| MCU | Clock | Supported pins | Supported modes | +| ---------------- |------ | ---------------- | --------------- | +| ATmega88/168/328 (Duemilanove, Uno, Nano, Pro) | 16MHz | A0, A1, A2, A3, A4, A5 | `1`, `2`, `3`, `4`, `5` | +| ATmega2560 (Mega, Mega nano) | 16MHz | A0, A1, A2, A3, A4, A5 | `1`, `2`, `3` | + +### Performance +`AnalogSampling` works with the following communication modes: +- `1` runs at 1024Bd or 128B/s (`AS_PRESCALE` 128) +- `2` runs at 1361Bd or 170B/s (`AS_PRESCALE` 128) +- `3` runs at 3773Bb or 471B/s (`AS_PRESCALE` 32) +- `4` runs at 5547Bb or 639B/s (`AS_PRESCALE` 16) +- `5` runs at 12658Bd or 1528B/s (`AS_PRESCALE` 8) + +Caution, mode `5` sets the ADC clock prescale to a higher rate than the manufacturer recommends as maximum ADC sample rate (prescale 16). + +### What can be done? +The most basic example is to connect two devices using a couple of visible light LEDs used as wireless transceivers. + +![PJON AnalogSampling LED wireless communication](http://www.pjon.org/assets/images/PJON-AnalogSampling-half-duplex-led-communication.png) + +Leveraging of the interesting features of LEDs: +- Emit light if powered by electricity +- Emit a small but detectable amount of electricity if hit by light (photo-electric effect) + +It is possible to use LEDs as wireless (bidirectional) transceivers. This means that wireless half-duplex connectivity can be provided by a single LED per device. See the [LED selection](documentation/LED-selection.md) guide to know more about how to choose the right LEDs and keep in mind that is necessary to add a 75K-5MΩ pull-down resistor connecting the pin used with ground to reduce the LED capacitance and externally induced interference and that depending on the voltage level used LEDs could be overpowered, add a current limiting resistor if required. + +`AnalogSampling` can be used to experiment with short range infrared or visible light communication (remote control, robot swarms, data streaming using lighting), medium range using light sources (cars transmitting data through front and backlights) or long range laser communication (data between ground and LEO). + +### Configuration +Before including the library it is possible to configure `AnalogSampling` using predefined constants: + +| Constant | Purpose | Supported value | +| ------------------------- |------------------------------------ | ------------------------------------------ | +| `AS_MODE` | Data transmission mode | 1, 2, 3, 4, 5 | +| `AS_RESPONSE_TIMEOUT` | Maximum response time-out | Duration in microseconds (15000 by default) | +| `AS_BACK_OFF_DEGREE` | Maximum back-off exponential degree | Numeric value (5 by default) | +| `AS_MAX_ATTEMPTS` | Maximum transmission attempts | Numeric value (10 by default) | +| `AS_PRESCALE` | Set ADC pre-scaler | 8, 16, 32 | + +Use `PJONAnalogSampling` to instantiate a PJON object ready to communicate using `AnalogSampling` strategy. All the other necessary information is present in the general [Documentation](/documentation). +```cpp +#include + +PJONAnalogSampling bus; + +void setup() { + // Set the pin A0 as the communication pin + bus.strategy.set_pin(A0); + + // Set pin A0 as input pin and pin 12 as output pin + bus.strategy.set_pins(A0, 12); + + // Set threshold (default value AS_THRESHOLD) + bus.strategy.set_threshold(AS_THRESHOLD); +} +``` +After the PJON object is defined with its strategy it is possible to set the communication pin accessing to the strategy present in the PJON instance. + +### Known issues +- Direct sunlight or other light sources can affect receiver's sensitivity and maximum communication range +- Long wires can degrade performance +- Depending on the power supply voltage, LEDs could be overpowered, add a current limiting resistor if required +- Oscilloscope's probe acting as a pull down resistor influences results and the required pull down resistor's value +- A pull-down resistor is required to obtain optimal performance, see above + +### Safety warning +In all cases, when installing or maintaining a PJON network, extreme care must be taken to avoid any danger. When working with an [AnalogSampling](/src/strategies/AnalogSampling) LED or laser based setup safety glasses must be worn and transceivers must be operated cautiously to avoid potential eye injuries. Consider that with [AnalogSampling](/src/strategies/AnalogSampling) all LEDs that are physically connected to an ADC may be used maliciously to both download or upload data wirelessly, effectively circumventing many air-gapping techniques. diff --git a/hal/transport/PJON/driver/strategies/AnalogSampling/Timing.h b/hal/transport/PJON/driver/strategies/AnalogSampling/Timing.h new file mode 100644 index 000000000..075e7f85e --- /dev/null +++ b/hal/transport/PJON/driver/strategies/AnalogSampling/Timing.h @@ -0,0 +1,212 @@ + +/* PJON AnalogSampling strategy Transmission Timing table + Copyright 2018, Giovanni Blu Mitolo All rights reserved. + __________________________________________________________ + | MODE 1 | Transmission speed 1024Bb - 128B/s | + | ADC prescale 128 | | + |-------------------|--------------------------------------| + | MODE 2 | Transmission speed 1361Bb - 170B/s | + | ADC prescale 128 | | + |-------------------|--------------------------------------| + | MODE 3 | Transmission speed 3773Bb - 471B/s | + | ADC prescale 32 | | + |-------------------|--------------------------------------| + | MODE 4 | Transmission speed 5547Bb - 639B/s | + | ADC prescale 16 | | + |-------------------|--------------------------------------| + | MODE 5 | Transmission speed 12658Bb - 1582B/s | + | ADC prescale 8 | Caution, ADC clocked faster than | + | | manifacturer raccomends as maximum | + | | ADC sample rate (prescale 16) | + |___________________|______________________________________| + + All benchmarks are obtained with NetworkAnalysis and SpeedTest examples. */ + +#pragma once + +#if defined(__AVR_ATmega88__) || defined(__AVR_ATmega168__) || \ + defined(__AVR_ATmega328__) || defined(__AVR_ATmega328P__) +#if AS_MODE == 1 +#if F_CPU == 16000000L +#define AS_BIT_WIDTH 750 +#define AS_BIT_SPACER 1050 +#define AS_READ_DELAY 0 +#endif +#elif AS_MODE == 2 +#if F_CPU == 16000000L +#define AS_BIT_WIDTH 572 +#define AS_BIT_SPACER 728 +#define AS_READ_DELAY 0 +#endif +#elif AS_MODE == 3 +#ifndef AS_PRESCALE +#define AS_PRESCALE 32 +#endif +#if F_CPU == 16000000L +#define AS_BIT_WIDTH 188 +#define AS_BIT_SPACER 428 +#define AS_READ_DELAY 0 +#endif +#elif AS_MODE == 4 +#ifndef AS_PRESCALE +#define AS_PRESCALE 16 +#endif +#if F_CPU == 16000000L +#define AS_BIT_WIDTH 128 +#define AS_BIT_SPACER 290 +#define AS_READ_DELAY 0 +#endif +#elif AS_MODE == 5 +#ifndef AS_PRESCALE +#define AS_PRESCALE 8 +#endif +#if F_CPU == 16000000L +#define AS_BIT_WIDTH 56 +#define AS_BIT_SPACER 128 +#define AS_READ_DELAY 16 +#endif +#endif +#endif + +/* ATmega1280/2560 - Arduino Mega/Mega-nano ------------------------------- */ +#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) +#if AS_MODE == 1 +#if F_CPU == 16000000L +/* Standard timing is applied below */ +#endif +#elif AS_MODE == 2 +#if F_CPU == 16000000L +/* Standard timing is applied below */ +#endif +#elif AS_MODE == 3 +#ifndef AS_PRESCALE +#define AS_PRESCALE 32 +#endif +#if F_CPU == 16000000L +/* Standard timing is applied below */ +#endif +#elif AS_MODE == 4 +#ifndef AS_PRESCALE +#define AS_PRESCALE 16 +#endif +#if F_CPU == 16000000L +/* Standard timing is applied below */ +#endif +#elif AS_MODE == 5 +#ifndef AS_PRESCALE +#define AS_PRESCALE 8 +#endif +#if F_CPU == 16000000L +/* TODO - define dedicated timing */ +#endif +#endif +#endif + +/* ATmega16/32U4 - Arduino Leonardo/Micro --------------------------------- */ +#if defined(__AVR_ATmega16U4__) || defined(__AVR_ATmega32U4__) +#if AS_MODE == 1 +/* TODO - define dedicated timing */ +#endif +#endif + +/* NodeMCU, generic ESP8266 ----------------------------------------------- */ +#if defined(ESP8266) +#if AS_MODE == 1 +#if F_CPU == 80000000L +/* TODO - define dedicated timing */ +#endif +#endif +#endif + +/* Fallback to standard timing if not previously defined: */ + +#if AS_MODE == 1 +#ifndef AS_BIT_WIDTH +#define AS_BIT_WIDTH 750 +#endif +#ifndef AS_BIT_SPACER +#define AS_BIT_SPACER 1050 +#endif +#ifndef AS_READ_DELAY +#define AS_READ_DELAY 0 +#endif +#elif AS_MODE == 2 +#ifndef AS_BIT_WIDTH +#define AS_BIT_WIDTH 572 +#endif +#ifndef AS_BIT_SPACER +#define AS_BIT_SPACER 728 +#endif +#ifndef AS_READ_DELAY +#define AS_READ_DELAY 0 +#endif +#elif AS_MODE == 3 +#ifndef AS_BIT_WIDTH +#define AS_BIT_WIDTH 188 +#endif +#ifndef AS_BIT_SPACER +#define AS_BIT_SPACER 428 +#endif +#ifndef AS_READ_DELAY +#define AS_READ_DELAY 0 +#endif +#elif AS_MODE == 4 +#ifndef AS_BIT_WIDTH +#define AS_BIT_WIDTH 128 +#endif +#ifndef AS_BIT_SPACER +#define AS_BIT_SPACER 290 +#endif +#ifndef AS_READ_DELAY +#define AS_READ_DELAY 0 +#endif +#elif AS_MODE == 5 +#ifndef AS_BIT_WIDTH +#define AS_BIT_WIDTH 56 +#endif +#ifndef AS_BIT_SPACER +#define AS_BIT_SPACER 128 +#endif +#ifndef AS_READ_DELAY +#define AS_READ_DELAY 16 +#endif +#endif + +/* Synchronous acknowledgement response timeout. (15 milliseconds default). + If (latency + CRC computation) > AS_RESPONSE_TIMEOUT synchronous + acknowledgement reliability could be affected or disrupted higher + AS_RESPONSE_TIMEOUT if necessary. */ + +#ifndef AS_RESPONSE_TIMEOUT +#define AS_RESPONSE_TIMEOUT 15000 +#endif + +/* Maximum initial delay in milliseconds: */ + +#ifndef AS_INITIAL_DELAY +#define AS_INITIAL_DELAY 1000 +#endif + +/* Maximum delay in case of collision in microseconds: */ + +#ifndef AS_COLLISION_DELAY +#define AS_COLLISION_DELAY 64 +#endif + +/* Maximum transmission attempts */ + +#ifndef AS_MAX_ATTEMPTS +#define AS_MAX_ATTEMPTS 10 +#endif + +/* Back-off exponential degree */ + +#ifndef AS_BACK_OFF_DEGREE +#define AS_BACK_OFF_DEGREE 5 +#endif + +/* Threshold decrease interval (10 millis) */ + +#ifndef AS_THRESHOLD_DECREASE_INTERVAL +#define AS_THRESHOLD_DECREASE_INTERVAL 10000 +#endif diff --git a/hal/transport/PJON/driver/strategies/AnalogSampling/documentation/LED-selection.md b/hal/transport/PJON/driver/strategies/AnalogSampling/documentation/LED-selection.md new file mode 100644 index 000000000..0f34f89b8 --- /dev/null +++ b/hal/transport/PJON/driver/strategies/AnalogSampling/documentation/LED-selection.md @@ -0,0 +1,38 @@ + +## LED selection +Not all LEDs behave as good as others so a preliminary evaluation of a set of different products is suggested: + +1. Position a couple of identical LEDs on a breadboard aiming at each other +2. Connect one channel of the oscilloscope to the positive lead of one LED +3. Power the connected LED with a 500Hz square wave +4. Connect oscilloscope's remaining channel to the other LED's positive lead +5. Connect all grounds together + +If you don't have a square wave generator you can use an Arduino: +```cpp +digitalWrite(12, HIGH); +delay(1); +digitalWrite(12, LOW); +delay(1); +``` + +Looking at the 2 channels it should be observed: + +- The Transmitter's channel showing a crisp 5v signal +- The Receiver's channel showing a lower voltage signal with transitions slopes + +Testing different LEDs with the same conditions shows that some produce a higher or lower voltage and transitions that are steeper or slower and more gradual. To obtain the best performance it is required to find a LED with the following characteristics: +- Highest voltage produced when hit by light +- Fastest and steepest transitions between states + +The picture below shows the [KCL5587S](https://datasheet.octopart.com/KCL5587S-Kodenshi-datasheet-62058055.pdf) that is evidently not the LED we are looking for. + +![AnalogSampling PJDLS bad LED](../images/AnalogSampling_PJDLS_Bad_LED.jpg) + +The [L-53SF4C](https://www.rapidonline.com/pdf/55-9204_v1.pdf) instead is able to run flawlessly at MODE 3 (3773Bb or 471B/s): + +![AnalogSampling PJDLS good LED](../images/AnalogSampling_PJDLS_Good_LED.jpg) + +The picture below shows a bidirectional exchange where both packet and acknowledgement are clearly visible: + +![AnalogSampling PJDLS bidirectional exchange](../images/AnalogSampling_PJDLS_LED_Transceiver.jpg) diff --git a/hal/transport/PJON/driver/strategies/AnalogSampling/images/AnalogSampling_PJDLS_Bad_LED.jpg b/hal/transport/PJON/driver/strategies/AnalogSampling/images/AnalogSampling_PJDLS_Bad_LED.jpg new file mode 100644 index 000000000..6a122cf00 Binary files /dev/null and b/hal/transport/PJON/driver/strategies/AnalogSampling/images/AnalogSampling_PJDLS_Bad_LED.jpg differ diff --git a/hal/transport/PJON/driver/strategies/AnalogSampling/images/AnalogSampling_PJDLS_Good_LED.jpg b/hal/transport/PJON/driver/strategies/AnalogSampling/images/AnalogSampling_PJDLS_Good_LED.jpg new file mode 100644 index 000000000..efbd15d0b Binary files /dev/null and b/hal/transport/PJON/driver/strategies/AnalogSampling/images/AnalogSampling_PJDLS_Good_LED.jpg differ diff --git a/hal/transport/PJON/driver/strategies/AnalogSampling/images/AnalogSampling_PJDLS_LED_Transceiver.jpg b/hal/transport/PJON/driver/strategies/AnalogSampling/images/AnalogSampling_PJDLS_LED_Transceiver.jpg new file mode 100644 index 000000000..46d8e751f Binary files /dev/null and b/hal/transport/PJON/driver/strategies/AnalogSampling/images/AnalogSampling_PJDLS_LED_Transceiver.jpg differ diff --git a/hal/transport/PJON/driver/strategies/AnalogSampling/specification/PJDLS-specification-v2.0.md b/hal/transport/PJON/driver/strategies/AnalogSampling/specification/PJDLS-specification-v2.0.md new file mode 100644 index 000000000..663257fb1 --- /dev/null +++ b/hal/transport/PJON/driver/strategies/AnalogSampling/specification/PJDLS-specification-v2.0.md @@ -0,0 +1,77 @@ +### Specifications index + +#### Network layer +- [PJON (Padded Jittering Operative Network) v4.0](/specification/PJON-protocol-specification-v4.0.md) +- [Network services list](/specification/PJON-network-services-list.md) +#### Data link layer +- [PJDL (Padded Jittering Data Link) v5.0](/src/strategies/SoftwareBitBang/specification/PJDL-specification-v5.0.md) +- [PJDLR (Padded Jittering Data Link over Radio) v3.0](/src/strategies/OverSampling/specification/PJDLR-specification-v3.0.md) +- **[PJDLS (Padded Jittering Data Link byte Stuffed) v2.0](/src/strategies/AnalogSampling/specification/PJDLS-specification-v2.0.md)** +- [TSDL (Tardy Serial Data Link) v3.0](/src/strategies/ThroughSerial/specification/TSDL-specification-v3.0.md) +- [SFSP (Secure Frame Separation Protocol) v1.0](/specification/SFSP-frame-separation-specification-v1.0.md) + +--- + +## PJDLS v2.0 +``` +Invented by Giovanni Blu Mitolo +Originally published: 20/11/2017 +Latest revision: 31/10/2018 +Related implementation: /src/strategies/AnalogSampling/ +Compliant versions: PJON v10.0 and following +Released into the public domain +``` + +PJDLS (Padded Jittering Data Link byte Stuffed) is an asynchronous serial data link for low-data-rate applications that supports one or many to many communication optimized for optical wireless communication. Frame separation is obtained with the use of [SFSP (Secure Frame Separation Protocol) v1.0](/specification/SFSP-frame-separation-specification-v1.0.md). PJDLS can be easily implemented on limited microcontrollers with low clock accuracy and can operate directly using one or two input-output pins. + +### Communication modes +The proposed communication modes are the result of years of testing and optimization for light pulses communication and have been selected to be easily supported also by limited microcontrollers. + +| MODE | Bit timing | Sync bit timing | Pad-bit ratio | Speed | +| ---- | ---------- | --------------- | ------------- | ------------------- | +| 1 | 750 | 1050 | 1.4 | 128B/s - 1024Bd | +| 2 | 572 | 728 | 1.2727 | 170B/s - 1361Bd | +| 3 | 188 | 428 | 2.2765 | 471B/s - 3773Bd | +| 4 | 128 | 290 | 2.2656 | 639B/s - 5547Bd | +| 5 | 56 | 128 | 2.2857 | 1582B/s - 12658Bd | + +Binary timing durations are expressed in microseconds. + +### Medium access control +PJDLS specifies a variation of the carrier-sense, non-persistent random multiple access method (non-persistent CSMA). Devices can detect an ongoing transmission for this reason collisions can only occur in multi-master mode when 2 or more devices start to transmit at the same time. When a collision occurs it can be detected by the receiver because of synchronization loss. + +### Byte transmission +Byte transmission is composed by 10 bits, the first two are called synchronization pad and are used to obtain sampling synchronization. The synchronization pad is composed by a logic 1 padding bit longer than data bits and a logic 0 data bit. The following 8 data bits contain information in LSB-first (least significant bit first) order. + +The reception technique is based on 3 steps: +1. Find a high bit which duration is equal to or acceptably shorter than a high padding bit +2. Synchronize to its falling edge +3. Ensure it is followed by a logic 0 data bit + +If this pattern is detected data reception starts, if not, interference, synchronization loss or simply absence of communication is detected. +```cpp + ___________ ___________________________ +| SYNC-PAD | DATA | +|_______ |___ ___ _____ | +| | | | | | | | | | +| | 1 | 0 | 1 | 0 0 | 1 | 0 | 1 1 | 0 | +|__|____|___|___|_____|___|___|_____|___| + | +Minimum acceptable padding bit duration +``` + +### Frame transmission +Before a frame transmission the communication medium is analysed, if any data is received communication is detected and collision is avoided, if logic 0 is detected for a duration longer than the response time-out plus a small random time, data is transmitted encapsulated in a [SFSP (Secure Frame Separation Protocol) v1.0](/specification/SFSP-frame-separation-specification-v1.0.md) frame. + +### Synchronous response +A frame transmission in both master-slave and multi-master modes can be optionally followed by a synchronous response of its recipient, all devices must use the same response time-out to avoid collisions. The acknowledgment reception phase must be shorter than the response time-out to be successful. + +```cpp +Transmission Response + _______ ______ ______ _____ _____ +| START || BYTE || BYTE || END | CRC COMPUTATION | ACK | +|-------||------||------||-----|-----------------|-----| +| 149 || H || I || 234 | LATENCY | 6 | +|_______||______||______||_____| |_____| +``` +The required response time-out for a given application can be determined practically transmitting the longest supported frame with the farthest physical distance between the two devices. The highest interval between packet transmission and acknowledgement measured plus a small margin is the correct time-out that should exclude acknowledgement losses. diff --git a/hal/transport/PJON/driver/strategies/AnalogSampling/specification/obsolete/PJDLS-specification-v1.0.md b/hal/transport/PJON/driver/strategies/AnalogSampling/specification/obsolete/PJDLS-specification-v1.0.md new file mode 100644 index 000000000..3d42d6d32 --- /dev/null +++ b/hal/transport/PJON/driver/strategies/AnalogSampling/specification/obsolete/PJDLS-specification-v1.0.md @@ -0,0 +1,94 @@ +- PJON (Padded Jittering Operative Network) Protocol specification: +[v2.0](/specification/PJON-protocol-specification-v2.0.md) +- Acknowledge specification: [v1.0](/specification/PJON-protocol-acknowledge-specification-v1.0.md) +- Dynamic addressing specification: [v1.0](/specification/PJON-dynamic-addressing-specification-v1.0.md) +- PJDL (Padded Jittering Data Link) specification: +[PJDL v2.0](/src/strategies/SoftwareBitBang/specification/PJDL-specification-v2.0.md) - [PJDLR v2.0](/src/strategies/OverSampling/specification/PJDLR-specification-v2.0.md) - **[PJDLS v1.0](/src/strategies/AnalogSampling/specification/PJDLS-specification-v1.0.md)** +- TSDL (Tardy Serial Data Link) specification: [TSDL v1.0](/src/strategies/ThroughSerial/specification/TSDL-specification-v1.0.md) + +```cpp +/* +Milan, Italy +Originally published: 02/10/2017 +PJDLS (Padded Jittering Data Link byte Stuffed) v1.0 +Invented by Giovanni Blu Mitolo, +released into the public domain + +Related implementation: /src/strategies/AnalogSampling/ +Compliant versions: PJON v9.0 and following + +Changelog: +- Added frame separation +- Added communication modes specification +*/ +``` +### PJDLS v1.0 +PJDLS (Padded Jittering Data Link byte Stuffed) is a simplex or half-duplex data link layer, that can be easily software emulated, enabling one or many to many communication in both master-slave and multi-master configuration, optimized for use cases where high quality synchronization cannot be achieved. Frame separation is obtained with the use of start and end flags along with byte stuffing. It has been engineered to have limited minimum requirements, and to be efficiently executed on limited microcontrollers with poor clock accuracy. No additional hardware is required to apply PJDLS, and, being implemented in c++, in less than 350 lines of code, it is easily portable to many different architectures. + +#### Basic concepts +* Define a synchronization pad initializer to identify a byte +* Use synchronization pad's falling edge to achieve byte level synchronization +* Support frame separation using `START`, `END` flags and byte stuffing +* Detect interference or absence of communication at byte level +* Support collision avoidance +* Support 1 byte synchronous response to frame + +#### Byte transmission +Each byte is prepended with a synchronization pad and transmission occurs LSB-first. The first bit is a longer than standard logic 1 followed by a standard logic 0. The reception method is based on finding a logic 1 as long as the first padding bit within a certain threshold, synchronizing to its falling edge and checking if it is followed by a logic 0. If this pattern is detected, reception starts, if not, interference, synchronization loss or simply absence of communication is detected at byte level. +```cpp + __________ ___________________________ +| SyncPad | Byte | +|______ |___ ___ _____ | +| | | | | | | | | | +| | 1 | 0 | 1 | 0 0 | 1 | 0 | 1 1 | 0 | +|__|___|___|___|_____|___|___|_____|___| + | +Minimum acceptable HIGH padding bit duration +``` +Padding bits add a certain overhead but are reducing the need of precise timing because synchronization is renewed every byte. The first padding bit duration is the synchronization timeframe the receiver has to receive a byte. If the length of the first padding bit is less than the minimum acceptable duration, the received signal is considered interference. + +#### Frame transmission +Before a frame transmission, the channel is analysed, if ongoing communication is detected collision is avoided, if free for a duration longer than the time-in before transmission, frame transmission starts with `START` flag, followed by data bytes, if necessary escaped with `ESC` flag and terminates the frame with an `END` flag. +```cpp + ______________________________ + | DATA 1-65535 bytes | + _______ |______ _____ _______ ______| _____ +| START | | BYTE || ESC || START || BYTE | | END | +|-------| |------||-----||-------||------| |-----| +| 149 | | 23 || 76 || 149 || 52 | | 234 | +|_______| |______||_____||_______||______| |_____| + | + Flags inside data are escaped + +START: 149 - 10010101 - 0x95 - • +END: 234 - 11101010 - 0xea - ê +ESC: 187 - 10111011 - 0xBB - » +``` +`START` and `END` flag bytes are special characters that signal when a frame begins and ends. +Whenever any of the special character appears in the data, transmitter inserts a special `ESC` character before it, that will be ignored and excluded from data during the reception process. Any corrupted special character or data byte causes the receiver to discard the frame and be ready to receive the next one nominally. + +#### Synchronous response +A frame transmission can be optionally followed by a synchronous response by its recipient. This feature is available for both master-slave and multi-master. In multi-master configuration the maximum acceptable acknowledgement overall response time must be less than the initial channel analysis duration before frame transmission. +```cpp +Transmission Response + _______ ______ ______ _____ _____ +| START || BYTE || BYTE || END | CRC COMPUTATION | ACK | +|-------||------||------||-----|-----------------|-----| +| 149 || H || I || 234 | LATENCY | 6 | +|_______||______||______||_____| |_____| +``` + +In master-slave configuration the maximum time dedicated to potential acknowledgement reception it is defined by the use case constraints like maximum packet length and latency or physical distance between devices. + +#### Communication modes +The proposed communication modes are the result of years of testing and optimization for light pulses communication and have been selected to be easily supported also by limited microcontrollers. + +| MODE | Bit timing | Sync bit timing | Pad-bit ratio | Speed | +| ---- | ---------- | --------------- | ------------- | ------------------- | +| 1 | 750 | 1050 | 1.4 | 128B/s - 1024Bb | +| 2 | 572 | 728 | 1.2727 | 170B/s - 1361Bb | +| 3 | 188 | 428 | 2.2765 | 471B/s - 3773Bb | +| 4 | 128 | 290 | 2.2656 | 639B/s - 5547Bb | +| 5 | 56 | 128 | 2.2857 | 1582B/s - 12658Bb | + +Binary timing durations are expressed in microseconds. diff --git a/hal/transport/PJON/driver/strategies/Any/Any.h b/hal/transport/PJON/driver/strategies/Any/Any.h new file mode 100644 index 000000000..db9571810 --- /dev/null +++ b/hal/transport/PJON/driver/strategies/Any/Any.h @@ -0,0 +1,121 @@ + +/* Any strategy + This strategy includes virtual inheritance and let a PJON object switch + from a strategy to another when required or a collection of PJON objects + with different strategies to be treated dynamically. + + Proposed and developed by Fred Larsen + ___________________________________________________________________________ + + Copyright 2010-2020 Giovanni Blu Mitolo gioscarab@gmail.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + +#pragma once + +#include "StrategyLinkBase.h" +#include "StrategyLink.h" + +class Any +{ +public: + StrategyLinkBase *s = NULL; + + /* Set a pointer to the StrategyLink to be used. + It will not be freed by this class: */ + + void set_link(StrategyLinkBase *strategy_link) + { + s = strategy_link; + } + + + /* Returns delay related to the attempts passed as parameter: */ + + uint32_t back_off(uint8_t attempts) + { + return s->back_off(attempts); + } + + + /* Begin method, to be called on initialization: */ + + bool begin(uint8_t did = 0) + { + return s->begin(did); + } + + + /* Check if the channel is free for transmission */ + + bool can_start() + { + return s->can_start(); + }; + + + /* Returns the maximum number of attempts for each transmission: */ + + uint8_t get_max_attempts() + { + return s->get_max_attempts(); + } + + + /* Returns the recommended receive time for this strategy: */ + + uint16_t get_receive_time() + { + return s->get_receive_time(); + } + + + /* Handle a collision: */ + + void handle_collision() + { + s->handle_collision(); + }; + + + /* Receive a frame: */ + + uint16_t receive_frame(uint8_t *data, uint16_t max_length) + { + return s->receive_frame(data, max_length); + } + + + /* Receive byte response: */ + + uint16_t receive_response() + { + return s->receive_response(); + } + + + /* Send byte response to package transmitter: */ + + void send_response(uint8_t response) + { + s->send_response(response); + } + + + /* Send a frame: */ + + void send_frame(uint8_t *data, uint16_t length) + { + s->send_frame(data, length); + } +}; diff --git a/hal/transport/PJON/driver/strategies/Any/README.md b/hal/transport/PJON/driver/strategies/Any/README.md new file mode 100644 index 000000000..4e97a71f8 --- /dev/null +++ b/hal/transport/PJON/driver/strategies/Any/README.md @@ -0,0 +1,31 @@ +## Any + +| Medium | Pins used | Inclusion | +|--------|-----------|--------------------| +| Any | NA | `#include ` | + + +The `Any` strategy includes virtual inheritance and let PJON objects change from a strategy to another after instantiation or a collection of PJON objects with different strategies to be treated agnostically. + +### How to use Any +Define a `StrategyLink` template class passing the desired strategy, then pass the type `Any` as PJON template parameter to instantiate a PJON object ready to communicate using this strategy. +```cpp +StrategyLink link; +PJON bus; +``` +Call the `set_link` method passing the `StrategyLink` instance: +```cpp +#include + +StrategyLink link; +PJONAny bus; + +void setup() { + Serial.begin(9600); + bus.strategy.set_link(&link); +} +``` + +See [MultiStrategyLink](../../examples/ARDUINO/Local/Any/MultiStrategyLink) and [StrategyLinkNetworkAnalysis](../../examples/ARDUINO/Local/Any/StrategyLinkNetworkAnalysis) examples. + +All the other necessary information is present in the general [Documentation](/documentation). diff --git a/hal/transport/PJON/driver/strategies/Any/StrategyLink.h b/hal/transport/PJON/driver/strategies/Any/StrategyLink.h new file mode 100644 index 000000000..878a75fd0 --- /dev/null +++ b/hal/transport/PJON/driver/strategies/Any/StrategyLink.h @@ -0,0 +1,107 @@ + +/* StrategyLink + + Proposed and developed by Fred Larsen + ___________________________________________________________________________ + + Copyright 2010-2020 Giovanni Blu Mitolo gioscarab@gmail.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + +#pragma once + +template +class StrategyLink : public StrategyLinkBase +{ +public: + Strategy strategy; + + /* Returns delay related to the attempts passed as parameter: */ + + uint32_t back_off(uint8_t attempts) + { + return strategy.back_off(attempts); + } + + + /* Begin method, to be called on initialization: */ + + bool begin(uint8_t did = 0) + { + return strategy.begin(did); + } + + + /* Check if the channel is free for transmission: */ + + bool can_start() + { + return strategy.can_start(); + } + + + /* Returns the maximum number of attempts for each transmission: */ + + uint8_t get_max_attempts() + { + return strategy.get_max_attempts(); + } + + + /* Returns the recommended receive time for this strategy: */ + + uint16_t get_receive_time() + { + return strategy.get_receive_time(); + } + + + /* Handle a collision: */ + + void handle_collision() + { + strategy.handle_collision(); + }; + + + /* Receive a frame: */ + + uint16_t receive_frame(uint8_t *data, uint16_t max_length) + { + return strategy.receive_frame(data, max_length); + } + + + /* Receive byte response: */ + + uint16_t receive_response() + { + return strategy.receive_response(); + } + + + /* Send byte response to package transmitter: */ + + void send_response(uint8_t response) + { + strategy.send_response(response); + } + + + /* Send a frame: */ + + void send_frame(uint8_t *data, uint16_t length) + { + strategy.send_frame(data, length); + } +}; diff --git a/hal/transport/PJON/driver/strategies/Any/StrategyLinkBase.h b/hal/transport/PJON/driver/strategies/Any/StrategyLinkBase.h new file mode 100644 index 000000000..2aa165bd9 --- /dev/null +++ b/hal/transport/PJON/driver/strategies/Any/StrategyLinkBase.h @@ -0,0 +1,73 @@ + +/* StrategyLinkBase + + Proposed and developed by Fred Larsen + ___________________________________________________________________________ + + Copyright 2010-2020 Giovanni Blu Mitolo gioscarab@gmail.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + +#pragma once + +class StrategyLinkBase +{ +public: + + /* Returns delay related to the attempts passed as parameter: */ + + virtual uint32_t back_off(uint8_t attempts) = 0; + + + /* Begin method, to be called on initialization */ + + virtual bool begin(uint8_t did = 0) = 0; + + + /* Check if the channel is free for transmission */ + + virtual bool can_start() = 0; + + + /* Returns the maximum number of attempts for each transmission: */ + + virtual uint8_t get_max_attempts() = 0; + + /* Returns the recommended receive time for this strategy: */ + + virtual uint16_t get_receive_time() = 0; + + /* Handle a collision: */ + + virtual void handle_collision() = 0; + + + /* Receive a frame: */ + + virtual uint16_t receive_frame(uint8_t *data, uint16_t max_length) = 0; + + + /* Receive byte response: */ + + virtual uint16_t receive_response() = 0; + + + /* Send byte response to package transmitter: */ + + virtual void send_response(uint8_t response) = 0; + + + /* Send a frame: */ + + virtual void send_frame(uint8_t *data, uint16_t length) = 0; +}; diff --git a/hal/transport/PJON/driver/strategies/DualUDP/DualUDP.h b/hal/transport/PJON/driver/strategies/DualUDP/DualUDP.h new file mode 100644 index 000000000..3b481b5fd --- /dev/null +++ b/hal/transport/PJON/driver/strategies/DualUDP/DualUDP.h @@ -0,0 +1,428 @@ + +/* DualUDP is a Strategy for the PJON framework. + It supports delivering PJON packets through Ethernet UDP to a registered + list of devices on the LAN, WAN or Internet. Each device must be registered + with its device id, IP address and listening port number. + + Autopopulating the node table based on received packets and broadcasts is + enabled by default. This requires only remote (non-LAN) devices to be + manually registered. + + So this strategy combines the manually populated node list from the + GlobalUDP strategy with the broadcast based autodiscovery of devices + on the LAN in the LocalUDP strategy. If improves on LocalUDP by + stepping from UDP broadcasts to directed UDP packets, creating less + noise for other devices that may be listening to the same port. + It can communicate with devices outside the LAN if they are added manually + to the list, or if they send a packet to this device first. + + Note that this strategy cannot send and receive any contents, only + packets with a valid PJON header. + ___________________________________________________________________________ + + DualUDP strategy proposed and developed by Fred Larsen 01/12/2018 + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + +#pragma once + +#ifdef HAS_ETHERNETUDP +#include +#else +#include +#endif + +#include + +// Timeout waiting for an ACK. This can be increased if the latency is high +#ifndef DUDP_RESPONSE_TIMEOUT +#define DUDP_RESPONSE_TIMEOUT 50000ul +#endif + +// Minimum time interval in ms between send attempts. Some devices go into +// contention if sending too fast. This can be overridden in an interface +// for a device type, or in user sketches. +#ifndef DUDP_MINIMUM_SEND_INTERVAL_MS +#define DUDP_MINIMUM_SEND_INTERVAL_MS 8 +#endif + +// Backoff function that can be overridden depending on network and devices +#ifndef DUDP_BACKOFF +#define DUDP_BACKOFF(attempts) (1000ul * attempts + PJON_RANDOM(500)) +#endif + +// Max number of retries +#ifndef DUDP_MAX_RETRIES +#define DUDP_MAX_RETRIES 5 +#endif + +// The size of the node table +#ifndef DUDP_MAX_REMOTE_NODES +#define DUDP_MAX_REMOTE_NODES 10 +#endif + +// Remove automatically registered nodes after this number of send failures +#ifndef DUDP_MAX_FAILURES +#define DUDP_MAX_FAILURES 10 +#endif + +#define DUDP_DEFAULT_PORT 7500 +#define DUDP_MAGIC_HEADER (uint32_t) 0x0EFA23FF + +// Recommended receive time for this strategy, in microseconds +#ifndef DUDP_RECEIVE_TIME +#define DUDP_RECEIVE_TIME 0 +#endif + +//#define DUDP_DEBUG_PRINT + +class DualUDP +{ + bool _udp_initialized = false; + bool _auto_registration = true, // Add all sender devices to node table + _auto_discovery = true, // Use UDP broadcast to locate LAN devices + _did_broadcast = false; // Whether last send was a broadcast + + uint16_t _port = DUDP_DEFAULT_PORT; + uint8_t _unremovable_node_count = PJON_NOT_ASSIGNED; + + // Remember the details of the last outgoing packet + uint8_t _last_out_receiver_id = 0; + uint8_t _last_out_sender_id = 0; + uint32_t _last_out_time = 0; + + // Remember the details of the last incoming packet + PJON_Packet_Info _packet_info; // Also used for last outgoing + uint16_t _last_in_sender_port = 0; + uint8_t _last_in_sender_ip[4]; + uint8_t _last_in_receiver_id = 0; + uint8_t _last_in_sender_id = 0; + + // Remote nodes table + uint8_t _remote_node_count = 0; + uint8_t _remote_id[DUDP_MAX_REMOTE_NODES]; + uint8_t _remote_ip[DUDP_MAX_REMOTE_NODES][4]; + uint16_t _remote_port[DUDP_MAX_REMOTE_NODES]; + uint8_t _send_attempts[DUDP_MAX_REMOTE_NODES]; + + UDPHelper udp; + + bool check_udp() + { + if(!_udp_initialized) { + udp.set_magic_header(htonl(DUDP_MAGIC_HEADER)); + if(udp.begin(_port)) { + _udp_initialized = true; + } + } + return _udp_initialized; + }; + + int16_t find_remote_node(uint8_t id) + { + for(uint8_t i = 0; i < _remote_node_count; i++) + if(_remote_id[i] == id) { + return i; + } + return -1; + }; + + int16_t autoregister_sender() + { + // Add the last sender to the node table + if(_auto_registration) { + // If parsing fails, it will be 0 + if( _last_in_sender_id == 0) { + return -1; + } + // See if PJON id is already registered, add if not + int16_t pos = find_remote_node( _last_in_sender_id); + if(pos == -1) + return add_node( + _last_in_sender_id, + _last_in_sender_ip, + _last_in_sender_port + ); + else { // Update IP and port of existing node + memcpy(_remote_ip[pos], _last_in_sender_ip, 4); + _remote_port[pos] = _last_in_sender_port; + return pos; + } + } + return -1; + }; + +public: + + /* Register each device we want to send to. + If device id is set and autoregistration enabled, this table will + be filled automatically with devices that send to this device. + Devices that this device will send to must be registered if they are + outside the LAN and do not send a packet to this device first. + Devices on this LAN do not need to be manually registered. */ + + int16_t add_node( + uint8_t remote_id, + const uint8_t remote_ip[], + uint16_t port_number = DUDP_DEFAULT_PORT + ) + { + if(_remote_node_count == DUDP_MAX_REMOTE_NODES) { + return -1; + } + // Remember how many nodes were present at startup + if(_unremovable_node_count == PJON_NOT_ASSIGNED) { + _unremovable_node_count = _remote_node_count; + } +#ifdef DUDP_DEBUG_PRINT + Serial.print("Register id "); + Serial.print(remote_id); + Serial.print(" ip "); + Serial.println(remote_ip[3]); +#endif + // Add the new node + _remote_id[_remote_node_count] = remote_id; + memcpy(_remote_ip[_remote_node_count], remote_ip, 4); + _remote_port[_remote_node_count] = port_number; + _send_attempts[_remote_node_count] = 0; + _remote_node_count++; + return _remote_node_count - 1; + }; + + /* Unregister a node, if unreachable */ + + bool remove_node(uint8_t pos) + { + // Only allow the automatically added nodes to be removed + if(pos < _unremovable_node_count) { + return false; + } +#ifdef DUDP_DEBUG_PRINT + Serial.print("Unregistering id "); + Serial.println(_remote_id[pos]); +#endif + for(uint8_t i = pos; i < _remote_node_count - 1; i++) { + _remote_id[i] = _remote_id[i + 1]; + memcpy(_remote_ip[i], _remote_ip[i + 1], 4); + _remote_port[i] = _remote_port[i + 1]; + _send_attempts[i] = _send_attempts[i + 1]; + } + _remote_node_count--; + return true; + }; + + /* Whether the last send was a broadcast or a directed packet. + This is to let routers and sketches have a little insight. */ + + bool did_broadcast() + { + return _did_broadcast; + }; + + /* Select if incoming packets should automatically add their sender + as a node */ + + void set_autoregistration(bool enabled = true) + { + _auto_registration = enabled; + }; + + /* Select if broadcast shall be used to reach unregistered devices + and then add them to the node table when replying, going from + broadcast to directed communication as soon as possible. */ + + void set_autodiscovery(bool enabled = true) + { + _auto_discovery = enabled; + }; + + /* Returns the suggested delay related to attempts passed as parameter: */ + + uint32_t back_off(uint8_t attempts) + { + return DUDP_BACKOFF(attempts); + }; + + /* Begin method, to be called on initialization: + (returns always true) */ + + bool begin(uint8_t device_id) + { + (void)device_id; // Avoid "unused parameter" warning + return check_udp(); + }; + + /* Check if the channel is free for transmission */ + + bool can_start() + { + return check_udp() && ((uint32_t)(PJON_MILLIS() - _last_out_time) >= + DUDP_MINIMUM_SEND_INTERVAL_MS); + }; + + /* Returns the maximum number of attempts for each transmission: */ + + static uint8_t get_max_attempts() + { + return DUDP_MAX_RETRIES; + }; + + /* Returns the recommended receive time for this strategy: */ + + static uint16_t get_receive_time() + { + return DUDP_RECEIVE_TIME; + }; + + /* Handle a collision (empty because handled on Ethernet level): */ + + void handle_collision() { }; + + /* Receive a frame: */ + + uint16_t receive_frame(uint8_t *data, uint16_t max_length) + { + uint16_t length = udp.receive_frame(data, max_length); + // Then get the IP address and port number of the sender + udp.get_sender( _last_in_sender_ip, _last_in_sender_port); + if(length != PJON_FAIL && length > 4) { + // Extract some info from the header + PJONTools::parse_header(data, _packet_info); + _last_in_receiver_id = _packet_info.rx.id; + _last_in_sender_id = _packet_info.tx.id; + // Autoregister sender if the packet was sent directly + if( + _packet_info.tx.id != PJON_NOT_ASSIGNED && + _last_out_sender_id != PJON_NOT_ASSIGNED && + _packet_info.rx.id == _last_out_sender_id + ) { + autoregister_sender(); + } + } + return length; + }; + + /* Receive byte response */ + + uint16_t receive_response() + { + uint32_t start = PJON_MICROS(); + uint8_t result[10]; + uint16_t reply_length = 0; + do { + reply_length = receive_frame(result, sizeof result); + if(reply_length == PJON_FAIL) { + continue; + } + + // Ignore full PJON packets, we expect only a tiny response packet + if(reply_length != 3) { + continue; + } + + // Decode response packet + _last_in_receiver_id = result[0]; + _last_in_sender_id = result[1]; + uint8_t code = result[2]; + + // Ignore packets not responding to the last outgoing packet + if(_last_in_receiver_id != _last_out_sender_id) { + continue; + } + + // Ignore packets not from the receiver of the last outgoing packet + // 20181205: NO, allow these ACKS even if they are delayed, + // because it could be caused by forwarding multiple hops, + // and a delayed ACK is still a confirmation of the correct route. + //if(_last_in_sender_id != _last_out_receiver_id) continue; + + if(code == PJON_ACK) { + // Autoregister sender of ACK + int16_t pos = autoregister_sender(); + // Reset send attempt counter + if(pos != -1) { + _send_attempts[pos] = 0; + } + return code; + } + } while((uint32_t)(PJON_MICROS() - start) < DUDP_RESPONSE_TIMEOUT); +#ifdef DUDP_DEBUG_PRINT + Serial.println("Receive_response FAILED"); +#endif + return PJON_FAIL; + }; + + /* Send byte response to package transmitter. + We have the IP so we can reply directly. + Use the receiver id of the last incoming packet instead of the id of + this device, to function also in router mode. */ + + void send_response(uint8_t response) // Empty, PJON_ACK is always sent + { + uint8_t buf[3]; + buf[0] = _last_in_sender_id; // Send to the device last received from + buf[1] = _last_in_receiver_id; // Send from the id last received to + buf[2] = response; + udp.send_response(buf, 3); + }; + + /* Send a frame: */ + + void send_frame(uint8_t *data, uint16_t length) + { + _did_broadcast = false; + if(length > 4) { + // Extract some info from the header + PJONTools::parse_header(data, _packet_info); + _last_out_receiver_id = _packet_info.rx.id; + _last_out_sender_id = _packet_info.tx.id; + + // Locate receiver in table unless it is a PJON broadcast (receiver 0) + int16_t pos = -1; + if(_last_out_receiver_id != 0) { + pos = find_remote_node(_last_out_receiver_id); + } + + // Check if receiver is not responding and should be unregistered + if( + pos != -1 && + (_send_attempts[pos] > (get_max_attempts() * DUDP_MAX_FAILURES)) && + remove_node((uint8_t)pos) + ) { + pos = -1; + } + + if(pos == -1) { // UDP Broadcast, send to all receivers + if(_auto_discovery) { + udp.send_frame(data, length); + } + _did_broadcast = true; +#ifdef DUDP_DEBUG_PRINT + Serial.print("Broadcast, id "); + Serial.println(_last_out_receiver_id); +#endif + } else { // To a specific IP+port + udp.send_frame(data, length, _remote_ip[pos], _remote_port[pos]); + _send_attempts[pos]++; + } + _last_out_time = PJON_MILLIS(); + } + }; + + /* Set the UDP port: */ + + void set_port(uint16_t port = DUDP_DEFAULT_PORT) + { + _port = port; + }; +}; diff --git a/hal/transport/PJON/driver/strategies/DualUDP/README.md b/hal/transport/PJON/driver/strategies/DualUDP/README.md new file mode 100644 index 000000000..d3cb2d198 --- /dev/null +++ b/hal/transport/PJON/driver/strategies/DualUDP/README.md @@ -0,0 +1,90 @@ +## DualUDP + +| Medium | Pins used | Inclusion | +|--------|-----------|--------------------| +| Ethernet port, wired or WiFi | NA |`#include `| + +With the `DualUDP` PJON strategy, multiple devices with Ethernet ports can use PJON to communicate with each other over an Ethernet network, wired or over WiFi or both. Like the `GlobalUDP` strategy, this strategy is not limited to the local network +and can therefore reach devices farther away, to another LAN connected through VPN, or potentially across the Internet (beware of security issues). Like the `LocalUDP` strategy it will reach devices on the LAN without configuration. + +Feature summary: +* Will autodiscover devices on the LAN. These can use DHCP assigned IP addresses. +* Can reach devices outside the LAN if they are added to the node table. These must have static IP, or send a packet to this device to be registered so that their IP is known. +* Steps from UDP broadcast to directed packets after device discovery. + +### Why PJON over DualUDP? +If a cabled or wireless Ethernet network exists, using this to let devices communicate can be easier than to pull new wires or utilize other radio communication modules. + +It can also be useful for connecting physically separate clusters of devices that are connected wired with the `SoftwareBitBang` strategy, or wirelessly with the `Oversampling` strategy, when a LAN or WAN is connecting the locations. + +### How to use DualUDP +Use `PJONDualUDP` to instantiate a PJON object ready to communicate using the `DualUDP` strategy: +```cpp + #include + + PJONDualUDP bus(44); // Use device id 44 +``` + +Now the Ethernet card must be set up. This can be done by using a static IP address or by getting an address assigned by DHCP. + +Set up the Ethernet card in the usual manner by calling `Ethernet.begin`, register the other devices to send to, then call the `begin` method on the PJON object: +```cpp +void setup() { + // Configure a fixed IP address + Ethernet.begin(mac, local_ip, gateway, gateway, subnet); + + // Add two devices to the node table + bus.strategy.add_node(45, remote_ip1); + bus.strategy.add_node(46, remote_ip2); + bus.begin(); +} +``` +or +```cpp +void setup() { + while (!Ethernet.begin(mac)); // Wait for DHCP + bus.begin(); +} +``` + +### Node table +DualUDP has a "node" table that is a list of devices with PJON id, IP address and port number (usually all using the same standard port number). A device in this table can be contacted directly by sending a packet specifically to its IP and port. The core functionality requires all relevant devices to be registered here, otherwise they cannot be reached or replied to because the IP address is not known. + +But since this is cumbersome, a couple of options have been added to make life simpler: +1. Adding senders of packets that are sent to this device automatically to the node table so that it is possible to reply. This option is named _auto_registration_ and can be disabled by a setter. +2. Trying to broadcast a packet when the receiver is not registered in the table. This will be picked up by the receiver if it is on the same network segment (LAN) and uses the same port number. If the device ACKs the broadcast packet, option 1 above will add it to the node table so that it can be contacted without broadcast from that point on. This is named _auto_discovery_ and can be disabled by a setter. + +These two options are enabled by default, making it possible to use DHCP on every device on a LAN, and to have no devices in the node table at startup. This makes it equally simple to use as `LocalUDP`, but with the strengths of `GlobalUDP`, therefore the name DualUDP. +After starting, all packets will be broadcast, but as packets are ACKed or replied to, the table will be populated and directed packets will be used instead. This does not disturb other devices like broadcast does, and if the traffic is high broadcasts may also put a load on any WiFi network connected to the LAN because all broadcast packets are sent over air as well. + +Note that the preprocessor define `DUDP_MAX_REMOTE_NODES` is important when using `autoregistration`. For a device it should be higher than the maximum number of other devices it will communicate with. Its default value of 10 is low to save memory, and in larger setups it must be increased, otherwise broadcast will be used for devices not registered in the table. + +### Remote devices +Devices not being present on the LAN will not be reached by broadcasts and will therefore not be automatically discovered unless they send a packet to this device. So if a master device has a fixed IP address and remote devices in different locations have the master in their node table and send a packet at startup and at regular intervals (in case master is restarted), communication will be established. + +Remote devices not sending any packet to this device will be unreachable unless they are manually added to the node table. + +The node table can have one or more remote devices added to it at startup, with all relevant devices on the LAN being added automatically as needed. + +All the IP addresses of the registered nodes should be reachable. UDP port forwarding can be used to obtain this through firewalls. + +### Device removal from table +If a device cannot be reached for a number of times (`DUDP_MAX_FAILURES`), it will be removed from the node table if it has been added automatically. If it has been added programmatically before the communication starts, it is kept in a static part of the table and will never be removed. + +If a device is removed from the table, it will be "downgraded" to be reached by broadcast instead of directed packets. If it does ACK or sends a packet, it will again be added to the node table and "upgraded" to directed packets. + +### Static IP address mode +DualUDP can work in a static mode by requiring all devices to have static IP addresses and all remote devices to be registered as 'nodes' at setup. This allows it to talk equally to devices on the LAN and on remote networks, and have full control of a closed group. + +To work in full manual mode, disable the two options: +``` + bus.set_autoregistration(false); + bus.set_autodiscovery(false); +``` +All the other necessary information is present in the general [Documentation](/documentation). + +#### Known issues +- Firewall may block `DualUDP` packets, edit its configuration to allow them + +#### Safety warning +In all cases, when installing or maintaining a PJON network, extreme care must be taken to avoid any danger. When connecting a local bus to the internet using [EthernetTCP](/src/strategies/EthernetTCP) or [DualUDP](/src/strategies/DualUDP) all connected devices must be considered potentially compromised, potentially manipulated or remotely actuated against your will. It should be considered a good practice not to connect to the internet systems that may create a damage (fire, flood, data-leak) if hacked. diff --git a/hal/transport/PJON/driver/strategies/ESPNOW/ESPNOW.h b/hal/transport/PJON/driver/strategies/ESPNOW/ESPNOW.h new file mode 100644 index 000000000..2b959ec8d --- /dev/null +++ b/hal/transport/PJON/driver/strategies/ESPNOW/ESPNOW.h @@ -0,0 +1,267 @@ + +/* ESPNOW is a Strategy for the PJON framework. + It supports delivering PJON packets through the Espressif ESPNOW 802.11 + protocol to a registered list of devices on the 802.11 ESPNOW network. Each + device must be registered with its device id and mac address. + ___________________________________________________________________________ + + ESPNOW strategy proposed and developed by xlfe in September 2018 + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + +#pragma once +#include "../../interfaces/ARDUINO/ESPNOWHelper.h" + +// Timeout waiting for an ACK. This can be increased if the latency is high. +#ifndef EN_RESPONSE_TIMEOUT +#define EN_RESPONSE_TIMEOUT 100000ul +#endif + +#ifndef EN_MAX_REMOTE_NODES +#define EN_MAX_REMOTE_NODES 10 +#endif + +// Recommended receive time for this strategy, in microseconds +#ifndef EN_RECEIVE_TIME +#define EN_RECEIVE_TIME 0 +#endif + +#define EN_MAGIC_HEADER (uint8_t*)"\xEE\xFE\x0E\xEF" + +class ESPNOW +{ + bool _espnow_initialised = false; + bool _auto_registration = true; + uint8_t _channel = 14; + char _espnow_pmk[17] = + "\xdd\xdb\xdd\x44\x34\xd5\x6a\x0b\x7e\x9f\x4e\x27\xd6\x5b\xa2\x81"; + + // Remote nodes + uint8_t _remote_node_count = 0; + uint8_t _remote_id[EN_MAX_REMOTE_NODES]; + uint8_t _remote_mac[EN_MAX_REMOTE_NODES][ESP_NOW_ETH_ALEN]; + + ENHelper en; + + bool check_en() + { + if(!_espnow_initialised) { + en.set_magic_header(EN_MAGIC_HEADER); + if(en.begin(_channel,(uint8_t*)_espnow_pmk)) { + _espnow_initialised = true; + } + } + return _espnow_initialised; + }; + + int16_t find_remote_node(uint8_t id, uint8_t* mac) + { + for(uint8_t i = 0; i < _remote_node_count; i++) + if( + ((_remote_id[i] == id) && (id != PJON_NOT_ASSIGNED)) || + (memcmp(_remote_mac, mac, ESP_NOW_ETH_ALEN) == 0) + ) { + return i; + } + return -1; + }; + + void autoregister_sender(const uint8_t *message, uint16_t length) + { + // Add the last sender to the node table + if(_auto_registration && length>4) { + // First get PJON sender id from incoming packet + PJON_Packet_Info packet_info; + PJONTools::parse_header(message, packet_info); + uint8_t sender_id = packet_info.tx.id; + if(sender_id == 0) { + ESP_LOGE("ESPNOW", "AutoRegister parsing failed"); + return; // If parsing fails, it will be 0 + } + + // Then get the mac address of the sender + uint8_t sender_mac[ESP_NOW_ETH_ALEN]; + en.get_sender(sender_mac); + + // See if PJON id is already registered, add if not + int16_t pos = find_remote_node(sender_id, sender_mac); + if(pos == -1) { + ESP_LOGI("ESPNOW", "Autoregister new sender %d",sender_id); + add_node(sender_id, sender_mac); + } else if(memcmp(_remote_mac[pos], sender_mac, ESP_NOW_ETH_ALEN) == 0) { + // Update mac of existing node + ESP_LOGI( + "ESPNOW", + "Update sender sender_id(%d) [%02X:%02X:%02X:%02X:%02X:%02X]", + sender_id, + sender_mac[0], + sender_mac[1], + sender_mac[2], + sender_mac[3], + sender_mac[4], + sender_mac[5] + ); + memcpy(_remote_mac[pos], sender_mac, ESP_NOW_ETH_ALEN); + } + } + }; + +public: + + void set_pmk(char *espnow_pmk) + { + memcpy(_espnow_pmk, espnow_pmk, 16); + } + + void set_channel(uint8_t channel) + { + _channel = channel; + } + + /* Register each device we want to send to */ + + int16_t add_node( + uint8_t remote_id, + uint8_t remote_mac[] + ) + { + if(_remote_node_count == EN_MAX_REMOTE_NODES) { + return -1; + } + _remote_id[_remote_node_count] = remote_id; + memcpy(_remote_mac[_remote_node_count], remote_mac, ESP_NOW_ETH_ALEN); + en.add_node_mac(remote_mac); + _remote_node_count++; + return _remote_node_count - 1; + }; + + + /* Set if each packet's sender is automatically added as a node */ + + void set_autoregistration(bool enabled) + { + _auto_registration = enabled; + }; + + + /* Returns the suggested delay related to attempts passed as parameter: */ + + uint32_t back_off(uint8_t attempts) + { + return 10000ul * attempts + PJON_RANDOM(10000); + }; + + + /* Begin method, to be called on initialization: + (returns always true) */ + + bool begin(uint8_t /*did*/ = 0) + { + return check_en(); + }; + + + /* Check if the channel is free for transmission */ + + bool can_start() + { + return check_en(); + }; + + + /* Returns the maximum number of attempts for each transmission: */ + + static uint8_t get_max_attempts() + { + return 10; + }; + + + /* Returns the recommended receive time for this strategy: */ + + static uint16_t get_receive_time() + { + return EN_RECEIVE_TIME; + }; + + + /* Handle a collision (empty because handled on Ethernet level): */ + + void handle_collision() { }; + + + /* Receive a frame: */ + + uint16_t receive_frame(uint8_t *data, uint16_t max_length) + { + uint16_t length = en.receive_frame(data, max_length); + if(length != PJON_FAIL) { + autoregister_sender(data, length); + } + return length; + } + + + /* Receive byte response */ + + uint16_t receive_response() + { + /* TODO: Improve robustness by ignoring packets not from the previous + receiver (Perhaps not that important as long as ACK/NAK responses are + directed, not broadcast) */ + uint32_t start = PJON_MICROS(); + uint8_t result[PJON_PACKET_MAX_LENGTH]; + uint16_t reply_length = 0; + do { + reply_length = receive_frame(result, sizeof result); + // We expect 1, if packet is larger it is not our ACK + if(reply_length == 1) + if(result[0] == PJON_ACK) { + return result[0]; + } + + } while ((uint32_t)(PJON_MICROS() - start) < EN_RESPONSE_TIMEOUT); + return PJON_FAIL; + }; + + + /* Send byte response to package transmitter. + We have the IP so we can reply directly. */ + + void send_response(uint8_t response) // Empty, PJON_ACK is always sent + { + en.send_response(response); + }; + + + /* Send a frame: */ + + void send_frame(uint8_t *data, uint16_t length) + { + if(length > 0) { + uint8_t id = data[0]; // Package always starts with a receiver id + PJON_Packet_Info packet_info; // use parser to get intended recipient MAC + PJONTools::parse_header(data, packet_info); + if(id == 0) { // Broadcast, send to all receivers + en.send_frame(data, length); + } else { // To a specific receiver + int16_t pos = find_remote_node(id, packet_info.rx.mac); + if(pos != -1) { + en.send_frame(data, length, _remote_mac[pos]); + } else { //Broadcast - any replies will get registered + en.send_frame(data, length); + } + } + } + }; +}; diff --git a/hal/transport/PJON/driver/strategies/ESPNOW/README.md b/hal/transport/PJON/driver/strategies/ESPNOW/README.md new file mode 100644 index 000000000..361c12a15 --- /dev/null +++ b/hal/transport/PJON/driver/strategies/ESPNOW/README.md @@ -0,0 +1,58 @@ +## ESPNOW + +| Medium | Pins used | Constant | +|--------|-----------|--------------------| +| ESPNOW over WiFi | NA | `#include ` | + +With the `ESPNOW` PJON strategy, up to 10 ESP32 devices can use PJON to communicate with each other over +the [Espressif ESPNOW protocol](https://www.espressif.com/en/products/software/esp-now/overview) (peer-to-peer 802.11). + +PJON over ESPNOW has the following benefits: +* Lower latency (no router is required, devices communicate directly with each other) +* Auto configuration (devices register on the group when starting up and so are accessible immediately) +* Further distance (ESPNOW claims up to 1km line of sight) + +PJON over WiFi has the following benefit: +* Connected directly over IP network (easier accessibility) + +### How to use ESPNOW +ESPNOW strategy can be used within the Arduino IDE or as an esp-idf component with [arduino-esp32](https://github.com/espressif/arduino-esp32). + +Use `PJONESPNOW` to instantiate a PJON object ready to communicate using `ESPNOW` strategy: + +```cpp + #include + + PJONESPNOW bus(44); // Use device id 44 +``` + +You can customise the channel (default is channel 1) and PMK (encryption) as follows (all devices must be the same): +```cpp +void setup() { + // Note PMK is 16 bytes, to store it in a char use char[17] or add an extra byte for the null terminator + char pmk[17] = "\x2b\xb2\x1e\x7a\x83\x13\x76\x9f\xf8\xa9\x3b\x1b\x5b\x52\xd0\x70"; + PJON bus(44); + + bus.strategy.set_channel(6); + bus.strategy.set_pmk(pmk); +} +``` + +The ESPNOW strategy will send a broadcast message if the device_id is not already registered. Once a response is received (assuming auto-registration is enabled) the device automatically adds the node id and mac in its table. Sender auto-registration is enabled by default and can be disabled using the following setter: + +```cpp + bus.set_autoregistration(false); +``` + +With auto-registration the sender of each incoming packet is automatically registered in the node table, meaning that you don't have to pre-register each devices' mac address. If you disable auto-registration you can add a node as follows: + +```cpp +uint8_t dev_mac[6] = {0x02, 0x23, 0x34, 0x22, 0x33, 0x44 }; +bus.strategy.add_node(device_id, dev_mac); +``` + +### Safety warning +In all cases, when installing or maintaining a PJON network, extreme care must be taken to avoid any danger. +When using ESPNOW keep in mind that all connected devices must be considered potentially compromised if the PMK is known +or can be guessed, and therefore potentially manipulated or remotely actuated against your will. It should be considered +a good practice to implement a message authentication procedure. diff --git a/hal/transport/PJON/driver/strategies/EthernetTCP/EthernetLink.h b/hal/transport/PJON/driver/strategies/EthernetTCP/EthernetLink.h new file mode 100644 index 000000000..21f003d87 --- /dev/null +++ b/hal/transport/PJON/driver/strategies/EthernetTCP/EthernetLink.h @@ -0,0 +1,1482 @@ +/* EthernetLink and Ethernet strategies contributed by Fred Larsen + + This EthernetLink class transfers packets of data over + TCP/IP Ethernet connections. + + It may accept incoming connections and send ACK back for each incoming + package, it may connect to another object in another device and deliver a + packet and get an ACK back, or it may do both -- connect to another device + for delivering packages and accept incoming connections and packages for + bidirectional transfer. + + When establishing connections, the target must be registered with the add_node + function for setting the IP and port along with a unique id that is used for + referring to the target in all other functions. + + An EthernetLink may work in multiple ways when bidirectional: + + 1. One-to-one connection with another EthernetLink. In this mode, using the + keep_connection is recommended because it reduces network activity and + speeds things up dramatically. In this mode, there are three options: + + A. When running on a firewall-free network one socket can be created in each + direction, allowing for bidirectional "full-duplex" (as far as that goes + for single-threaded code), with data being buffered in each direction. It + requires each side to know about each other's address, but delivers + higher throughput than a single_socket approach. An EthernetLink object + in this mode has one node added by add_node, and both sides calls + start_listening to accept incoming connections. If there are multiple + objects on one device, each has to listen to a unique port number + (specified in the start_listening function call). + + B. Set single_socket to mave a master-slave scenario where one side, the + initiator, creates a socket connection and sends any packages it may + have and then receives any package waiting to be sendt in the opposite + direction. This requires the initiator to know where to connect, but not + the other way around. This is suitable for connections through the + Internet because a firewall opening only has to be opened in one place. + An EthernetLink object in this mode has one node added by add_node + (the initiator side), or calls the start_listening function to start + listening (the receiver side). If there are multiple receiving objects + on one device, each has to listen to a unique port number + (specified in the start_listening function call). + + C. Set single_initiate_direction to have one device create two sockets to + the other device, using one for each packet direction. This is a variant + of B) that is firewall friendly and more efficient, but uses one more + socket, and cannot be run on the simplest Ethernet cards with only one + available socket. This mode has 3-4 times the speed of B) and does not + do polling so it will not generate network traffic when idle. + + 2. If running a one-to-many or many-to-many scenario the keep_connection + should be deactivated because then one incoming socket would block others + from connecting. Not using keep_connection drops the transfer speed some, though. + + A. When running on a firewall-free network each packet is delivered through + a dedicated connection created for delivering that package. After + delivery and ACK the socket is closed. All devices can send to and + receive from each other. + + B. Using the single_socket approach will use a master-slave like approach + where only the initiators need to know the address of the receiver(s), + for easier firewall traversal. After the two-way transfer of packets on + the same connection, the connection will be closed so that the receiver + is ready for another connection. This requires only _one firewall + opening_ for bidirectional transfer with 255 others. + + Limitations for the different modes when using the W5100 based Ethernet shield + with a 4 socket limit: + + 1A. Two EthernetLink objects can be used for bidirectional communication + with two sites. + + 1B. Four EthernetLink objects can be used for bidirectional communication + with four sites. + + 2A. As the sockets are only used temporarily each object can accept + connections from 255 others, and deliver packets to 255 others. Up to + two objects can be used in one device, if multiple listening ports are + needed. Each object can have 1 to 255 nodes added by add_node, and can + deliver to all. There can be up to three objects in each device if needed. + One socket must remain free for outgoing connections. + 2B. As 2A, each receiver can receive packets from and send packets to 255 + initiators, using a _single listening port_. There can be four receiver + objects in on device, or three receiver objects and an unlimited number + of initiator objects that will be able to send to and receive from to up + to 255 sites each. + + Limitations for the different modes when using the ENC28J60 based Ethernet + shield with a 1 socket limit: + + 1A. Not available. + 1B. One EthernetLink object can be used for fast bidirectional communication + with one site. + 1C. Not available. + 2A. Not available. + 2B. One receiver can receive from and deliver packets to unlimited initiators, + _or_ unlimited initator objects can send to and receive from to up to + 255 sites each + + Note: To use the ENC28J60 shield instead of the WIZ5100 shield, include + before EthernetLink.h. + + PJON and EthernetLink + The EthernetLink can be used standalone for simple delivery between two + devices. But the EthernetLink is also used as a tool by the EthernetTCP strategy + of PJON. A site consisting of one or more buses of devices communicating + through PJON, wired and/or wirelessly, can be connected seamlessly with + multiple other similar sites in other places of the world, communicating + through Internet with minimal firewall configuration. + + NOTE: The W5100 full-size Ethernet shields come in multiple variants. If you + get a problem where the card is not starting after plugging in the + power, check if there is small resistor network behind the Ethernet + outlet, with one resistor having printed "511" on it. If so, try + another brand. Even startup delays do not fix this, but it can be solved + with a capacitor+resistor, search for it. + + NOTE: If needing single_socket functionality with ACK (polling mode), + define ETCP_SINGLE_SOCKET_WITH_ACK. The program size has been reduced by + only including this when needed. + + The same goes for ETCP_SINGLE_DIRECTION. It is not included by default + to reduce program size. Define this when needed. + */ + +#pragma once + +#ifdef ARDUINO +#include "../../interfaces/ARDUINO/TCPHelper_ARDUINO.h" +#else +#include "../../interfaces/LINUX/TCPHelper_POSIX.h" +#ifndef F +#define F(x) (x) +#endif +#define Serial DummyPrint +struct DummyPrint { + static void print(const char *s) + { + printf("%s", s); + } + static void print(int n) + { + printf("%d", n); + } + static void println(const char *s) + { + printf("%s\n", s); + } + static void println(int n) + { + printf("%d\n", n); + } +} DummyPrint; +#endif + +// Constants +#ifndef PJON_ACK +#define PJON_ACK 6 +#endif + +// Internal constants +#ifndef PJON_FAIL +#define PJON_FAIL 0x100 +#endif + +#ifndef ETCP_MAX_REMOTE_NODES +#define ETCP_MAX_REMOTE_NODES 10 +#endif + +#define ETCP_DEFAULT_PORT 7000 + +/* The maximum packet size to be transferred, this protects again buffer overflow. + Set this to a size that is guaranteed to be available in RAM during runtime, + depending on the hardware and software. */ +#ifndef ETCP_MAX_PACKET_SIZE +#define ETCP_MAX_PACKET_SIZE 300 +#endif + + +/* If an incoming packet has not arrived for some time, disconnect the socket so + it will be reconnected on demand. The timeout is in ms. + The reason for this is that in some cases an idle socket may have gotten + disconnected without it being detected, unless trying to write to it. + So we could be waiting for data that never arrives. */ +#ifndef ETCP_IDLE_TIMEOUT +#define ETCP_IDLE_TIMEOUT 30000ul +#endif + +// Magic number to verify that we are aligned with telegram start and end +#define ETCP_HEADER 0x18ABC427ul +#define ETCP_FOOTER 0x9ABE8873ul +#define ETCP_SINGLE_SOCKET_HEADER 0x4E92AC90ul +#define ETCP_SINGLE_SOCKET_FOOTER 0x7BB1E3F4ul +#define ETCP_CONNECTION_HEADER_A 0xFEDFED67ul // Primary socket, packets in initiated direction +#define ETCP_CONNECTION_HEADER_A_ACK 0xFEDFED68ul // Same, but request ACK for all packets +#define ETCP_CONNECTION_HEADER_B 0xFEDFED77ul // Reverse socket, packets in reverse direction + +/* UIPEthernet library used for the ENC28J60 based Ethernet shields has the + correct return value from the read call, while the standard Ethernet library + does not follow the standard! */ +#if defined(ARDUINO) && !defined(UIPETHERNET_H) +#define ETCP_ERROR_READ 0 +#else +#define ETCP_ERROR_READ -1 +#endif + +typedef void (*link_receiver)( + uint8_t id, + const uint8_t *payload, + uint16_t length, + void *callback_object +); + +typedef void (*link_error)( + uint8_t code, + uint8_t data +); + +class TmpBuffer +{ + uint8_t *buf = NULL; + uint16_t len = 0; +public: + TmpBuffer(uint16_t size) + { + len = size; + buf = new uint8_t[size]; + } + ~TmpBuffer() + { + if (buf) { + delete buf; + } + } + uint8_t* operator()() + { + return buf; + } + uint16_t size() + { + return len; + } +}; + +class EthernetLink +{ +private: + // ********* Dynamic members ************ + + TCPHelperServer *_server = NULL; + + // Connection for writing outgoing packets + TCPHelperClient _client_out; + + // Connection for reading incoming packets + TCPHelperClient _client_in; + + // The id of the remove device/node that we have connected to + int16_t _current_device = -1; + + // When a socket is received, the connection header specifies if ACKs are wanted + bool _ack_requested = false; + + // Remember the connection statistics + uint32_t _connection_time = 0; + uint32_t _connection_count = 0; + uint32_t _last_receive_time = 0; + + // ********* Configuration ************ + + link_receiver _receiver = NULL; + link_error _error = NULL; + void *_callback_object = NULL; + + // Local node + uint8_t _local_id = 0; + uint8_t _local_ip[4]; + uint16_t _local_port = ETCP_DEFAULT_PORT; + + // Remote nodes + uint8_t _remote_node_count = 0; + uint8_t _remote_id[ETCP_MAX_REMOTE_NODES]; + uint8_t _remote_ip[ETCP_MAX_REMOTE_NODES][4]; + uint16_t _remote_port[ETCP_MAX_REMOTE_NODES]; + + // Keep sockets permanently open instead of reconnecting for each transfer + bool _keep_connection = false; + + // Do bidirectional transfer on a single socket + bool _single_socket = false; + + // Request an immediate ACK for eack packet delivery to ensure guaranteed delivery + bool _request_ack = false; + + // To avoid deadlocks while connecting (if receiver tries to connect back simultanously), + // receive and discard packets while doing non-blocking connect. This should resolve deadlocks, + // and the missing ACK should make the packet being resent later. + bool _receive_and_discard = false; + + // With single_socket = false, there is one socket for each packet direction. + // Normally the sockets are initiated from the side sending the packet. + // By setting initiate_both_sockets_in_same direction, both sockets can be + // initiated from one of the devices, to simplify firewall setup, or for letting + // only one of the devices have a static IP address. + // This should only be used with _keep_connection = true, and is meant for permanent + // one-to-one links. + bool _initiate_both_sockets_in_same_direction = false; + // Whether to be the side initiating both sockets or not + bool _initiator = true; + +public: + + void init() + { + memset(_local_ip, 0, 4); + }; + + + int16_t find_remote_node(uint8_t id) + { + for(uint8_t i = 0; i < _remote_node_count; i++) + if(_remote_id[i] == id) { + return i; + } + return -1; + }; + + + int16_t read_bytes( + TCPHelperClient &client, + uint8_t *contents, + uint16_t length, + uint16_t timeout_ms = 2000 + ) + { + int32_t total_bytes_read = 0, bytes_read = ETCP_ERROR_READ; + uint32_t start_ms = PJON_MILLIS(); + /* NOTE: The Arduino standard recv/read functions returns + -1 if no data waiting + 0 if socket closed + This is the opposite of POSIX. */ + do { +#ifdef HAS_ETHERNETUDP // Avoid using blocking read until there is data present + int16_t avail = 0; + while( + client.connected() && + (avail = client.available()) <= 0 && + (uint32_t)(PJON_MILLIS() - start_ms) < min(1000, timeout_ms) + ) { + PJON_DELAY_MICROSECONDS(250); + } + if (avail <= 0) { + continue; + } +#endif + + bytes_read = client.read( + &contents[total_bytes_read], + length - total_bytes_read + ); + + if(bytes_read > 0) { + total_bytes_read += bytes_read; + } + } while( + bytes_read != ETCP_ERROR_READ && + total_bytes_read < length && + (uint32_t)(PJON_MILLIS() - start_ms) < timeout_ms + ); + + if(bytes_read == ETCP_ERROR_READ) { + stop(client); // Lost connection +#ifdef ETCP_ERROR_PRINT + Serial.println(F("Read failed, closing cl")); +#endif + } + return total_bytes_read; + }; + + + // Read a package from a connected client (incoming or outgoing socket) and send ACK + uint16_t receive(TCPHelperClient &client, bool wait) + { + uint16_t return_value = PJON_FAIL; + uint32_t start_ms = PJON_MILLIS(), avail; + if (wait) { + while( + client.connected() && + (avail = client.available()) <= 0 && + (uint32_t)(PJON_MILLIS() - start_ms) < 1000 + ) { + PJON_DELAY_MICROSECONDS(250); + } + } else { + avail = client.connected() ? client.available() : 0; + } + + if(avail > 0) { +#ifdef ETCP_DEBUG_PRINT + Serial.println(F("Recv from cl")); +#endif + + // Locate and read encapsulation header (4 bytes magic number) + bool ok = read_until_header(client, ETCP_HEADER); +#ifdef ETCP_DEBUG_PRINT + Serial.print(F("Read header, stat ")); + Serial.println(ok); +#endif + + // Read sender device id (1 byte) and length of contents (4 bytes) + int16_t bytes_read = 0; + uint8_t sender_id = 0; + uint32_t content_length = 0; + if(ok) { + uint8_t buf[5]; + bytes_read = read_bytes(client, buf, 5); + if(bytes_read != 5) { + ok = false; + } else { + memcpy(&sender_id, buf, 1); + memcpy(&content_length, &buf[1], 4); + content_length = ntohl(content_length); + if(content_length == 0) { + ok = 0; + } + } + } + + // Protect against too large packets + if (content_length > ETCP_MAX_PACKET_SIZE) { + return PJON_FAIL; + } + + // Read contents and footer + TmpBuffer buf(content_length); + if(ok) { + bytes_read = read_bytes(client, (uint8_t*)buf(), content_length); + if((uint32_t)bytes_read != content_length) { + ok = false; + } + } + + // Read footer (4 bytes magic number) + if(ok) { + uint32_t foot = 0; + bytes_read = read_bytes(client, (uint8_t*) &foot, 4); + if(bytes_read != 4 || foot != htonl(ETCP_FOOTER)) { + ok = false; + } + } + +#ifdef ETCP_DEBUG_PRINT + Serial.print(F("Stat rec: ")); + Serial.print(ok); + Serial.print(" len: "); + Serial.println(content_length); +#else +#if defined(ETCP_ERROR_PRINT) + if (!ok) { + Serial.print(F("FAIL rec: ")); + Serial.println(bytes_read); + } +#endif +#endif + + return_value = ok ? PJON_ACK : PJON_FAIL; + if (ok) { + _last_receive_time = PJON_MILLIS(); + } + if (_ack_requested && !_receive_and_discard) { + // Write PJON_ACK + int8_t acklen = 0; + if(ok) { + uint16_t r = htons(return_value); + acklen = client.write((uint8_t*) &r, 2); + client.flush(); + } + +#ifdef ETCP_DEBUG_PRINT + Serial.print("Sent "); + Serial.print(ok ? "PJON_ACK: " : "PJON_FAIL: "); + Serial.println(acklen); +#else + (void)acklen; // Avoid "set but not used" warning +#if defined(ETCP_ERROR_PRINT) + if (!ok) { + Serial.println("FAILURE sending ACK"); + } +#endif +#endif + } + + // Call receiver callback function + if(ok && !_receive_and_discard && content_length > 0) { + _receiver(sender_id, (uint8_t*)buf(), content_length, _callback_object); + } + + if (!ok) { + disconnect_in(); + } + } + return return_value; + }; + + + bool connect(uint8_t id) + { + // Locate the node's IP address and port number + int16_t pos = find_remote_node(id); + + // Determine if to disconnect + bool disconnect = !_keep_connection, + reverse = _initiate_both_sockets_in_same_direction && _initiator; + + // Break existing connection if not connected to the wanted server + if(!disconnect && _client_out && _current_device != id) { + // Connected, but to the wrong device +#ifdef ETCP_DEBUG_PRINT + Serial.println(F("Switch conn to another srv")); +#endif + disconnect = true; + _current_device = -1; + } + + // Check if established sockets have been disconnected + if (!disconnect && _client_out && !_client_out.connected()) { + disconnect = true; + } +#ifdef ETCP_SINGLE_DIRECTION + if (!disconnect && reverse && _client_in && !_client_in.connected()) { + disconnect = true; + } + if (!disconnect && reverse && (!_client_in || !_client_out)) { + disconnect = true; + } + if (!disconnect && reverse && _client_in && got_receive_timeout()) { +#ifdef ETCP_ERROR_PRINT + Serial.println(F("Receive timeout, disconn.")); +#endif + disconnect = true; + } +#endif + if (disconnect) { + disconnect_out(); + } + // NOTE: From this point we can avoid using the expensive connected() call + + bool connected = _client_out; +#ifdef ETCP_DEBUG_PRINT + if (!connected) { + Serial.print(F("Conn to srv pos=")); + Serial.println(pos); + } +#endif + + if(pos < 0) { + return false; + } + + // Try to connect to server if not already connected + bool did_connect = false; + if(!connected) { +#ifdef ETCP_DEBUG_PRINT + Serial.println("Conn.."); +#endif +#ifdef HAS_ETHERNETUDP // Arduino, otherwise POSIX + connected = _client_out.connect(_remote_ip[pos], _remote_port[pos]); +#else + // Use non-blocking calls, and receive and discard incoming packets + if (_client_out.prepare_connect(_remote_ip[pos], _remote_port[pos])) { + int8_t status = 0; + uint32_t start = PJON_MILLIS(); + do { + if (!_initiate_both_sockets_in_same_direction && !_single_socket && _server) { + TCPHelperClient client = _server->available(); + if (client) { + client.stop(); + } + } + status = _client_out.try_connect(); + if (status == 0) { + PJON_DELAY_MICROSECONDS(PJON_RANDOM(250)); // Avoid misusing CPU while waiting + } + } while (status == 0 && (uint32_t)(PJON_MILLIS()-start)<4000); + connected = (status == 1); + } +#endif + +#ifdef ETCP_DEBUG_PRINT + Serial.println(connected ? "Conn to srv" : "Failed conn to srv"); +#endif + if(connected) { + // Adjust connection header A to include ACK request flag + uint32_t conn_header = htonl(_request_ack ? ETCP_CONNECTION_HEADER_A_ACK : + ETCP_CONNECTION_HEADER_A); + if (_client_out.write((uint8_t*) &conn_header, 4) != 4) { + connected = false; +#ifdef ETCP_ERROR_PRINT + Serial.println(F("Disconn. failed write connhead")); +#endif + } + } + if (!connected) { + stop(_client_out); + } else { + did_connect = true; + } + } + + bool connected_rev = false; +#ifdef ETCP_SINGLE_DIRECTION + if (connected && reverse) { + connected_rev = _client_in; + if (!connected_rev) { +#ifdef ETCP_DEBUG_PRINT + Serial.println("Conn rev.."); +#endif + uint32_t start = PJON_MILLIS(); + do { + connected_rev = _client_in.connect(_remote_ip[pos], _remote_port[pos]); + } while (!connected_rev && (uint32_t)(PJON_MILLIS()-start) < 2000); +#ifdef ETCP_DEBUG_PRINT + Serial.println(connected_rev ? F("Conn rev to srv") : F("Failed rev conn to srv")); +#endif + if(connected_rev) { + uint32_t conn_header = htonl(ETCP_CONNECTION_HEADER_B); + if (_client_in.write((uint8_t*) &conn_header, 4) != 4) { + connected_rev = false; +#ifdef ETCP_ERROR_PRINT + Serial.println(F("Disconn rev. failed write connhead")); +#endif + } + } + if (connected_rev) { + // ACK active on both sockets or none in this mode + _ack_requested = _request_ack; + did_connect = true; + _last_receive_time = PJON_MILLIS(); // Count the connection as a receive action + } + } + } +#endif + + if (did_connect) { // Gather a litte connection information + _connection_time = PJON_MILLIS(); + _connection_count++; + if (_single_socket) { + _ack_requested = _request_ack; // Same mode in both directions + } + } + + if (!connected || (reverse && !connected_rev)) { +#ifdef ETCP_ERROR_PRINT + Serial.print(F("Fail conn, closing ")); + Serial.print(connected); + Serial.println(connected_rev); +#endif + disconnect_out(); + _current_device = -1; + PJON_DELAY(10); // Slow down if failure + return false; // Server is unreachable or busy + } else { + _current_device = id; // Remember who we are connected to + } + + return true; + }; + + + void stop(TCPHelperClient &client) + { + client.stop(); + }; + + void disconnect_in() + { +#ifdef ETCP_DEBUG_PRINT + if (_client_in || (_initiate_both_sockets_in_same_direction && _client_out)) { + Serial.println(F("Disconn in&rev")); + } +#endif +#ifdef ETCP_SINGLE_DIRECTION + if (_initiate_both_sockets_in_same_direction && _client_out) { + stop(_client_out); + } +#endif + if (_client_in) { + stop(_client_in); + } + } + + + void disconnect_out() + { +#ifdef ETCP_DEBUG_PRINT + if (_client_out || (_initiate_both_sockets_in_same_direction && _client_in)) { + Serial.println(F("Disconn out&rev")); + } +#endif + if (_client_out) { + stop(_client_out); + } +#ifdef ETCP_SINGLE_DIRECTION + if (_initiate_both_sockets_in_same_direction && _client_in) { + stop(_client_in); + } +#endif + } + + + bool accept() + { + // Determine if to disconnect + bool disconnect = !_keep_connection; +#ifdef ETCP_SINGLE_DIRECTION + bool reverse = _initiate_both_sockets_in_same_direction && !_initiator; +#endif + if (!disconnect && _client_in && !_client_in.connected()) { + disconnect = true; + } +#ifdef ETCP_SINGLE_DIRECTION + if (!disconnect && reverse && _client_out && !_client_out.connected()) { + disconnect = true; + } + if (!disconnect && reverse && (!_client_in || !_client_out)) { + disconnect = true; + } +#endif + if (!disconnect && _client_in && got_receive_timeout()) { +#ifdef ETCP_ERROR_PRINT + Serial.println(F("Receive timeout, disconn.")); +#endif + disconnect = true; + } + if (disconnect) { + disconnect_in(); + } + // NOTE: From this point we can avoid using the expensive connected() call + + // Accept new incoming connection + bool did_connect = false, connected = _client_in; + if (!connected) { + _client_in = _server->available(); + connected = _client_in; +#ifdef ETCP_DEBUG_PRINT + if(connected) { + Serial.println(F("Accepted")); + } +#endif + if (connected) { + uint32_t connection_header = 0; + bool header_ok = false; + if (read_bytes(_client_in, (uint8_t*) &connection_header, 4) == 4) { + if (connection_header == htonl(ETCP_CONNECTION_HEADER_A)) { + header_ok = true; + _ack_requested = false; + } else if (connection_header == htonl(ETCP_CONNECTION_HEADER_A_ACK)) { + header_ok = true; + _ack_requested = true; + } + } + if (header_ok) { + did_connect = true; + } else { + disconnect_in(); + connected = false; +#ifdef ETCP_ERROR_PRINT + Serial.println(F("Disconn. no connhead")); +#endif + } + } + } + + // Accept reverse connection if relevant +#ifdef ETCP_SINGLE_DIRECTION + if (connected && reverse && !_client_out) { +#ifdef ETCP_DEBUG_PRINT + Serial.println(F("Lst rev")); +#endif + // ACK active on both sockets or none in this mode + _request_ack = _ack_requested; + + bool connected_reverse = false; + uint32_t start = PJON_MILLIS(); + do { + _client_out = _server->available(); + } while (!_client_out && (uint32_t)(PJON_MILLIS()-start) < 2000); + + if(_client_out) { +#ifdef ETCP_DEBUG_PRINT + Serial.println("Accept rev OK"); +#endif + uint32_t connection_header = 0; + if (read_bytes(_client_out, (uint8_t*) &connection_header, 4) == 4 && + connection_header == htonl(ETCP_CONNECTION_HEADER_B)) { + connected_reverse = true; + } else { +#ifdef ETCP_ERROR_PRINT + Serial.println(F("Disconn. rev no connhead")); +#endif + } + } else { +#ifdef ETCP_ERROR_PRINT + Serial.println(F("Accept rev timed out")); +#endif + } + + if (connected_reverse) { + did_connect = true; + } else { +#ifdef ETCP_ERROR_PRINT + Serial.print(F("Fail accept, closing, ")); + Serial.print(connected); + Serial.println(connected_reverse); +#endif + connected = false; + disconnect_in(); + } + } +#endif + + if (did_connect) { + _connection_time = PJON_MILLIS(); + _connection_count++; + _last_receive_time = PJON_MILLIS(); // Count the connection as a receive action + } + return connected; + }; + + + void disconnect_out_if_needed(uint16_t result) + { + //bool connected = _client_out.connected(); + if(_client_out && (result == PJON_FAIL || !_keep_connection)) { + stop(_client_out); +#ifdef ETCP_DEBUG_PRINT + Serial.print("Disconn outcl. OK="); + Serial.println(result == PJON_ACK); +#endif + } + }; + + bool got_receive_timeout() + { + return (uint32_t)(PJON_MILLIS() - _last_receive_time) > ETCP_IDLE_TIMEOUT; + } + + bool disconnect_in_if_needed() + { + if(_client_in && !_keep_connection) { +#ifdef ETCP_DEBUG_PRINT + Serial.println("Disc. inclient."); +#endif + stop(_client_in); + } + return true; + }; + + + uint16_t send( + TCPHelperClient &client, + uint8_t id, + const uint8_t *packet, + uint16_t length + ) + { + // Assume we are connected. Try to deliver the package + uint32_t head = htonl(ETCP_HEADER), foot = htonl(ETCP_FOOTER), len = htonl(length); + uint8_t buf[9]; + memcpy(buf, &head, 4); + memcpy(&buf[4], &id, 1); + memcpy(&buf[5], &len, 4); + + if (!_single_socket && _client_in) if (_client_in.available() > 0) { + return PJON_BUSY; + } + +#ifdef HAS_ETHERNETUDP + bool ok = client.write((uint8_t*) buf, 9) == 9; + if(ok) { + ok = client.write((uint8_t*) packet, length) == length; + } + if(ok) { + ok = client.write((uint8_t*) &foot, 4) == 4; + } +#else + // On a POSIX capable device we expect to have enough memory to collect all into one buffer + // so that it will not be sent as 3 separate packets when TCP_NODELAY is active. + TmpBuffer totalbuf(9+length+4); + memcpy(totalbuf(), buf, 9); + memcpy(&(totalbuf()[9]), packet, length); + memcpy(&(totalbuf()[9+length]), &foot, 4); + bool ok = client.write((uint8_t*)totalbuf(), 9+length+4) == (9+length+4); +#endif + if(ok) { + client.flush(); + } +#ifdef ETCP_DEBUG_PRINT + Serial.print("Write stat: "); + Serial.println(ok); +#endif + + uint16_t result = PJON_FAIL; + if (_request_ack) { + // Read ACK + if(ok) { + uint16_t code = 0; + int16_t status = read_bytes(client, (uint8_t*) &code, 2); + code = ntohs(code); + if(status == 2 && code == PJON_ACK) { + result = code; + } else { + ok = false; + } + } +#ifdef ETCP_DEBUG_PRINT + Serial.print("PJON_ACK stat: "); + Serial.print(result == PJON_ACK); + Serial.println(ok ? " OK" : " FAIL"); +#endif + } else { + result = ok ? PJON_ACK : PJON_FAIL; + } + return result; // PJON_FAIL, PJON_ACK + }; + + + /* Do ACKed bidirectional transfer of packets over a single socket connection by + using a master-slave mode where the master connects and delivers packets + or a placeholder, then reads packets or placeholder back before closing + the connection (unless letting it stay open). */ +#ifdef ETCP_SINGLE_SOCKET_WITH_ACK + uint16_t single_socket_transfer( + TCPHelperClient &client, + int16_t id, + bool master, + const uint8_t *contents, + uint16_t length + ) + { + if(master) { // Creating outgoing connections + // Connect or check that we are already connected to the correct server + bool connected = connect(id); +#ifdef ETCP_DEBUG_PRINT + //Serial.println(connected ? "Out conn" : "No out conn"); +#endif + if(!connected) { + return PJON_FAIL; + } + + // Send singlesocket header and number of outgoing packets + bool ok = true; + uint32_t head = htonl(ETCP_SINGLE_SOCKET_HEADER); + uint8_t numpackets_out = length > 0 ? 1 : 0; + uint8_t buf[5]; + memcpy(buf, &head, 4); + memcpy(&buf[4], &numpackets_out, 1); + if(ok) { + ok = client.write((uint8_t*) &buf, 5) == 5; + } + if(ok) { + client.flush(); + } + + // Send the packet and read PJON_ACK + if(ok && numpackets_out > 0) { + ok = send(client, id, contents, length) == PJON_ACK; +#ifdef ETCP_DEBUG_PRINT + Serial.print(F("++++Sent p, ok=")); + Serial.println(ok); +#endif + } + + // Read number of incoming messages + uint8_t numpackets_in = 0; + if(ok) { + ok = read_bytes(client, &numpackets_in, 1) == 1; + } +#ifdef ETCP_DEBUG_PRINT + if (!ok || numpackets_in > 0) { + Serial.print("Read np_in: "); + Serial.print(numpackets_in); + Serial.println(ok ? " OK" : " FAIL"); + } +#endif + + // Read incoming packages if any + for(uint8_t i = 0; ok && i < numpackets_in; i++) { + while(client.available() < 1 && client.connected()) ; + ok = receive(client, true) == PJON_ACK; +#ifdef ETCP_DEBUG_PRINT + Serial.print(F("------->Read p ")); + Serial.print(i); + Serial.print(F(", ok=")); + Serial.println(ok); +#endif + } + + // Write singlesocket footer for the whole thing + uint32_t foot = htonl(ETCP_SINGLE_SOCKET_FOOTER); + if(ok) { + ok = client.write((uint8_t*) &foot, 4) == 4; + } + if(ok) { + client.flush(); + } +#ifdef ETCP_DEBUG_PRINT + // Serial.print("Sent ss foot, ok="); + // Serial.println(ok); +#endif + + // Disconnect + uint16_t result = ok ? PJON_ACK : PJON_FAIL; + disconnect_out_if_needed(result); +#ifdef ETCP_DEBUG_PRINT + if (numpackets_in > 0 || numpackets_out > 0) { + Serial.print("INOUTm "); + Serial.print(numpackets_in); + Serial.print(numpackets_out); + Serial.println(ok ? " OK" : " FAIL"); + } +#endif + + // Return PJON_ACK if successfully sent or received a packet + return contents == NULL ? (numpackets_in > 0 ? result : PJON_FAIL) : result; + + } else { // Receiving incoming connections and packets and request + if (client && got_receive_timeout()) { +#ifdef ETCP_ERROR_PRINT + Serial.println(F("Receive timeout, disconn.")); +#endif + stop(client); + } + + // Wait for and accept connection + bool connected = accept(); +#ifdef ETCP_DEBUG_PRINT + //Serial.println(connected ? "In conn" : "No in conn"); +#endif + if(!connected) { + return PJON_FAIL; + } + + // Read singlesocket header + bool ok = read_until_header(client, ETCP_SINGLE_SOCKET_HEADER); + if (ok) { + _last_receive_time = PJON_MILLIS(); + } +#ifdef ETCP_DEBUG_PRINT + //Serial.print("Read ss head, ok="); + //Serial.println(ok); +#endif + + // Read number of incoming packets + uint8_t numpackets_in = 0; + if(ok) { + ok = read_bytes(client, (uint8_t*) &numpackets_in, 1) == 1; + } +#ifdef ETCP_DEBUG_PRINT + if (!ok || numpackets_in > 0) { + Serial.print("Read np_in: "); + Serial.print(numpackets_in); + Serial.println(ok ? " OK" : " FAIL"); + } +#endif + + // Read incoming packets if any, send ACK for each + for(uint8_t i = 0; ok && i < numpackets_in; i++) { + while(client.available() < 1 && client.connected()) ; + ok = receive(client, true) == PJON_ACK; +#ifdef ETCP_DEBUG_PRINT + Serial.print(F("Read p ")); + Serial.print(i); + Serial.print(F(", ok=")); + Serial.println(ok); +#endif + } + + // Write number of outgoing packets + uint8_t numpackets_out = length > 0 ? 1 : 0; + if(ok) { + ok = client.write((uint8_t*) &numpackets_out, 1) == 1; + } + if(ok) { + client.flush(); + } + + // Write outgoing packets if any + if(ok && numpackets_out > 0) { + ok = send(client, id, contents, length) == PJON_ACK; +#ifdef ETCP_DEBUG_PRINT + Serial.print(F("Sent p, ok=")); + Serial.println(ok); +#endif + } + + // Read singlesocket footer + if(ok) { + uint32_t foot = 0; + ok = read_bytes(client, (uint8_t*) &foot, 4) == 4; + if(foot != htonl(ETCP_SINGLE_SOCKET_FOOTER)) { + ok = 0; + } +#ifdef ETCP_DEBUG_PRINT + //Serial.print(F("Read ss foot, ok=")); + //Serial.println(ok); +#endif + } + + // Disconnect + disconnect_in_if_needed(); + +#ifdef ETCP_DEBUG_PRINT + if (numpackets_in > 0 || numpackets_out > 0) { + Serial.print("INOUT "); + Serial.print(numpackets_in); + Serial.print(numpackets_out); + Serial.println(ok ? " OK" : " FAIL"); + } +#endif + + if (!ok) { + stop(client); +#ifdef ETCP_ERROR_PRINT + Serial.print(F("Failure, disconnecting.")); +#endif + } + + // Return PJON_ACK if successfully sent or received a packet + uint16_t result = ok ? PJON_ACK : PJON_FAIL; + return contents == NULL ? (numpackets_in > 0 ? result : PJON_FAIL) : result; + } + return PJON_FAIL; + }; +#endif + + /* Read until a specific 4 byte value is found. + This will resync if stream position is lost. */ + + bool read_until_header(TCPHelperClient &client, uint32_t header) + { + header = htonl(header); // Network byte order + uint32_t head = 0; + int8_t bytes_read = 0; + bytes_read = (uint8_t)read_bytes(client, (uint8_t*) &head, 4); + if(bytes_read != 4 || head != header) { + // Did not get header. Lost position in stream? + do { + /* Try to resync if we lost position in the stream + (throw avay all until ETCP_HEADER found) */ + head = head >> 8; + // Make space for 8 bits to be read into the most significant byte + bytes_read = (uint8_t)read_bytes(client, &((uint8_t*) &head)[3], 1); + if(bytes_read != 1) { + break; + } + } while(head != header); + } + return head == header; + }; + +public: + + EthernetLink() + { + init(); + }; + + + EthernetLink(uint8_t id) + { + init(); + set_id(id); + }; + +#ifndef ARDUINO + // Destructor not needed on Arduino, uses a lot of program space + ~EthernetLink() + { + if (_server) { + delete _server; + } + } +#endif + + int16_t add_node( + uint8_t remote_id, + const uint8_t remote_ip[], + uint16_t port_number = ETCP_DEFAULT_PORT + ) + { + if (_remote_node_count == ETCP_MAX_REMOTE_NODES) { + return -1; + } + _remote_id[_remote_node_count] = remote_id; + memcpy(_remote_ip[_remote_node_count], remote_ip, 4); + _remote_port[_remote_node_count] = port_number; + _remote_node_count++; + return _remote_node_count - 1; + }; + + + /* This function must be called after configuration before running, + for ALL configurations EXCEPT when initiator in single_socket + and single_initiate_direction modes. + In single_socket and single_initiate_direction modes, calling this + function sets it as receiver, NOT initiator. */ + + void start_listening(uint16_t port_number = ETCP_DEFAULT_PORT) + { + _local_port = port_number; + _initiator = false; // If we are listening, we are not the initiator + if(_server == NULL && (!_initiate_both_sockets_in_same_direction || !_initiator)) { +#ifdef ETCP_DEBUG_PRINT + Serial.print("Lst on port "); + Serial.println(port_number); +#endif + _server = new TCPHelperServer(port_number); + _server->begin(); + } + }; + + + /* Whether to keep outgoing connection live until we need connect to + another EthernetLink node */ + + void keep_connection(bool keep) + { + _keep_connection = keep; + }; + + + /* Whether to do bidirectional data transfer on a single socket or use one + socket for each direction. Single socket transfer is slower but more + firewall-friendly. Client connects to server, and packets are exchanged in + both directions on the same socket. If using this, _only one_ of the sides + should call start_listening, and that side will be receiver with the other + initiator (establishing connections) */ +#ifdef ETCP_SINGLE_SOCKET_WITH_ACK + void single_socket(bool single_socket) + { + _single_socket = single_socket; + keep_connection(true); + request_ack(true); + }; +#endif + + /* With single_socket = false, there is one socket for each packet direction. + Normally the sockets are initiated from the side sending the packet. + By setting initiate_both_sockets_in_same direction, both sockets can be + initiated from one of the devices, to simplify firewall setup, or for letting + only one of the devices have a static IP address. + This should only be used with _keep_connection = true, and is meant for permanent + one-to-one links. */ +#ifdef ETCP_SINGLE_DIRECTION + void single_initiate_direction(bool single_initiate_direction) + { + _initiate_both_sockets_in_same_direction = single_initiate_direction; + keep_connection(true); + request_ack(true); + }; +#endif + + /* Request an explicit immediate ACK for each packet being sent. + This make it possible with guearanteed delivery. + Without ACK, a packet may be lost before a dead socket is discovered, + but for some shemes this is acceptable. */ + + void request_ack(bool request_ack) + { + _request_ack = request_ack; + } + + /* Some connection statistics */ + uint32_t get_connection_time() const + { + return _connection_time; + } + uint32_t get_connection_count() const + { + return _connection_count; + } + + + // Keep trying to send for a maximum duration + + uint16_t send_with_duration( + uint8_t id, + const uint8_t *packet, + uint16_t length, + uint32_t duration_us + ) + { + uint32_t start = PJON_MICROS(); + uint16_t result = PJON_FAIL; + do { + result = send(id, packet, length); + } while( + result != PJON_ACK && + (uint32_t)(PJON_MICROS() - start) <= duration_us + ); + return result; + }; + + + /* In single-socket mode and acting as initiator, connect and check for + incoming packets from a specific device */ + + uint16_t poll_receive(uint8_t remote_id) + { + /* Create connection if needed but only poll for incoming packet + without delivering any */ + if(_single_socket) { + if(!_server) { +#ifdef ETCP_SINGLE_SOCKET_WITH_ACK + return single_socket_transfer(_client_out, remote_id, true, NULL, 0); +#else + (void)remote_id; // Avoid "unused parameter" warning + return PJON_FAIL; +#endif + } + } else { // Just do an ordinary receive without using the id + return receive(); + } + return PJON_FAIL; + }; + + + uint8_t get_id() const + { + return _local_id; + }; + + + // Overridden functions below ----------------------------------------------- + + // Connect to a server if needed, then read incoming package and send ACK + + uint16_t receive() + { + if(_server == NULL) { // Not listening for incoming connections +#if defined(ETCP_SINGLE_SOCKET_WITH_ACK) || defined(ETCP_SINGLE_DIRECTION) + int16_t remote_id = _remote_node_count == 1 ? _remote_id[0] : -1; +#endif + if(_single_socket) { // Single-socket mode. + /* Only read from already established outgoing socket, or create + connection if there is only one remote node configured (no doubt about + which node to connect to). */ +#ifdef ETCP_SINGLE_SOCKET_WITH_ACK + return single_socket_transfer(_client_out, remote_id, true, NULL, 0); +#else + return PJON_FAIL; +#endif + } +#ifdef ETCP_SINGLE_DIRECTION + else if (_initiate_both_sockets_in_same_direction && _initiator) { + connect(remote_id); + uint16_t result = receive(_client_in, false); + disconnect_out_if_needed(PJON_ACK); // Do not disconnect if nothing to read + disconnect_in_if_needed(); + return result; + } +#endif + } else { + // Accept new incoming connection if connection has been lost + if(_single_socket) { +#ifdef ETCP_SINGLE_SOCKET_WITH_ACK + return single_socket_transfer(_client_in, -1, false, NULL, 0); +#else + return PJON_FAIL; +#endif + } else { + // Accept incoming connection(s) + if(!accept()) { + return PJON_FAIL; + } + + uint16_t result = receive(_client_in, !_keep_connection); + + disconnect_in_if_needed(); + return result; + } + } + return PJON_FAIL; + }; + + + uint16_t receive(uint32_t duration_us) + { + uint32_t start = PJON_MICROS(); + uint16_t result = PJON_FAIL; + do { + result = receive(); + if (result != PJON_ACK) { + PJON_DELAY_MICROSECONDS(250); + } + } while( + result != PJON_ACK && + (uint32_t)(PJON_MICROS() - start) <= duration_us + ); + return result; + }; + + + uint16_t send( + uint8_t id, + const uint8_t *packet, + uint16_t length, + uint32_t = 0 // timing_us + ) + { + // Special algorithm for single-socket transfers + if(_single_socket) +#ifdef ETCP_SINGLE_SOCKET_WITH_ACK + return single_socket_transfer( + _server ? _client_in : _client_out, + id, + _server ? false : true, + packet, + length + ); +#else + return PJON_FAIL; +#endif + + // Connect or check that we are already connected to the correct server + bool connected = false; +#ifdef ETCP_SINGLE_DIRECTION + if (_initiate_both_sockets_in_same_direction && !_initiator) { + connected = accept(); + } else +#endif + connected = connect(id); + + // Send the packet and read PJON_ACK + uint16_t result = PJON_FAIL; + if(connected) { + result = send(_client_out, id, packet, length); + } + + // Disconnect + disconnect_out_if_needed(result); + + return result; + }; + + + uint8_t device_id() + { + return _local_id; + }; + + + uint8_t acquire_id() // Not supported yet + { + return 0; + }; + + + void set_error(link_error e) + { + _error = e; + }; + + + void set_id(uint8_t id) + { + _local_id = id; + }; + + + void set_receiver(link_receiver r, void *callback_object) + { + _receiver = r; + _callback_object = callback_object; + }; + + + void update() { }; +}; + +#undef Serial diff --git a/hal/transport/PJON/driver/strategies/EthernetTCP/EthernetTCP.h b/hal/transport/PJON/driver/strategies/EthernetTCP/EthernetTCP.h new file mode 100644 index 000000000..1d3fdf4dd --- /dev/null +++ b/hal/transport/PJON/driver/strategies/EthernetTCP/EthernetTCP.h @@ -0,0 +1,175 @@ + +/* EthernetTCP is a Strategy for the PJON framework (included in version v5.2) + It supports delivering PJON packets over Ethernet TCP. + Compliant with the PJON protocol layer specification v0.3 + _____________________________________________________________________________ + + EthernetTCP strategy and EthernetLink proposed and developed by Fred Larsen + 02/10/2016 + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + +#pragma once + +#include "EthernetLink.h" +#include + +// Recommended receive time for this strategy, in microseconds +#ifndef ETCP_RECEIVE_TIME +#define ETCP_RECEIVE_TIME 0 +#endif + +class EthernetTCP +{ +public: + EthernetLink link; + uint16_t last_send_result = PJON_FAIL; + + /* Caching incoming packet to enable byte for byte delivery */ + + uint8_t *incoming_packet_buf_ptr = NULL; + uint16_t current_buffer_size = 0; + uint16_t incoming_packet_size = 0; + + static void static_receiver( + uint8_t id, + const uint8_t *payload, + uint16_t length, + void *callback_object + ) + { + if(callback_object) { + ((EthernetTCP*)callback_object)->receiver(id, payload, length); + } + }; + + + void receiver(uint8_t /*id*/, const uint8_t *payload, uint16_t length) + { + if(length <= current_buffer_size && incoming_packet_buf_ptr != NULL) { + memcpy(incoming_packet_buf_ptr, payload, length); + incoming_packet_size = length; + } + }; + + + EthernetTCP() + { + link.set_receiver(static_receiver, this); +#ifdef _WIN32 + // Initialize Winsock + WSAData wsaData; + WSAStartup(MAKEWORD(2, 2), &wsaData); +#endif + }; + +#ifdef _WIN32 + ~EthernetTCP() + { + // Cleanup Winsock + WSACleanup(); + }; +#endif + + + /* Returns the suggested delay related to the attempts passed as parameter: */ + + uint32_t back_off(uint8_t attempts) + { + return 10000ul*attempts; + }; + + + /* Begin method, to be called on initialization: + (returns always true) */ + + bool begin(uint8_t /*did*/ = 0) + { + return true; + }; + + + /* Check if the channel is free for transmission */ + + bool can_start() + { + return link.device_id() != 0; + }; + + + /* Returns the maximum number of attempts for each transmission: */ + + static uint8_t get_max_attempts() + { + return 5; + }; + + + /* Returns the recommended receive time for this strategy: */ + + static uint16_t get_receive_time() + { + return ETCP_RECEIVE_TIME; + }; + + + /* Handle a collision (empty because handled on Ethernet level): */ + + void handle_collision() { }; + + + /* Receive a frame: */ + + uint16_t receive_frame(uint8_t *data, uint16_t max_length) + { + // Register supplied buffer as target for EthernetLink callback function + incoming_packet_buf_ptr = data; + current_buffer_size = max_length; + incoming_packet_size = 0; + + // Receive a packet + uint16_t result = link.receive(); + + // Forget about buffer and return result size + uint16_t received_packet_size = incoming_packet_size; + incoming_packet_buf_ptr = NULL; + incoming_packet_size = 0; + + return result == PJON_ACK ? received_packet_size : PJON_FAIL; + } + + + /* Receive byte response */ + + uint16_t receive_response() + { + return last_send_result; + }; + + + /* Send byte response to package transmitter */ + + void send_response(uint8_t /*response*/) // Empty, ACK is always sent + { + }; + + + /* Send a frame: */ + + void send_frame(uint8_t *data, uint16_t length) + { + if(length > 0) + last_send_result = + link.send((uint8_t)data[0], (const uint8_t*)data, length); + }; +}; diff --git a/hal/transport/PJON/driver/strategies/EthernetTCP/README.md b/hal/transport/PJON/driver/strategies/EthernetTCP/README.md new file mode 100644 index 000000000..9432adda2 --- /dev/null +++ b/hal/transport/PJON/driver/strategies/EthernetTCP/README.md @@ -0,0 +1,77 @@ +## EthernetTCP + +| Medium | Pins used | Inclusion | +|--------|-----------|--------------------| +| Ethernet port, wired or WiFi | NA | `#include `| + +With the `EthernetTCP` PJON strategy, multiple devices with Ethernet ports can use PJON to communicate with each other on a LAN, WAN or across the Internet. Take a look at the [video introduction](https://www.youtube.com/watch?v=DQzcAv38yxM) for a brief showcase of its features. + +### Why PJON over Ethernet TCP/IP? +If a cabled or wireless Ethernet network exists, using this to let devices communicate can be easier than to pull new wires or utilize other radio communication modules. + +It can also be useful for connecting physically separate clusters of devices that are connected wired with the `SoftwareBitBang` strategy, or wirelessly with the `Oversampling` strategy, when an Ethernet network is connecting the locations. + +It also enables devices to be connected through the Internet if firewalls are configured to allow this. + +### How to use EthernetTCP +Use `PJONEthernetTCP` to instantiate a PJON object ready to communicate using `EthernetTCP` strategy: +```cpp + #include + + PJONEthernetTCP bus(44); // Use device id 44 +``` +Set up the Ethernet card in the usual manner by calling `Ethernet.begin`, then call setter functions for the link object in the EthernetTCP strategy, and then call the `begin` method on the PJON object: +```cpp + void setup() { + Ethernet.begin(mac, local_ip, gateway, gateway, subnet); + + bus.strategy.link.set_id(bus.device_id()); + bus.strategy.link.add_node(44, remote_ip); + // Do not call this if SINGLE_SOCKET and transmitter + bus.strategy.link.start_listening(); + + bus.set_receiver(receiver_function); + bus.begin(); + } +``` +Note that the device id of each PJON device to communicate with must be added with the `add_node` function along with the device's IP address. The IP addresses can be completely unrelated, allowing it to communicate with devices all over the world as if they were on a local bus. DNS is currently not supported (and would lower the reliability because communication would be depending on the DNS server to be reachable), so all devices to be reached must have static IP addresses. Usually a static IP address can be leased for an Internet connection, sometimes for as low as $1 a month. + +If the device will only talk to one other device with the single-socket configuration (calling `link.single_socket(true)`) or single-direction (calling `link.single_initiate_direction(true)`) it can have a DHCP assigned IP address itself. + +### Single socket +In the standard mode, each device will listen for incoming socket connections on one port while creating outgoing connections to other devices for outgoing packets. So there is one connection in each direction, allowing packets to be sent in parallel in different directions, as far as that goes with single-threaded programs. Establishing sockets in both directions requires both devices to have fixed IP addresses, and if there are firewalls in between, there must be openings or port-forwarding in both directions. + +To simplify traffic crossing firewalls, the use of a single-socket configuration, calling `link.single_socket(true)` is available. It makes one device create a connection to another device with a fixed IP and a firewall opening / port forwarding. Packets can be sent in both directions on this single socket, even if one device has dynamic IP address and no incoming firewall openings. + +A use case for this is having a master device, and a slave device in a different location that connects to the master to send or receive packets. Only the master needs a fixed IP address and one firewall opening / port forwarding. + +Using the single-socket configuration, calling `link.single_socket(true)`, will cause some traffic (poll requests) to flow each time PJON update() or receive() are called even when no packets are being sent. + +Unlike the standard mode, this mode will not establish and close a connection for each packet to be transferred, but keep the socket connection permanently. It will be re-connected if it fails. + +### Single initiate direction +Just like the single-socket mode, this mode allows one device to connect to another device, and let packets flow in both directions. + +Instead of creating one single socket like the single-socket mode, this mode creates one socket for each packet direction, but both sockets are established in the same direction for easy firewall traversal. +This mode is not poll-based like the single-socket approach, so it will not generate network traffic when idle. + +This mode is also based on permanently connected sockets. + +### Use-cases +When communicating on a LAN between multiple devices, the standard mode should be used. This will open a socket from a sender to a receiver for each packet to be sent, send the packet, receive an ACK, and close the socket. +In this scenario, the LocalUDP or GlobalUDP strategies may be smaller and more efficient strategies. + +When communicating through firewalls, or only one of the devices having a fixed IP address, the configuration should be single-direction with `link.single_initiate_direction(true)` or single-socket with `link.single_socket(true)`. + +One use case is to have a PJON program on a computer not able to run a strategy like SWBB take part in a SWBB bus by using a Surrogate. So a Windows/Linux/Raspberry program can communicate with all devices on a SWBB bus via an Arduino running the Surrogate example sketch. The program will talk to the Surrogate via EthernetTCP in single-direction or single-socket mode, and the Surrogate will forward messages in both directions between the ETCP bus and the SWBB bus. + +### EthernetLink worker class +The `EthernetTCP` strategy looks simple because most of the implementation is present in a worker class named `EthernetLink`. This class can be used standalone in some contexts. + +All the other necessary information is present in the general [Documentation](/documentation). + +### Known issues +- Firewall may block `EthernetTCP` packets, edit its configuration to allow them + +### Safety warning +In all cases, when installing or maintaining a PJON network, extreme care must be taken to avoid any danger. When connecting a local bus to the internet using [EthernetTCP](/src/strategies/EthernetTCP) or [GlobalUDP](/src/strategies/GlobalUDP) all connected devices must be considered potentially compromised, potentially manipulated or remotely actuated against your will. It should be considered a good practice not to connect to the internet systems that may create a damage (fire, flood, data-leak) if hacked. diff --git a/hal/transport/PJON/driver/strategies/GlobalUDP/GlobalUDP.h b/hal/transport/PJON/driver/strategies/GlobalUDP/GlobalUDP.h new file mode 100644 index 000000000..226708d8a --- /dev/null +++ b/hal/transport/PJON/driver/strategies/GlobalUDP/GlobalUDP.h @@ -0,0 +1,266 @@ + +/* EthernetUDP is a Strategy for the PJON framework. + It supports delivering PJON packets through Ethernet UDP to a registered list + of devices on the LAN, WAN or Internet. Each device must be registered with + its device id, IP address and listening port number. + ___________________________________________________________________________ + + EthernetUDP strategy proposed and developed by Fred Larsen 01/09/2017 + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + +#pragma once + +#ifdef HAS_ETHERNETUDP +#include "../../interfaces/ARDUINO/UDPHelper_ARDUINO.h" +#else +#include "../../interfaces/LINUX/UDPHelper_POSIX.h" +#endif + +#include + +// Timeout waiting for an ACK. This can be increased if the latency is high. +#ifndef GUDP_RESPONSE_TIMEOUT +#define GUDP_RESPONSE_TIMEOUT 100000ul +#endif + +#ifndef GUDP_MAX_REMOTE_NODES +#define GUDP_MAX_REMOTE_NODES 10 +#endif + +// Recommended receive time for this strategy, in microseconds +#ifndef GUDP_RECEIVE_TIME +#define GUDP_RECEIVE_TIME 0 +#endif + +#define GUDP_DEFAULT_PORT 7000 +#define GUDP_MAGIC_HEADER (uint32_t) 0x0DFAC3FF + +class GlobalUDP +{ + bool _udp_initialized = false; + uint16_t _port = GUDP_DEFAULT_PORT; + bool _auto_registration = true; + + // Remote nodes + uint8_t _remote_node_count = 0; + uint8_t _remote_id[GUDP_MAX_REMOTE_NODES]; + uint8_t _remote_ip[GUDP_MAX_REMOTE_NODES][4]; + uint16_t _remote_port[GUDP_MAX_REMOTE_NODES]; + + UDPHelper udp; + + bool check_udp() + { + if(!_udp_initialized) { + udp.set_magic_header(htonl(GUDP_MAGIC_HEADER)); + if (udp.begin(_port)) { + _udp_initialized = true; + } + } + return _udp_initialized; + }; + + int16_t find_remote_node(uint8_t id) + { + for(uint8_t i = 0; i < _remote_node_count; i++) + if(_remote_id[i] == id) { + return i; + } + return -1; + }; + + void autoregister_sender(const uint8_t *message, uint16_t length) + { + // Add the last sender to the node table + if (_auto_registration && length>4) { + // First get PJON sender id from incoming packet + PJON_Packet_Info packet_info; + PJONTools::parse_header(message, packet_info); + uint8_t sender_id = packet_info.tx.id; + if (sender_id == 0) { + return; // If parsing fails, it will be 0 + } + + // Then get the IP address and port number of the sender + uint8_t sender_ip[4]; + uint16_t sender_port; + udp.get_sender(sender_ip, sender_port); + + // See if PJON id is already registered, add if not + int16_t pos = find_remote_node(sender_id); + if (pos == -1) { + add_node(sender_id, sender_ip, sender_port); + } else { + // Update IP and port of existing node + memcpy(_remote_ip[pos], sender_ip, 4); + _remote_port[pos] = sender_port; + } + } + } + +public: + + /* Register each device we want to send to */ + + int16_t add_node( + uint8_t remote_id, + const uint8_t remote_ip[], + uint16_t port_number = GUDP_DEFAULT_PORT + ) + { + if (_remote_node_count == GUDP_MAX_REMOTE_NODES) { + return -1; + } + _remote_id[_remote_node_count] = remote_id; + memcpy(_remote_ip[_remote_node_count], remote_ip, 4); + _remote_port[_remote_node_count] = port_number; + _remote_node_count++; + return _remote_node_count - 1; + }; + + + /* Select if incoming packets should automatically add their sender as a node */ + + void set_autoregistration(bool enabled) + { + _auto_registration = enabled; + } + + + /* Returns the suggested delay related to attempts passed as parameter: */ + + uint32_t back_off(uint8_t attempts) + { +#ifdef PJON_ESP + return 10000ul * attempts + PJON_RANDOM(10000); +#elif _WIN32 + (void)attempts; // Avoid "unused parameter" warning + return 1000ul + PJON_RANDOM(1000); +#else + (void)attempts; // Avoid "unused parameter" warning + return 1; +#endif + }; + + + /* Begin method, to be called on initialization: + (returns always true) */ + + bool begin(uint8_t /*did*/ = 0) + { + return check_udp(); + }; + + + /* Check if the channel is free for transmission */ + + bool can_start() + { + return check_udp(); + }; + + + /* Returns the maximum number of attempts for each transmission: */ + + static uint8_t get_max_attempts() + { + return 10; + }; + + + /* Returns the recommended receive time for this strategy: */ + + static uint16_t get_receive_time() + { + return GUDP_RECEIVE_TIME; + }; + + + /* Handle a collision (empty because handled on Ethernet level): */ + + void handle_collision() { }; + + + /* Receive a frame: */ + + uint16_t receive_frame(uint8_t *data, uint16_t max_length) + { + uint16_t length = udp.receive_frame(data, max_length); + if (length != PJON_FAIL) { + autoregister_sender(data, length); + } + return length; + } + + + /* Receive byte response */ + + uint16_t receive_response() + { + /* TODO: Improve robustness by ignoring packets not from the previous + receiver (Perhaps not that important as long as ACK/NAK responses are + directed, not broadcast) */ + uint32_t start = PJON_MICROS(); + uint8_t result[8]; + uint16_t reply_length = 0; + do { + reply_length = receive_frame(result, sizeof result); + + // We expect 1, if packet is larger it is not our ACK + if(reply_length == 1) + if (result[0] == PJON_ACK) { + return result[0]; + } + + } while ((uint32_t)(PJON_MICROS() - start) < GUDP_RESPONSE_TIMEOUT); + return PJON_FAIL; + }; + + + /* Send byte response to package transmitter. + We have the IP so we can reply directly. */ + + void send_response(uint8_t response) // Empty, PJON_ACK is always sent + { + udp.send_response(response); + }; + + + /* Send a frame: */ + + void send_frame(uint8_t *data, uint16_t length) + { + if(length > 0) { + uint8_t id = data[0]; // Package always starts with a receiver id + if (id == 0) { // Broadcast, send to all receivers + for(uint8_t pos = 0; pos < _remote_node_count; pos++) { + udp.send_frame(data, length, _remote_ip[pos], _remote_port[pos]); + } + } else { // To a specific receiver + int16_t pos = find_remote_node(id); + if (pos != -1) { + udp.send_frame(data, length, _remote_ip[pos], _remote_port[pos]); + } + } + } + }; + + + /* Set the UDP port: */ + + void set_port(uint16_t port = GUDP_DEFAULT_PORT) + { + _port = port; + }; +}; diff --git a/hal/transport/PJON/driver/strategies/GlobalUDP/README.md b/hal/transport/PJON/driver/strategies/GlobalUDP/README.md new file mode 100644 index 000000000..3947c9d99 --- /dev/null +++ b/hal/transport/PJON/driver/strategies/GlobalUDP/README.md @@ -0,0 +1,58 @@ +## GlobalUDP + +| Medium | Pins used | Inclusion | +|--------|-----------|--------------------| +| Ethernet port, wired or WiFi | NA | `#include `| + +With the `GlobalUDP` PJON strategy, multiple devices with Ethernet ports can use PJON to communicate with each other over an +Ethernet network, wired or over WiFi or both. This strategy demands a little more configuration than the `LocalUDP` strategy +but is not limited to the local network and can therefore reach devices farther away, to another LAN connected through +VPN, or potentially across the Internet (beware of security issues). + +### Why PJON over UDP? +If a cabled or wireless Ethernet network exists, using this to let devices communicate can be easier than to pull new wires or utilize other radio communication modules. + +It can also be useful for connecting physically separate clusters of devices that are connected wired with the `SoftwareBitBang` strategy, or wirelessly with the `Oversampling` strategy, when a LAN or WAN is connecting the locations. + +### How to use GlobalUDP +Use `PJONGlobalUDP` to instantiate a PJON object ready to communicate using `PJONGlobalUDP` strategy: +```cpp + #include + + PJONGlobalUDP bus(44); // Use device id 44 +``` +Set up the Ethernet card in the usual manner by calling `Ethernet.begin`, register the other devices to send to, +then call the `begin` method on the PJON object: +```cpp +void setup() { + Ethernet.begin(mac, local_ip, gateway, gateway, subnet); + bus.strategy.add_node(45, remote_ip1); + bus.strategy.add_node(46, remote_ip2); + bus.begin(); +} +``` +All the IP addresses of the registered nodes should be reachable. UDP port forwarding can be used to obtain this +through firewalls. The IP address of the device can be DHCP assigned if none of the other devices need to reach it +except with ACKs. Otherwise it should be static, unless using sender autoregistration. + +Sender autoregistration is now enabled by default and can be disabled with: + +``` + bus.set_autoregistration(false); +``` + +With sender autoregistration the sender of each incoming packet is automatically registered in the node table, meaning that replies will work also for unregistered devices with static or dynamic IP. Also, after a packet from a device has been received, packets can be sent to it by its PJON id. + +This means that sender autoregistration can be used in setups where packets are exchanged through a central device, typically a master or switch, to let all devices except the central device use DHCP for dynamic network configuration. Each device then need to register the central device in its table, and the central device can have an empty table at startup. + +Note that the preprocessor define `GUDP_MAX_REMOTE_NODES` is important when using autoregistration. For a device it should be higher than the maximum number of other devices it will communicate with. Its default value of 10 is low to save memory, and in larger setups it must be increased. + +UDP packets are _not_ broadcast like with the `LocalUDP` strategy, but directed to a selected receiver. + +All the other necessary information is present in the general [Documentation](/documentation). + +### Known issues +- Firewall may block `GlobalUDP` packets, edit its configuration to allow them + +### Safety warning +In all cases, when installing or maintaining a PJON network, extreme care must be taken to avoid any danger. When connecting a local bus to the internet using [EthernetTCP](/src/strategies/EthernetTCP) or [GlobalUDP](/src/strategies/GlobalUDP) all connected devices must be considered potentially compromised, potentially manipulated or remotely actuated against your will. It should be considered a good practice not to connect to the internet systems that may create a damage (fire, flood, data-leak) if hacked. diff --git a/hal/transport/PJON/driver/strategies/LocalFile/FileLockFunctions.h b/hal/transport/PJON/driver/strategies/LocalFile/FileLockFunctions.h new file mode 100644 index 000000000..de4096f2f --- /dev/null +++ b/hal/transport/PJON/driver/strategies/LocalFile/FileLockFunctions.h @@ -0,0 +1,257 @@ +#include +#include +#include +#include + +#ifdef _WIN32 +#define W_OK 02 +#define R_OK 04 +#define X_OK 04 +#include +#include +#include +#include +#define access _access +#define lseek _lseek +#define locking _locking +#define open _open +#define write _write +#define read _read +#define close _close +#else +#include +#include +#endif + + +//----------------------------------------------------------------------------- +int CheckIfDir(const char *name) +//----------------------------------------------------------------------------- +/* testing if name is a directory */ +{ + struct stat s_buf; + return ((stat(name, &s_buf) != -1) && ((s_buf.st_mode & S_IFMT) == S_IFDIR)); +} + + +//----------------------------------------------------------------------------- +int CheckIfFile(const char *name) +//----------------------------------------------------------------------------- +/* testing if name is a file */ +{ + struct stat s_buf; + return ((stat(name, &s_buf) != -1) && ((s_buf.st_mode & S_IFMT) == S_IFREG)); +} + + +//----------------------------------------------------------------------------- +int CheckForWrite(const char *filepath) +//----------------------------------------------------------------------------- +/* test if a file or directory is writable for current user */ +{ + struct stat s_buf; + return ((stat(filepath, &s_buf) != -1) && s_buf.st_mode && (access(filepath, W_OK) != -1)); +} + + +//----------------------------------------------------------------------------- +int CheckForRead(const char *filepath) +//----------------------------------------------------------------------------- +/* testing if a file or directory is readable for current user */ +{ + struct stat s_buf; + return ((stat(filepath, &s_buf) != -1) && s_buf.st_mode && (access(filepath, R_OK) != -1)); +} + + +//----------------------------------------------------------------------------- +int CheckForExe(const char *filepath) +//----------------------------------------------------------------------------- +/* testing if a file or directory is readable for current user */ +{ + struct stat s_buf; + return ((stat(filepath, &s_buf) != -1) && s_buf.st_mode && (access(filepath, X_OK) != -1)); +} + + +//----------------------------------------------------------------------------- +int GetFileSize(const char *filepath) +//----------------------------------------------------------------------------- +/* testing if a file or directory and return the actual size of the file */ +{ + struct stat s_buf; + + if ( stat( filepath, &s_buf) == -1) { + return (-1); + } else { + return (s_buf.st_size); + } +} + + +// Returns 1 if the section is locked, 0 otherwise. If an error occurs, the +// return value will indicate that the section is locked, to avoid possible +// access collisions or file corruptions. +// NOTE: This function will return 1 if it is not possible to get the requested +// lock because of an already existing lock. If bExclusive is 1 then this is +// straight-forward: a return value of 0 means that no locks are present. +//----------------------------------------------------------------------------- +int IsFileSectionLocked(const int iFileNo, // the fileno + const int nPosition, // start position + const int nSize, // size of section to test + const int bExclusive) // write or read lock +//----------------------------------------------------------------------------- +{ +#ifdef _WIN32 + if (nPosition>=0) { + lseek(iFileNo, nPosition, SEEK_SET); + } + if (locking(iFileNo, LK_NBLCK, nSize)!=0) { + return(1); + } + locking(iFileNo, LK_UNLCK, nSize); + return(0); +#else + struct flock fl; + fl.l_whence = 0; + fl.l_start = nPosition; + fl.l_len = nSize; + fl.l_pid = 0; + int fno = iFileNo; + if (fno==-1) { + return(1); + } + + fl.l_type = bExclusive ? F_WRLCK : F_RDLCK; + + if (fcntl(fno, F_GETLK, &fl)!=0) { + printf("IsFileSectionLocked: fcntl(F_GETLK,...) failed " + "with error %d (%s)\n", errno, strerror(errno)); + return(1); + } + return (fl.l_type != F_UNLCK); +#endif +} + + +//----------------------------------------------------------------------------- +int LockFileSection(const int iFileNo, // the fileno + const int nPosition, // start position + const int nSize, // size of section to lock + const int bLock, // 1=lock, 0=unlock + const int bExclusive, // write or read lock + const int bNonBlocking) // blocking or non-blocking +//----------------------------------------------------------------------------- +{ +#ifdef _WIN32 + // set position + if (nPosition>=0) { + lseek(iFileNo, nPosition, SEEK_SET); + } + + // get the windows file handle + HANDLE hFile = (void *) _get_osfhandle(iFileNo); + if (hFile == INVALID_HANDLE_VALUE) { + printf("LockFileSection: get_ofshandle failed for file %d\n", iFileNo); + return(0); + } + + // lock or unlock + struct _OVERLAPPED ol; + ol.Offset = nPosition; + ol.OffsetHigh = 0; + ol.hEvent = 0; + int bOK = 1; + if (bLock) { + // blocking or non-blocking? + int nMode = LOCKFILE_EXCLUSIVE_LOCK; + if (bNonBlocking) { + nMode |= LOCKFILE_FAIL_IMMEDIATELY; + } + + // Try to lock, repeat until success, an unexpected error, + // or exceeding the max time. + int nRetries = 0, nMaxRetries = 0, iLastErr = 0; + if (!bNonBlocking) { + nMaxRetries = 1000000000; + } + do { + bOK = LockFileEx(hFile, nMode, 0, nSize, 0, &ol); + if (!bOK) { + iLastErr = GetLastError(); + } + if (iLastErr!=ERROR_LOCK_VIOLATION) { + break; + } + Sleep(100); + } while (!bOK && (nMaxRetries>nRetries++)); + } else { + // unlock + bOK = UnlockFileEx(hFile, 0, nSize, 0, &ol); + } + + // log error if any + if (!bOK) { + DWORD iLastErr = GetLastError(); + LPVOID lpMsgBuf; + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | + FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, iLastErr, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language + (LPTSTR) &lpMsgBuf, 0, NULL); + printf("%s of file %d returned error %d (%s)\n", + (const char *)(bLock ? "Locking" : "Unlocking"), + iFileNo, iLastErr, (const char*)(LPCTSTR) lpMsgBuf); + LocalFree(lpMsgBuf); + return(0); + } + return(1); +#else + struct flock fl; + fl.l_whence = 0; + fl.l_start = nPosition; + fl.l_len = nSize<0 ? 0 : nSize; + fl.l_pid = 0; + int fno = iFileNo; + if (fno==-1) { + return(0); + } + + // Now take care to use the lock type specified. If we are trying to + // get a write lock on a file opened for reading only, this will result + // in a 'Bad file number' error (errno 9) on some Unix platforms. + fl.l_type = bExclusive ? F_WRLCK : F_RDLCK; + int retval = 0; + if (bLock) { + if (bNonBlocking) { // non-blocking + retval = (fcntl(fno, F_SETLK, &fl)==0); + } else { // blocking + retval = (fcntl(fno, F_SETLKW, &fl)==0); + if (!retval) { + if (errno == EAGAIN) { + printf("LockFileSection: " + "Lock is being held by process %d\n", fl.l_pid); + } else { + printf("Lock of file %d (proc=%d) returned error %d (%s)\n", + fno, fl.l_pid, errno, strerror(errno)); + } + } + } + } else { + fl.l_type = F_UNLCK; + retval = (fcntl(fno, F_SETLKW, &fl)==0); + if (retval==0) { + printf("LockFileSection: Unlock failed with error %d (%s)\n", errno, strerror(errno)); + } + } + return(retval); +#endif +} + + +#ifdef _WIN32 +#undef W_OK +#undef R_OK +#undef X_OK +#endif + diff --git a/hal/transport/PJON/driver/strategies/LocalFile/LocalFile.h b/hal/transport/PJON/driver/strategies/LocalFile/LocalFile.h new file mode 100644 index 000000000..a7778add1 --- /dev/null +++ b/hal/transport/PJON/driver/strategies/LocalFile/LocalFile.h @@ -0,0 +1,321 @@ + +#pragma once + +#include "FileLockFunctions.h" + +#include "PJON.h" + +// The maximum number of messages in the content file +#ifndef LF_QUEUESIZE +#define LF_QUEUESIZE 20 +#endif + +// The name of the content file +#ifndef LF_FILENAME +#define LF_FILENAME "../PJONLocalFile.dat" +#endif + +// Delay in ms between each check for new packets on the disk +#ifndef LF_POLLDELAY +#define LF_POLLDELAY 10 +#endif + +// Recommended receive time for this strategy, in microseconds +#ifndef LF_RECEIVE_TIME +#define LF_RECEIVE_TIME 0 +#endif + +#define PJON_LF_DEBUG + +class LocalFile +{ +private: + int fn = -1; + + /* The last record read from file, this is remembered to decide which + records have been read and which are unread. Note that record number 0 + is never used, even when overflowing/wrapping around. */ + uint16_t lastRecordIdRead = 0; + + uint16_t last_send_result = PJON_ACK; + + struct Record { + uint16_t length; + uint8_t message[PJON_PACKET_MAX_LENGTH]; + Record() + { + memset(this, 0, sizeof(Record)); + } + }; + + void doOpen(const bool create = false) + { + if(fn != -1) { + return; // Already open + } + int mode = O_RDWR, + permissions = S_IREAD | S_IWRITE; + if(create) { + mode |= O_CREAT; + } +#ifdef _WIN32 + mode |= O_RANDOM | O_BINARY; + _sopen_s(&fn, LF_FILENAME, mode, _SH_DENYNO, permissions); +#else + permissions |= S_IRGRP | S_IWGRP | S_IROTH; + // rw for owner+group, r for others + fn = open(LF_FILENAME, mode, permissions); +#endif + }; + + bool openContentFile() + { + if(fn != -1) { + return true; + } + bool file_exists = CheckIfFile(LF_FILENAME); + if(!file_exists) { + // Create and initialize the file + uint16_t lastRecordId = 0, index[LF_QUEUESIZE]; + memset(index, 0, LF_QUEUESIZE * sizeof(uint16_t)); + doOpen(true); + if(fn != -1) { + lock(); + write(fn, (const char*)&lastRecordId, sizeof(lastRecordId)); + write(fn, (const char*)index, LF_QUEUESIZE*sizeof(uint16_t)); + Record record; + for(uint8_t i=0; i maxDiff) { + maxDiff = diff; + pos = i; + } + } + return pos; + }; + + uint8_t findNextUnreadPosition( + const uint16_t lastRecordId, + const uint16_t index[LF_QUEUESIZE] + ) + { + uint8_t pos = PJON_NOT_ASSIGNED; + if(lastRecordIdRead == 0) { + lastRecordIdRead = lastRecordId - LF_QUEUESIZE; + } + if(lastRecordId == lastRecordIdRead) { + return pos; // Nothing new has arrived + } + uint16_t minDiff = 0xFFFF; + for(uint8_t i = 0; i < LF_QUEUESIZE; i++) { + if(index[i] == 0 || index[i] == lastRecordIdRead) { + continue; // Never used or already read + } + uint16_t diff = (uint16_t)(index[i] - lastRecordIdRead); + if(diff < 0x8FFF && diff < minDiff) { + minDiff = diff; + pos = i; + } + } + return pos; + }; + +public: + + ~LocalFile() + { + closeContentFile(); + }; + + uint32_t back_off(uint8_t attempts) + { + return 1000 * attempts; + }; + + bool begin(uint8_t did) + { + return openContentFile(); + }; + + void handle_collision() + { + PJON_DELAY(10); + }; + + bool can_start() + { + if(fn == -1) { + openContentFile(); + } + return fn != -1; + }; + + uint8_t get_max_attempts() + { + return 10; + }; + + static uint16_t get_receive_time() + { + return LF_RECEIVE_TIME; + }; + + uint16_t receive_frame(uint8_t *data, uint16_t max_length) + { + Record record; + if(readNextPacketFromFile(record)) { + uint16_t length = + record.length < max_length ? record.length : max_length; + memcpy(data, record.message, length); + return length; + // Relax polling to avoid stressing the disk and CPU too much + } else { + PJON_DELAY(LF_POLLDELAY); + } + return PJON_FAIL; + }; + + uint16_t receive_response() + { + return last_send_result; + }; + + void send_response(uint8_t response) { }; + + void send_frame(uint8_t *data, uint16_t length) + { + Record record; + memcpy(&record.message, data, length); + record.length = length; + bool ok = writePacketToFile(record); + last_send_result = ok ? PJON_ACK : PJON_FAIL; + }; +}; diff --git a/hal/transport/PJON/driver/strategies/LocalFile/README.md b/hal/transport/PJON/driver/strategies/LocalFile/README.md new file mode 100644 index 000000000..1c5e1efbf --- /dev/null +++ b/hal/transport/PJON/driver/strategies/LocalFile/README.md @@ -0,0 +1,35 @@ +## LocalFile + +| Medium | Pins used | Inclusion | +|--------|-----------|--------------------| +| System memory | NA | `#include `| + +`LocalFile` uses a file present on the hard drive to let multiple processes communicate on the same machine. It can be used for inter-process communication and for developing, simulating and testing applications and networks on a real time operative system without having to deploy physical hardware. + +### Configuration + +Before including the library it is possible to configure `LocalFile` using predefined constants: + +| Constant | Purpose | Supported value | +| ------------------ |--------------------------------------------- | ------------------------------------------ | +| `LF_POLLDELAY` | Poll interval | Duration in milliseconds (20 by default) | +| `LF_FILENAME` | Name and location of the file used as medium | Duration in microseconds (1500 by default) | +| `LF_QUEUESIZE` | Size of the packets queue | > 0 (20 by default) | + +Use `PJONLocalFile` to instantiate a PJON object ready to communicate using `LocalFile` strategy: +```cpp + #include + + PJONLocalFile bus(44); // Use device id 44 +``` +After testing or simulation you may want to use conditional compiling and exchange this strategy with the actual one on your target hardware. + +The directory [examples/LINUX/Local/LocalFile/PingPong](../../../examples/LINUX/Local/LocalFile/PingPong) contains examples. To build these on Linux, simply type "make". To build on Windows, open the solution file in Visual Studio 2017. + +Reading messages is based on polling. The poll interval in milliseconds is defined by the pre-processor definition `LF_POLLDELAY`. Decreasing this value will increase the communication speed but also use more CPU and cause more disk activity. + +The strategy uses a file where messages are persisted. A queue of the last messages is kept there, with the queue size set by the pre-processor define `LF_QUEUESIZE`. + +### Known issues +- Will create the file `PJONLocalFile.dat` in the parent directory. This +file can be deleted when your client is not running. The name and location of the file can be specified using the pre-processor define `LF_FILENAME` which is `../PJONLocalFile.dat` by default. diff --git a/hal/transport/PJON/driver/strategies/LocalUDP/LocalUDP.h b/hal/transport/PJON/driver/strategies/LocalUDP/LocalUDP.h new file mode 100644 index 000000000..21e0e3961 --- /dev/null +++ b/hal/transport/PJON/driver/strategies/LocalUDP/LocalUDP.h @@ -0,0 +1,168 @@ + +/* LocalUDP is a Strategy for the PJON framework (included in version v5.2) + It supports delivering PJON packets over Ethernet UDP on local network (LAN). + Compliant with the PJON protocol layer specification v0.3 + _____________________________________________________________________________ + + LocalUDP strategy proposed and developed by Fred Larsen 02/10/2016 + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + +#pragma once + +#ifdef HAS_ETHERNETUDP +#include "../../interfaces/ARDUINO/UDPHelper_ARDUINO.h" +#else +#include "../../interfaces/LINUX/UDPHelper_POSIX.h" +#endif + +#include + +#define LUDP_DEFAULT_PORT 7100 +#ifndef LUDP_RESPONSE_TIMEOUT +#define LUDP_RESPONSE_TIMEOUT (uint32_t) 100000 +#endif +#define LUDP_MAGIC_HEADER (uint32_t) 0x0DFAC3D0 + +// Recommended receive time for this strategy, in microseconds +#ifndef LUDP_RECEIVE_TIME +#define LUDP_RECEIVE_TIME 0 +#endif + +class LocalUDP +{ + bool _udp_initialized = false; + uint16_t _port = LUDP_DEFAULT_PORT; + UDPHelper udp; + + bool check_udp() + { + if(!_udp_initialized) { + udp.set_magic_header(htonl(LUDP_MAGIC_HEADER)); + if (udp.begin(_port)) { + _udp_initialized = true; + } + } + return _udp_initialized; + }; + +public: + /* Returns the suggested delay related to the attempts passed as parameter: */ + + uint32_t back_off(uint8_t attempts) + { +#ifdef PJON_ESP + return 10000ul * attempts + PJON_RANDOM(10000); +#elif _WIN32 + return 1000ul * attempts + PJON_RANDOM(1000); +#else + (void)attempts; // Avoid "unused parameter" warning + return 1; +#endif + }; + + + /* Begin method, to be called on initialization: + (returns always true) */ + + bool begin(uint8_t /*did*/ = 0) + { + return check_udp(); + }; + + + /* Check if the channel is free for transmission */ + + bool can_start() + { + return check_udp(); + }; + + + /* Returns the maximum number of attempts for each transmission: */ + + static uint8_t get_max_attempts() + { + return 10; + }; + + + /* Returns the recommended receive time for this strategy: */ + + static uint16_t get_receive_time() + { + return LUDP_RECEIVE_TIME; + }; + + + /* Handle a collision (empty because handled on Ethernet level): */ + + void handle_collision() { }; + + + /* Receive a frame: */ + + uint16_t receive_frame(uint8_t *data, uint16_t max_length) + { + return udp.receive_frame(data, max_length); + } + + + /* Receive byte response */ + + uint16_t receive_response() + { + /* TODO: Improve robustness by ignoring packets not from the previous + receiver (Perhaps not that important as long as ACK/NAK responses are + directed, not broadcast) */ + uint32_t start = PJON_MICROS(); + uint8_t result[6]; + uint16_t reply_length = 0; + do { + reply_length = receive_frame(result, sizeof result); + // We expect 1, if packet is larger it is not our ACK. + // When an ACK is received we know it is for us because an ACK + // will never be broadcast but directed. + if(reply_length == 1) + if(result[0] == PJON_ACK) { + return result[0]; + } + } while ((uint32_t)(PJON_MICROS() - start) < LUDP_RESPONSE_TIMEOUT); + return PJON_FAIL; + }; + + + /* Send byte response to package transmitter. + We have the IP so we can skip broadcasting and reply directly. */ + + void send_response(uint8_t response) // Empty, PJON_ACK is always sent + { + udp.send_response(response); + }; + + + /* Send a frame: */ + + void send_frame(uint8_t *data, uint16_t length) + { + udp.send_frame(data, length); + }; + + + /* Set the UDP port: */ + + void set_port(uint16_t port = LUDP_DEFAULT_PORT) + { + _port = port; + }; +}; diff --git a/hal/transport/PJON/driver/strategies/LocalUDP/README.md b/hal/transport/PJON/driver/strategies/LocalUDP/README.md new file mode 100644 index 000000000..05dc32c42 --- /dev/null +++ b/hal/transport/PJON/driver/strategies/LocalUDP/README.md @@ -0,0 +1,39 @@ +## LocalUDP + +| Medium | Pins used | Inclusion | +|--------|-----------|--------------------| +| Ethernet port, wired or WiFi | NA | `#include `| + +With the `LocalUDP` PJON strategy, multiple devices with Ethernet ports can use PJON to communicate with each other on a local subnet, wired or over WiFi or both. Take a look at the [video introduction](https://www.youtube.com/watch?v=cxEUqkK5BQg) for a brief showcase of its features. + +### Why PJON over UDP? +If a cabled or wireless Ethernet network exists, using this to let devices communicate can be easier than to pull new wires or utilize other radio communication modules. + +It can also be useful for connecting physically separate clusters of devices that are connected wired with the SoftwareBitBang strategy, or wirelessly with the Oversampling strategy, when a LAN is connecting the locations. + +### How to use LocalUDP +Use `PJONLocalUDP` to instantiate a PJON object ready to communicate using `LocalUDP` strategy: +```cpp + #include + + PJONLocalUDP bus(44); // Device id 44 +``` +Set up the Ethernet card in the usual manner by calling `Ethernet.begin`, then call the `begin` method on the PJON object: +```cpp + void setup() { + Ethernet.begin(mac, local_ip, gateway, gateway, subnet); + bus.begin(); + } +``` +The IP address used is irrelevant as long as it is on a subnet common with the other devices it shall communicate with. +Using DHCP assigned IP addresses is fine, and the strategy does not need to relate to it. +The strategy will broadcast the packets, and the correct receiver will pick them up and ACK if requested. Other devices will observe but ignore packets not meant for them. + +All the other necessary information is present in the general [Documentation](/documentation). + +### Known issues +- Firewall may block `LocalUDP` packets, edit its configuration to allow them +- If using `LocalUDP` on a LAN with an attached WiFi router in access point mode, high `LocalUDP` traffic may lower the WiFi bandwidth because the access point sends `LocalUDP` broadcasts over WiFi. If this is a problem, `DualUDP` strategy may be a better alternative. + +### Safety warning +In all cases, when installing or maintaining a PJON network, extreme care must be taken to avoid any danger. When connecting a local bus to the internet using [EthernetTCP](/src/strategies/EthernetTCP) or [GlobalUDP](/src/strategies/GlobalUDP) all connected devices must be considered potentially compromised, potentially manipulated or remotely actuated against your will. It should be considered a good practice not to connect to the internet systems that may create a damage (fire, flood, data-leak) if hacked. diff --git a/hal/transport/PJON/driver/strategies/MQTTTranslate/MQTTTranslate.h b/hal/transport/PJON/driver/strategies/MQTTTranslate/MQTTTranslate.h new file mode 100644 index 000000000..ee4e4f319 --- /dev/null +++ b/hal/transport/PJON/driver/strategies/MQTTTranslate/MQTTTranslate.h @@ -0,0 +1,603 @@ + +/* MQTTTranslate uses the ReconnectingMqttClient + https://github.com/fredilarsen/ReconnectingMqttClient + library to deliver PJON packets over TCP on local network (LAN) as a MQTT + protocol client. + + This strategy works in one of four modes. + The first two modes are for allowing a PJON bus via MQTT, the first mode is + "closed" and the second is "open" to use by non-PJON programs. + The last two modes are for behaving like MQTT devices normally do. + + * "Raw bus mode" will send the binary JSON packets delivered to a topic like + pjon/device45 (where 45 is a receiver device id). Each device + will subscribe to a topic with its own name and will receive packets like + from any other PJON strategy. This strategy requires that all senders and + receivers are linked with PJON for encoding/decoding, so other systems + are not easily connected. + + * "JSON bus mode" will send JSON packets with to, from and data, delivered + to a topic like pjon/device45 (where 45 is a receiver device id). Each + device will subscribe to a topic with its own name and will receive + packets like + {to:45,from:44,data:"message text sent from device 44 to device 45"}. + + * "Device mirror, translating" mode will not use JSON encapsulation of + values, and will publish to its own topic, not the receiver's. It will + publish to a "output" folder and subscribe to an "input" folder. An + outgoing packet with payload "P=44.1,T=22.0" would result in the topics + pjon/device44/output/temperature, with a value "44.1" + pjon/device44/output/pressure, with a value "22.0" + Likewise, a receiving an update of: + pjon/device44/input/setpoint, with a value "45" + would result in a packet with payload "S=45". + This mode supports a translation table to allow short names to be used + in packets while topic names are longer. For example "T" -> "temperature". + If no translation table is populated, the same names will be used in + the packets and the topics. + + * "Device mirror, direct" works like the first device mirror mode, but will + just pass the payload on without any translation, leaving the formatting + to the user. It will not split packets into separate topics but transfer + the packets as-is to one output topic and from one input topic: + pjon/device44/output + pjon/device44/input + The user sketch will have control of the format used, which can be + plain text like "P=44.1,T=22.0" or a JSON text. + + The "Translate" in the strategy name is because a translation table can be + used to translate PJON packet contents to MQTT topics and back. This is to + enable PJON packets to remain small ("t=44.3") between devices with limited + memory, while the MQTT packets are made more explicit ("temperature") to + support longer name syntax in external systems. + + The preprocessor define MQTTT_USE_MAC can be set when using one of the + MIRROR modes, to change the topic name from i.e. /pjon/device44/output to + pjon/AE7804FEA7D0/output. This can be useful to avoid setting a device id + and instead just use the MAC address as a unique subject identifier. + + Compliant with the PJON protocol layer specification v4.0 + _____________________________________________________________________________ + + MQTTTranslate strategy proposed and developed by Fred Larsen 07/12/2019 + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + +#pragma once +#include +#include +#ifndef ARDUINO +#include +#endif + +#define MQTTT_DEFAULT_PORT 1883 +#ifndef MQTTT_RESPONSE_TIMEOUT +#define MQTTT_RESPONSE_TIMEOUT (uint32_t) 10000 +#endif + +// This is the maximum size of MQTT packets after translation +#ifndef MQTTT_BUFFER_SIZE +#define MQTTT_BUFFER_SIZE (uint32_t) PJON_PACKET_MAX_LENGTH +#endif + +// Max size of key and value in MQTTT_MODE_MIRROR_TRANSLATE mode +#ifndef MQTTT_KEY_SIZE +#define MQTTT_KEY_SIZE 15 +#endif +#ifndef MQTTT_VALUE_SIZE +#define MQTTT_VALUE_SIZE 15 +#endif + +#define MQTTT_MODE_BUS_RAW 0 +#define MQTTT_MODE_BUS_JSON 1 +#define MQTTT_MODE_MIRROR_TRANSLATE 2 +#define MQTTT_MODE_MIRROR_DIRECT 3 + +// Select which mode to use +#ifndef MQTTT_MODE +#define MQTTT_MODE MQTTT_MODE_BUS_RAW +#endif + +// The maximum number of keys to be translated in MIRROR_TRANSLATE mode +#ifndef MQTTT_TRANSLATION_TABLE_SIZE +#define MQTTT_TRANSLATION_TABLE_SIZE 5 +#endif + +// Recommended receive time for this strategy, in microseconds +#ifndef MQTTT_RECEIVE_TIME +#define MQTTT_RECEIVE_TIME 0 +#endif + +#if defined(MQTTT_USE_MAC) && ((MQTTT_MODE == MQTTT_MODE_MIRROR_TRANSLATE) || (MQTTT_MODE == MQTTT_MODE_MIRROR_DIRECT)) +#define MQTTT_MAC +#endif + +class MQTTTranslate +{ + bool last_send_success = false; + + uint16_t incoming_packet_size = 0; + // TODO: Eliminate extra buffer -- is the on in the MqttClient not enough? + uint8_t packet_buffer[MQTTT_BUFFER_SIZE]; + PJON_Packet_Info _packet_info; // Used for parsing incoming and outgoing packets + +#ifdef MQTTT_MAC + uint8_t mac[6]; + + char *add_mac(char *p) + { + sprintf(p, "/%2X%2X%2X%2X%2X%2X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + p += strlen(p); + return p; + } +#endif + +#if (MQTTT_MODE == MQTTT_MODE_MIRROR_TRANSLATE) + char key[MQTTT_KEY_SIZE]; + char value[MQTTT_VALUE_SIZE]; + // Translation table + uint8_t translation_count = 0; + char pjon_keys[MQTTT_TRANSLATION_TABLE_SIZE][MQTTT_KEY_SIZE]; + char mqtt_keys[MQTTT_TRANSLATION_TABLE_SIZE][MQTTT_KEY_SIZE]; + + bool translate(char *key, uint8_t len, bool to_mqtt) + { + for (uint8_t i=0; ireceiver(topic, payload, len); + } + } + +#if (MQTTT_MODE == MQTTT_MODE_BUS_JSON) + bool find_next_json_key(const char **p, const char *last) + { + while (**p && *p < last && **p != '{' && **p != ',') { + (*p)++; // Find start brace or comma + } + if (!**p || *p >= last) { + return false; + } + (*p)++; // Skip start brace or comma {"to":44,"from":"45","data":"sgfsdf"} + while (**p && *p < last && **p != '\"') { + (*p)++; // Skip until double quote + } + if (**p != '\"' || *p >= last) { + return false; + } + (*p)++; // Point to first char of key + return true; + } + + bool find_next_json_value(const char **p, const char *last) + { + while (**p && *p < last && **p != ':') { + (*p)++; // Find colon + } + if (!**p || *p >= last) { + return false; + } + (*p)++; // Skip colon {"to":44,"from":"45","data":"sgfsdf"} + while (**p && *p < last && (**p == ' ' || **p == '\t')) { + (*p)++; // Skip potential whitespace + } + if (!**p || *p >= last) { + return false; + } + if (**p == '\"' && *p < last) { + (*p)++; // Skip leading quote if present + } + return true; + } +#endif + + void receiver(const char *topic, const uint8_t *payload, uint16_t len) + { +#if (MQTTT_MODE == MQTTT_MODE_BUS_RAW) + if(len <= MQTTT_BUFFER_SIZE) { + memcpy(packet_buffer, payload, len); + incoming_packet_size = len; + } +#endif +#if (MQTTT_MODE == MQTTT_MODE_BUS_JSON) + // Must assume that payload is text, unless UUencoding/base64encoding + // {"to": to_id, "from": from id, "data": "payload"} + uint8_t sender_id = 0, receiver_id = 0; + const char *p = (const char*)payload, *last = p+len-1; + bool found = false; + while (find_next_json_key(&p, last)) { // to, from or data + if (strncmp(p, "to\"", 3)==0 && find_next_json_value(&p, last)) { + receiver_id = atoi(p); + } else if (strncmp(p, "from\"", 5)==0 && find_next_json_value(&p, last)) { + sender_id = atoi(p); + } else if (strncmp(p, "data\"", 5)==0 && find_next_json_value(&p, last)) { + const char *p2 = p; + while (p2-(const char*)payload+1 < len && *p2 && *p2 != '\"') { + p2++; + } + if (*p2 == '\"') { + found = true; + payload = (uint8_t*)p; + len = p2 - p; + } + } + } + if (receiver_id == 0 || !found) { + return; + } + // Package the data message into a PJON packet + uint8_t h = header; + if (sender_id != 0) { + header |= PJON_TX_INFO_BIT; + } + incoming_packet_size = PJONTools::compose_packet(sender_id, bus_id, receiver_id, + bus_id, packet_buffer, payload, len, h); +#endif +#if (MQTTT_MODE == MQTTT_MODE_MIRROR_TRANSLATE || MQTTT_MODE == MQTTT_MODE_MIRROR_DIRECT) + uint8_t receiver_id = my_id; +#ifdef MQTTT_MAC + // Parse topic to get source device MAC + const char *device_start = strstr(topic, "/"); +#else + // Parse topic to get source device id + const char *device_start = strstr(topic, "/device"); + if (device_start) { + receiver_id = (uint8_t) atoi(&device_start[7]); + } +#endif +#endif +#if (MQTTT_MODE == MQTTT_MODE_MIRROR_TRANSLATE) + // Split into multiple topics (must assume a specific payload format to parse): + // "T=44.1,P=1.1" -> + // pjon/device44/output/temperature 44.1 + // pjon/device44/output/pressure 1.1 + if (device_start) { + // Find start of /input/ + const char *start = (const char*)memchr(device_start+1, '/', + (const char*)payload-device_start+len-2); + // Find end of /input/ + if (start) { + start = (const char*)memchr(start+1, '/', (const char*)payload-start+len-2); + } + if (start) { // Get variable name + uint8_t l = min(start - device_start + len -1, sizeof key -1); + strncpy(key, start+1, l); + key[l] = 0; // Null terminate + translate(key, sizeof key, false); + l = min(len, sizeof value-1); + strncpy(value, (const char*)payload, l); + value[l] = 0; // Null terminate + String s = key; + s += "="; + s += value; + // Package the key=value into a PJON packet + incoming_packet_size = PJONTools::compose_packet(receiver_id, bus_id, receiver_id, + bus_id, packet_buffer, s.c_str(), s.length()+1, header); + } + } + return; +#endif +#if (MQTTT_MODE == MQTTT_MODE_MIRROR_DIRECT) + // Package the payload as it is, into a PJON packet + incoming_packet_size = PJONTools::compose_packet(receiver_id, bus_id, receiver_id, + bus_id, packet_buffer, payload, len, header); +#endif + } + +public: + ReconnectingMqttClient mqttclient; + bool retain = false; // Leave message in broker for clients to receive at connect + uint8_t qos = 0; // Set this to 1 to have guaranteed delivery + String topic = "pjon"; // e.g. "pjon" without trailing slash + uint8_t my_id = PJON_NOT_ASSIGNED; + uint8_t bus_id[4] = {0,0,0,0}; + uint8_t header = 0; + bool lowercase_topics = true; + bool subscribe_all = false; // Read input from all devices or just my own topic? + + void set_config(uint8_t id, const uint8_t bus_id[4], uint8_t header) + { + my_id = id; + if (bus_id != NULL) { + memcpy(this->bus_id, bus_id, 4); + } + this->header = header; + } + void set_qos(uint8_t qos) + { + this->qos = qos; + } + void set_retain(bool retain) + { + this->retain = retain; + } + void set_topic(const char *topic) + { + this->topic = topic; + } + + + /* Subscribe to input from all devices, not only this device? + This is needed to use router mode in PJON. + */ + + void set_subscribe_all(bool yes) + { + subscribe_all = yes; + } + + + /* Set the broker's ip, the port used and the topic */ + + void set_address( + const uint8_t server_ip[4], + const uint16_t server_port, + const char *client_id + ) + { + mqttclient.set_address(server_ip, server_port, client_id); + } + + + /* Returns the suggested delay related to the attempts passed as parameter: */ + + uint32_t back_off(uint8_t attempts) + { + return 10000; + }; + + + /* Begin method, to be called on initialization */ + + bool begin(uint8_t device_id = 0) + { +#ifdef MQTTT_MAC + PJON_GET_MAC(mac); +#endif + my_id = device_id; + mqttclient.set_receive_callback(static_receiver, this); + char *p = (char*)packet_buffer; + strcpy(p, topic.c_str()); + p += strlen(p); +#ifdef MQTTT_MAC + // In this mode (MIRROR modes), I are only interested in my own input + p = add_mac(p); // Adds slash and MAC +#else + if (subscribe_all) { + strcpy(p, "/+"); // Pick up for all devices + p += 2; + } else { // For one single device id + strcpy(p, "/device"); + p += strlen(p); + p += mqttclient.uint8toa(device_id, p); // Now like pjon/device44 + } +#endif +#if (MQTTT_MODE == MQTTT_MODE_MIRROR_TRANSLATE) + strcat(p, "/input/+"); +#endif +#if (MQTTT_MODE == MQTTT_MODE_MIRROR_DIRECT) + strcat(p, "/input"); // Only one input topic +#endif + mqttclient.subscribe((const char*)packet_buffer, qos); + return mqttclient.connect(); + }; + + + /* Check if the channel is free for transmission */ + + bool can_start() + { + return mqttclient.connect(); + }; + + + /* Returns the maximum number of attempts for each transmission: */ + + static uint8_t get_max_attempts() + { + return 0; + }; + + + /* Returns the recommended receive time for this strategy: */ + + static uint16_t get_receive_time() + { + return MQTTT_RECEIVE_TIME; + }; + + + /* Handle a collision (empty because handled on Ethernet level): */ + + void handle_collision() { }; + + + /* Receive a frame: */ + + uint16_t receive_frame(uint8_t *data, uint16_t max_length) + { + if (incoming_packet_size == 0) { + mqttclient.update(); + } + if (incoming_packet_size > 0 && incoming_packet_size <= max_length) { + memcpy(data, packet_buffer, incoming_packet_size); + uint16_t len = incoming_packet_size; + incoming_packet_size = 0; // Flag as handled + return len; + } + return PJON_FAIL; + } + + + /* Receive byte response */ + + uint16_t receive_response() + { + return last_send_success ? PJON_ACK : PJON_FAIL; + }; + + + /* Send byte response to package transmitter */ + + void send_response(uint8_t response) // Empty + { + }; + + + /* Send a frame: */ + + void send_frame(uint8_t *data, uint16_t length) + { + // Extract some info from the packet header + PJONTools::parse_header(data, _packet_info); + + // Compose topic + uint8_t len = strlen(topic.c_str()); + if (len >= SMCTOPICSIZE) { + return; + } + strcpy(mqttclient.topic_buf(), topic.c_str()); + char *p = &mqttclient.topic_buf()[len]; +#ifdef MQTTT_MAC + if (p-mqttclient.topic_buf()+7+7 >= SMCTOPICSIZE) { + return; + } + p = add_mac(p); + strcpy(p, "/output"); + p += strlen(p); // End of /output +#else + if (p-mqttclient.topic_buf()+7+3+7 >= SMCTOPICSIZE) { + return; + } + strcpy(p, "/device"); + p += 7; +#if (MQTTT_MODE == MQTTT_MODE_MIRROR_TRANSLATE || MQTTT_MODE == MQTTT_MODE_MIRROR_DIRECT) + p += mqttclient.uint8toa(_packet_info.tx.id, p); + strcat(p, "/output"); // Like pjon/device44/output + p = &p[strlen(p)]; // End of /output +#else // One of the bus modes, publish to receiver device + mqttclient.uint8toa(_packet_info.rx.id, p); +#endif +#endif +#if (MQTTT_MODE != MQTTT_MODE_BUS_RAW) + uint8_t overhead = PJONTools::packet_overhead(_packet_info.header); + uint8_t crc_size = PJONTools::crc_overhead(_packet_info.header); +#endif + + // Re-compose packet in other modes than RAW +#if (MQTTT_MODE == MQTTT_MODE_MIRROR_TRANSLATE) + // Split into multiple topics (must assume a specific payload format to parse): + // "T=44.1,P=1.1" -> + // pjon/device44/output/temperature 44.1 + // pjon/device44/output/pressure 1.1 + uint8_t send_cnt = 0; + const char *d = (const char*)&data[overhead - crc_size], *v = d, *c, *e; + uint16_t plen = length - overhead; + while (v && (c = find_value_separator(v, d-v+plen))) { + if (e = (const char *)memchr(v, '=', (uint16_t)(c-v))) { + uint8_t l = min(e-v, sizeof key-1); + strncpy(key, v, l); // Complete topic like /pjon/device44/output/temperature + key[l] = 0; + l = min(c-e-1, sizeof value-1); + strncpy(value, e+1, l); + value[l] = 0; + if (!translate(key, sizeof key, true)) + if (lowercase_topics) for (char *k=key; *k!=0; k++) { + *k = tolower(*k); + } + if (p-mqttclient.topic_buf()+1+strlen(key) >= SMCTOPICSIZE) { + return; + } + *p = '/'; + strcpy(p+1, key); + send_cnt += mqttclient.publish(mqttclient.topic_buf(), (uint8_t*)value, strlen(value), retain, qos); + v = c-d >= plen ? NULL : c+1; + } + } + last_send_success = send_cnt > 0; + return; // We have sent multiple smaller packets, just return +#endif +#if (MQTTT_MODE == MQTTT_MODE_MIRROR_DIRECT) + // Post just the payload as it is, to the output topic + data = &data[overhead - crc_size]; + length -= overhead; +#endif +#if (MQTTT_MODE == MQTTT_MODE_BUS_JSON) + // Must assume that payload is text, unless UUencoding/base64encoding + // {"to": to_id, "from": from id, "data": "payload"} + p = (char *) packet_buffer; + if (6+3+8+3+9+payload_len+2 >= MQTTT_BUFFER_SIZE) { + return; + } + strcpy(p, "{\"to\":"); + p += 6; + p += mqttclient.uint8toa(_packet_info.rx.id, p); + strcpy(p, ",\"from\":"); + p+= 8; + p += mqttclient.uint8toa(_packet_info.tx.id, p); + strcpy(p, ",\"data\":\""); + p+= 9; + uint8_t payload_len = length - overhead; + strncpy(p, (const char*)&data[overhead - crc_size], payload_len); + p[payload_len] = 0; + p += strlen(p); + strcpy(p, "\"}"); + p += 2; + data = packet_buffer; + length = ((uint8_t*)p - packet_buffer); +#endif + + // Publish + last_send_success = mqttclient.publish(mqttclient.topic_buf(), data, length, retain, qos); + }; + + const char *find_value_separator(const char *value, uint16_t len) + { + // This does the job of a strchr but accepting that null-terminator may be missing + const char *p = value; + while (p != NULL && (p-value < len) && *p != ',' && *p != 0) { + p++; + } + return p; + } + +#if (MQTTT_MODE == MQTTT_MODE_MIRROR_TRANSLATE) + bool add_translation(const char *pjon_key, const char *mqtt_key) + { + if (translation_count >= MQTTT_TRANSLATION_TABLE_SIZE) { + return false; + } + strncpy(pjon_keys[translation_count], pjon_key, MQTTT_KEY_SIZE); + pjon_keys[translation_count][MQTTT_KEY_SIZE-1] = 0; + strncpy(mqtt_keys[translation_count], mqtt_key, MQTTT_KEY_SIZE); + mqtt_keys[translation_count][MQTTT_KEY_SIZE-1] = 0; + for (char *p=mqtt_keys[translation_count]; *p!=0; p++) { + *p = tolower(*p); + } + translation_count++; + return true; + } +#endif +}; diff --git a/hal/transport/PJON/driver/strategies/MQTTTranslate/README.md b/hal/transport/PJON/driver/strategies/MQTTTranslate/README.md new file mode 100644 index 000000000..85b039863 --- /dev/null +++ b/hal/transport/PJON/driver/strategies/MQTTTranslate/README.md @@ -0,0 +1,47 @@ +## MQTTTranslate + +| Medium | Pins used | Inclusion Constant | +|--------|-----------|--------------------| +| MQTT protocol | NA | `#include `| + +MQTTTranslate uses the [ReconnectingMqttClient](https://github.com/fredilarsen/ReconnectingMqttClient) library (minimum version required v1.1.1) to deliver PJON packets over TCP on local network (LAN) as a MQTT protocol client. It may be useful to connect PJON networks and more standard applications to each other using the MQTT protocol. This strategy works in one of four modes. The first two modes enable to implement a PJON bus via MQTT, the first mode is "closed" and the second is "open" to use by non-PJON programs. The last two modes are for behaving like MQTT devices normally do. + +1. `MQTTT_MODE_BUS_RAW` mode sends binary JSON packets delivered to a topic like `pjon/device45` (where `45` is a receiver device id). Each device subscribes to a topic with its own name and receives packets like any other PJON strategy. This mode requires that all senders and receivers are linked with PJON for encoding/decoding, so other systems are not easily connected. The directory [examples/WINX86/Local/MQTTTranslate/PingPong](../../../examples/WINX86/Local/MQTTTranslate/PingPong) contains examples for windows, to build it, open the solution file in Visual Studio 2017. The directory [examples/ARDUINO/Local/MQTTTranslate/PingPong](../../../examples/ARDUINO/Local/MQTTTranslate/PingPong) contains the Arduino examples, to build them, just use the Arduino IDE. + +2. `MQTTT_MODE_BUS_JSON` mode sends JSON packets with to, from and data, delivered to a topic like `pjon/device45` (where `45` is a receiver device id). Each device subscribes to a topic with its own name and receives packets like `{to:45,from:44,data:"message text sent from device 44 to device 45"}`. + +3. `MQTTT_MODE_MIRROR_TRANSLATE` mode does not not use JSON encapsulation of values, and publishes to its own topic, not the receiver's. It publishes to an "output" folder and subscribes to an "input" folder. An outgoing packet with payload `P=44.1,T=22.0` results in the topics `pjon/device44/output/temperature`, with a value `22.0` and `pjon/device44/output/pressure`, with a value `44.1`. Likewise, when receiving an update of `pjon/device44/input/setpoint`, with a value `45` results in a packet with payload `S=45`. This mode supports a translation table to allow short names to be used in packets while topic names are longer. For example `T` translated in `temperature`. If no translation table is populated, the same names will be used in the packets and the topics. The directory [examples/ESP8266/Local/MQTTTranslate/EnvironmentController](../../../examples/ESP8266/Local/MQTTTranslate/EnvironmentController) contains the ESP8266 example, to build it, just use the Arduino IDE. + +4. `MQTTT_MODE_MIRROR_DIRECT` mode works like `MQTTT_MODE_MIRROR_TRANSLATE`, but just passes the payload on without any translation, leaving the formatting to the user. It does not split packets into separate topics but transfers the packets as-is to one output topic and from one input topic `pjon/device44/output`, `pjon/device44/input`. The user sketch will have control of the format used, which can be plain text like `P=44.1,T=22.0` or a JSON text. The directory [examples/ARDUINO/Local/MQTTTranslate/SWBB-MQTT-Gateway](../../../examples/ARDUINO/Local/MQTTTranslate/SWBB-MQTT-Gateway) contains the Arduino SWBB-MQTT-Gateway example, that showcases bidirectional, transparent data transmission between an MQTT client and a [SoftwareBitBang](../SoftwareBitBang/README.md) bus. To build it, just use the Arduino IDE. + +The "Translate" in the strategy name is because a translation table can be used to translate PJON packet contents to MQTT topics and back. This is to enable PJON packets to remain small `t=44.3` between devices with limited memory, while the MQTT packets are made more explicit `temperature` to support longer name syntax in external systems. + +#### MAC address usage + +The topic names like `pjon/device45/output/temperature` in the two MIRROR modes can be replaced with topic names containing the MAC address of the Ethernet/WiFi card of the device, like `pjon/DACA7EEFFE5D/output/temperature`. This is selected by setting the `MQTTT_USE_MAC` preprocessor definition. + +This gives the option to flash the same sketch without modifications to a lot of devices that will all appear in dedicated topics, to enable plug and play. + +Note that this functionality does not cover Windows/Linux/OsX in this release. + +### Configuration + +Before including the library it is possible to configure `MQTTTranslate` using predefined constants: + +| Constant | Purpose | Supported value | +| ------------------ |--------------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| `MQTTT_MODE` | Select mode | `MQTTT_MODE_BUS_RAW`, `MQTTT_MODE_BUS_JSON`, `MQTTT_MODE_MIRROR_TRANSLATE`, `MQTTT_MODE_MIRROR_DIRECT` | + +Use `PJONMQTTTranslate` to instantiate an object ready to communicate using `MQTTTranslate` strategy: + +```cpp + #include // Include the PJON library + // Use MQTTTranslate strategy with PJON device id 44 + PJONMQTTTranslate bus(44); + uint8_t broker_ip[] = { 127, 0, 0, 1 }; + + void setup() { + // Sets the broker's ip, port and topic used + bus.strategy.set_address(broker_ip, 1883, "receiver"); + } +``` diff --git a/hal/transport/PJON/driver/strategies/OverSampling/OverSampling.h b/hal/transport/PJON/driver/strategies/OverSampling/OverSampling.h new file mode 100644 index 000000000..7e9a7fd83 --- /dev/null +++ b/hal/transport/PJON/driver/strategies/OverSampling/OverSampling.h @@ -0,0 +1,341 @@ + +/* OverSampling 1 or 2 wires software-defined data link + used as a Strategy by PJON (included in version v3.0) + Compliant with PJDLR (Padded Jittering Data Link Radio) specification v3.0 + + It uses the over-sampling method to receive data, that is generally + implemented on physical layers characterized by low bandwidth and high + noise such as ASK/FSK radio transceivers. + ____________________________________________________________________________ + + Copyright 2015-2018 Giovanni Blu Mitolo gioscarab@gmail.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + +#pragma once + +/* MODE 1: + Medium: STX882/SRX882 433MHz ASK/FSK modules or 315/433 MHz modules (green) + RX http://nicerf.com/manage/upfile/indexbanner/635331970881921250.pdf + TX http://nicerf.com/manage/upfile/indexbanner/635169453223382155.pdf + Timing for other hardware can be easily implemented in Timing.h + + Performance: + Transfer speed: 1620Bb or 202B/s + Absolute communication speed: 180B/s (data length 20 of characters) + Data throughput: 150B/s (data length 20 of characters) + Range: 250m with no line of sight, 5 km with direct line of sight */ + +#ifndef OS_MODE +#define OS_MODE 1 +#endif + +// Recommended receive time for this strategy, in microseconds +#ifndef OS_RECEIVE_TIME +#define OS_RECEIVE_TIME 1000 +#endif + +// Used to signal communication failure +#define OS_FAIL 65535 + +// Used for pin handling +#define OS_NOT_ASSIGNED 255 + +#include "Timing.h" + +class OverSampling +{ +public: + + /* Returns the suggested delay related to the attempts passed as parameter: */ + + uint32_t back_off(uint8_t attempts) + { + uint32_t result = attempts; + for(uint8_t d = 0; d < OS_BACK_OFF_DEGREE; d++) { + result *= (uint32_t)(attempts); + } + return result; + }; + + + /* Begin method, to be called on initialization: + (returns always true) */ + + bool begin(uint8_t did = 0) + { + PJON_DELAY(PJON_RANDOM(OS_INITIAL_DELAY) + did); + return true; + }; + + + /* Check if the channel is free for transmission: + If receiving 10 bits no 1s are detected + there is no active transmission */ + + bool can_start() + { + float value = 0.5; + uint32_t time = PJON_MICROS(); + PJON_IO_MODE(_input_pin, INPUT); + while((uint32_t)(PJON_MICROS() - time) < OS_TIMEOUT) { + value = (value * 0.999) + (PJON_IO_READ(_input_pin) * 0.001); + } + if(value > 0.5) { + return false; + } + if(PJON_IO_READ(_input_pin)) { + return false; + } + PJON_DELAY_MICROSECONDS(PJON_RANDOM(OS_COLLISION_DELAY)); + if(PJON_IO_READ(_input_pin)) { + return false; + } + return true; + }; + + /* Returns the maximum number of attempts for each transmission: */ + + static uint8_t get_max_attempts() + { + return OS_MAX_ATTEMPTS; + }; + + + /* Returns the recommended receive time for this strategy: */ + + static uint16_t get_receive_time() + { + return OS_RECEIVE_TIME; + }; + + + /* Handle a collision: */ + + void handle_collision() + { + PJON_DELAY_MICROSECONDS(PJON_RANDOM(OS_COLLISION_DELAY)); + }; + + + /* Read a byte from the pin */ + + uint8_t read_byte() + { + uint8_t byte_value = 0B00000000; + for(uint8_t i = 0; i < 8; i++) { + uint32_t time = PJON_MICROS(); + float value = 0.5; + while((uint32_t)(PJON_MICROS() - time) < OS_BIT_WIDTH) { + value = ((value * 0.999) + (PJON_IO_READ(_input_pin) * 0.001)); + } + byte_value += (value > 0.5) << i; + } + return byte_value; + }; + + + /* Try to receive a byte: */ + + uint16_t receive_byte() + { + if(sync()) { + return read_byte(); + } + return OS_FAIL; + }; + + /* Receive byte response: + Transmitter emits a OS_BIT_SPACER / 2 long bit and tries + to get a response cyclically for OS_TIMEOUT microseconds. + Receiver then can blindly send PJON_ACK */ + + uint16_t receive_response() + { + if(_output_pin != _input_pin && _output_pin != OS_NOT_ASSIGNED) { + PJON_IO_WRITE(_output_pin, LOW); + } + uint16_t response = OS_FAIL; + uint32_t time = PJON_MICROS(); + while( + response == OS_FAIL && + (uint32_t)(PJON_MICROS() - OS_TIMEOUT) <= time + ) { + PJON_IO_WRITE(_input_pin, LOW); + if(sync()) { + response = receive_byte(); + } + if(response != PJON_ACK) { + PJON_IO_MODE(_output_pin, OUTPUT); + PJON_IO_WRITE(_output_pin, HIGH); + PJON_DELAY_MICROSECONDS(OS_BIT_SPACER / 2); + PJON_IO_PULL_DOWN(_output_pin); + } + } + return response; + }; + + + /* Receive a frame: */ + + uint16_t receive_frame(uint8_t *data, uint16_t max_length) + { + uint16_t result; + if(max_length == PJON_PACKET_MAX_LENGTH) { + uint32_t time = PJON_MICROS(); + // Look for frame initializer + if(!sync() || !sync() || !sync()) { + return OS_FAIL; + } + if( // Check its timing consistency + (uint32_t)(PJON_MICROS() - time) < + ((OS_BIT_WIDTH * 3) + (OS_BIT_SPACER * 3)) + ) { + return OS_FAIL; + } + } // Receive incoming byte + result = receive_byte(); + if(result == OS_FAIL) { + return OS_FAIL; + } + *data = result; + return 1; + }; + + + /* Every byte is prepended with 2 synchronization padding bits. The first + is a shorter than standard logic 1 followed by a standard logic 0. + _____ ___________________________ + |Sync | Byte | + |_ |___ ___ _____ | + | | | | | | | | | + |1| 0 | 1 | 0 0 | 1 | 0 | 1 1 | 0 | + |_|___|___|_____|___|___|_____|___| + + The reception tecnique is based on finding a logic 1 as long as the + first padding bit, synchronizing to its falling edge and checking if + it is followed by a logic 0. If this pattern is recognised, reception + starts, if not, interference, synchronization loss or simply absence + of communication is detected at byte level. */ + + void send_byte(uint8_t b) + { + pulse(1); + for(uint8_t mask = 0x01; mask; mask <<= 1) { + PJON_IO_WRITE(_output_pin, b & mask); + PJON_DELAY_MICROSECONDS(OS_BIT_WIDTH); + } + }; + + /* Send byte response: + Transmitter sends a OS_BIT_SPACER / 2 microseconds long HIGH bit and + tries to receive a response cyclically for OS_TIMEOUT + microseconds. Receiver then can blindly send PJON_ACK */ + + void send_response(uint8_t response) + { + PJON_IO_PULL_DOWN(_input_pin); + PJON_IO_MODE(_output_pin, OUTPUT); + pulse(1); + send_byte(response); + PJON_IO_PULL_DOWN(_output_pin); + }; + + /* Send a frame: */ + + void send_frame(uint8_t *data, uint16_t length) + { + PJON_IO_MODE(_output_pin, OUTPUT); + pulse(3); // Send frame inititializer + for(uint16_t b = 0; b < length; b++) { + send_byte(data[b]); // Send data + } + PJON_IO_PULL_DOWN(_output_pin); + }; + + + bool sync() + { + PJON_IO_PULL_DOWN(_input_pin); + if(_output_pin != OS_NOT_ASSIGNED && _output_pin != _input_pin) { + PJON_IO_PULL_DOWN(_output_pin); + } + float value = 0.5; + uint32_t time = PJON_MICROS(); + /* Average pin value until the pin stops to be HIGH or passed more + time than BIT_SPACER duration */ + while( + ((uint32_t)(PJON_MICROS() - time) < OS_BIT_SPACER) && + PJON_IO_READ(_input_pin) + ) { + value = (value * 0.999) + (PJON_IO_READ(_input_pin) * 0.001); + } + /* If the pin value is in average more than 0.5, is a 1, and if lasted + more than OS_ACCEPTANCE (a minimum HIGH duration) and what is coming + after is a LOW bit probably a byte is coming so try to receive it. */ + if(PJON_MICROS() - time < OS_ACCEPTANCE) { + return false; + } + if(value > 0.5) { + value = 0.5; + time = PJON_MICROS(); + while((uint32_t)(PJON_MICROS() - time) < OS_BIT_WIDTH) { + value = (value * 0.999) + (PJON_IO_READ(_input_pin) * 0.001); + } + if(value < 0.5) { + return true; + } + return false; + } + return false; + }; + + /* Emit synchronization pulse: */ + + void pulse(uint8_t n) + { + while(n--) { + PJON_IO_WRITE(_output_pin, HIGH); + PJON_DELAY_MICROSECONDS(OS_BIT_SPACER); + PJON_IO_WRITE(_output_pin, LOW); + PJON_DELAY_MICROSECONDS(OS_BIT_WIDTH); + } + }; + + /* Set the communicaton pin: */ + + void set_pin(uint8_t pin) + { + PJON_IO_PULL_DOWN(pin); + _input_pin = pin; + _output_pin = pin; + }; + + + /* Set a pair of communication pins: */ + + void set_pins( + uint8_t input_pin = OS_NOT_ASSIGNED, + uint8_t output_pin = OS_NOT_ASSIGNED + ) + { + PJON_IO_PULL_DOWN(input_pin); + PJON_IO_PULL_DOWN(output_pin); + _input_pin = input_pin; + _output_pin = output_pin; + }; + +private: + uint8_t _input_pin; + uint8_t _output_pin; +}; diff --git a/hal/transport/PJON/driver/strategies/OverSampling/README.md b/hal/transport/PJON/driver/strategies/OverSampling/README.md new file mode 100644 index 000000000..d18e508c0 --- /dev/null +++ b/hal/transport/PJON/driver/strategies/OverSampling/README.md @@ -0,0 +1,76 @@ +## OverSampling +| Medium | Pins used | Inclusion | +|--------|-----------|--------------------| +| ASK/FSK radio modules | 1 or 2 | `#include `| + +`OverSampling` is a software implementation of [PJDLR (Padded Jittering Data Link over Radio)](/src/strategies/OverSampling/specification/PJDLR-specification-v3.0.md). It supports simplex and half-duplex asynchronous serial communication and implements a carrier-sense, non-persistent random multiple access method (non-persistent CSMA). This implementation can run on limited microcontrollers with low clock accuracy, supports communication for many devices connected to the same medium and stable operation in spite of interference. Its procedure is a more efficient alternative to the LoRa Open Standard (that specifies a variation of Slotted ALOHA) and it is designed to obtain long range and high reliability using FSK, ASK or OOK modulation radio transceivers. Take a look at the [video introduction](https://www.youtube.com/watch?v=G1ckfsMzPns) for a brief showcase of its features. + +### Compatibility +| MCU | Clock | Supported pins | +| ---------------- |------ | ---------------- | +| ATmega88/168/328 (Duemilanove, Uno, Nano, Pro) | 16MHz | 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, A0, A1 | +| ATmega16u4/32u4 (Leonardo, Micro) | 16MHz | 2, 4, 8, 12 | +| ATmega2560 (Mega, Mega nano) | 16MHz | 3, 4, 7, 8, 9, 10, 12 | +| ESP8266 | 80/160MHz | D0 or GPIO 16, D1 or GPIO 5 | + +### Performance +- Transfer speed: 202 B/s or 1620 Baud +- Data throughput: 150 B/s +- Range: 250 meters with no line of sight, 5 km with line of sight and ideal atmospheric conditions + +### Configuration +Before including the library it is possible to configure `OverSampling` using predefined constants: + +| Constant | Purpose | Supported value | +| ------------------------- |------------------------------------ | ------------------------------------------- | +| `OS_TIMEOUT` | Maximum latency | Duration in microseconds (10000 by default) | +| `OS_BACK_OFF_DEGREE` | Maximum back-off exponential degree | Numeric value (5 by default) | +| `OS_MAX_ATTEMPTS` | Maximum transmission attempts | Numeric value (10 by default) | + +Use `PJONOverSampling` to instantiate a new PJON object ready to communicate using the `OverSampling` strategy. All the other necessary information is present in the general [Documentation](/documentation). +```cpp + +#include + +PJONOverSampling bus; + +void setup() { + // Set the pin 12 as the communication pin + bus.strategy.set_pin(12); + + // Set pin 11 as input pin and pin 12 as output pin + bus.strategy.set_pins(11, 12); +} +``` +After the PJON object is defined with its strategy it is possible to set the communication pins using the setters described in the example below. + +### Use OverSampling with cheap 433MHz transceivers +To build an open-source PJON packet radio able to communicate up to 5 km away you need only a couple (for `PJON_SIMPLEX` mode) or two couples (for `PJON_HALF_DUPLEX` mode) of cheap 315/433MHz ASK/FSK/OOK transmitter and receiver modules (with a cost around 2/3 dollars each). Please be sure of the regulations your government imposes on radio transmission over these frequencies before use. + +![PJON Oversampling packet radio](http://www.gioblu.com/PJON/PJON-OverSampling-packet-radio-STX882-SRX882.jpg) + +The maximum detected range was experimented with a small packet radio transmitting its position every minute. The maximum range obtained was slightly more than 5 kilometers in line of sight in open area. Testing it instead in an urban environment the range is down to 250 meters. Two couples of STX882 and SRX882 were used as transceivers. If you choose these modules, remember to set `HIGH` the pin `CS` in the receiver module before starting reception. + +### Antenna design +Experiments in `PJON_HALF_DUPLEX` mode have shown that it seems better to keep isolated the two antennas, using two different, not connected elements to transmit and receive. The first suggested antenna design is a wide beam dipole antenna made by two 173mm (quarter wavelength) or 345mm (half wavelength) long conductors, one connected to ground and the other connected to the input or output pin: +```cpp +173mm quarter wavelength / 345mm half wavelength + +-------------------|-------------------- + __|__ + |TX/RX| + |_____| +``` +A more directional, compact and long range antenna design is the wip antenna. Can be easily crafted with two 173mm (quarter wavelength) / 345mm (half wavelength) long insulated wire sections wrapped with each other every 5mm, one is connected to ground and the other to the input or output pin. This design helps because of its strong ground plane, often necessary to have decent results with this sort of hardware. +```cpp + 5mm + || 173mm quarter wavelength / 345mm half wavelength +GND --\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\ +RX/TX --/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/ +``` + +### Known issues +- In older versions, `OverSampling` was affected by ineffective and short range if used in `PJON_HALF_DUPLEX` mode. This issue has been fixed by suggesting the use of pins part of 2 different port groups. + +### Safety warning +In all cases, when installing or maintaining a PJON network, extreme care must be taken to avoid any danger. Before any practical test or hardware purchase for a wireless [OverSampling](/src/strategies/OverSampling) radio setup, compliance with government requirements and regulations must be ensured. diff --git a/hal/transport/PJON/driver/strategies/OverSampling/Timing.h b/hal/transport/PJON/driver/strategies/OverSampling/Timing.h new file mode 100644 index 000000000..1ec09bd88 --- /dev/null +++ b/hal/transport/PJON/driver/strategies/OverSampling/Timing.h @@ -0,0 +1,91 @@ + +/* PJON OverSampling strategy Transmission Timing table + Copyright 2018, Giovanni Blu Mitolo All rights reserved. + + All benchmarks are executed with NetworkAnalysis and SpeedTest examples. + + The following constants setup is quite conservative and determined only + with a huge amount of time testing tweaking values and analysing results. + Theese can be changed to obtain faster speed. Probably you will need + experience, time and an oscilloscope. */ + +#pragma once + +/* ATmega88/168/328 - Arduino Duemilanove, Uno, Nano, Mini, Pro, Pro mini */ +#if defined(__AVR_ATmega88__) || defined(__AVR_ATmega168__) || \ + defined(__AVR_ATmega328__) || defined(__AVR_ATmega328P__) +#if OS_MODE == 1 +#if F_CPU == 16000000L +#define OS_BIT_WIDTH 512 +#define OS_BIT_SPACER 328 +#define OS_ACCEPTANCE 78 +#endif +#endif +#endif + +/* ATmega1280/2560 - Arduino Mega/Mega-nano ------------------------------- */ +#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) +#if OS_MODE == 1 +#define OS_BIT_WIDTH 508 +#define OS_BIT_SPACER 332 +#define OS_ACCEPTANCE 82 +#endif +#endif + +/* ATmega16/32U4 - Arduino Leonardo/Micro --------------------------------- */ +#if defined(__AVR_ATmega16U4__) || defined(__AVR_ATmega32U4__) +#if OS_MODE == 1 +#define OS_BIT_WIDTH 508 +#define OS_BIT_SPACER 332 +#define OS_ACCEPTANCE 82 +#endif +#endif + +/* NodeMCU, generic ESP8266 ----------------------------------------------- */ +#if defined(ESP8266) +#if OS_MODE == 1 +#if F_CPU == 80000000L +#define OS_BIT_WIDTH 520 +#define OS_BIT_SPACER 332 +#define OS_ACCEPTANCE 82 +#endif +#endif +#endif + +#ifndef OS_BIT_WIDTH +#define OS_BIT_WIDTH 512 // 520 microseconds detected by oscilloscope +#endif + +#ifndef OS_BIT_SPACER +#define OS_BIT_SPACER 328 // 340 microseconds detected by oscilloscope +#endif + +#ifndef OS_ACCEPTANCE +#define OS_ACCEPTANCE 78 // 340 microseconds detected by oscilloscope +#endif + +#define OS_TIMEOUT 10000 // 10 milliseconds maximum sync ack timeout + +/* Maximum initial delay in milliseconds: */ + +#ifndef OS_INITIAL_DELAY +#define OS_INITIAL_DELAY 1000 +#endif + +/* Maximum delay in case of collision in microseconds: */ + +#ifndef OS_COLLISION_DELAY +#define OS_COLLISION_DELAY 64 +#endif + +/* Maximum transmission attempts */ + +#ifndef OS_MAX_ATTEMPTS +#define OS_MAX_ATTEMPTS 10 +#endif + +/* Back-off exponential degree */ + +#ifndef OS_BACK_OFF_DEGREE +#define OS_BACK_OFF_DEGREE 5 +#endif diff --git a/hal/transport/PJON/driver/strategies/OverSampling/specification/PJDLR-specification-v3.0.md b/hal/transport/PJON/driver/strategies/OverSampling/specification/PJDLR-specification-v3.0.md new file mode 100644 index 000000000..aac3f079a --- /dev/null +++ b/hal/transport/PJON/driver/strategies/OverSampling/specification/PJDLR-specification-v3.0.md @@ -0,0 +1,97 @@ + +### Specifications index + +#### Network layer +- [PJON (Padded Jittering Operative Network) v4.0](/specification/PJON-protocol-specification-v4.0.md) +- [Network services list](/specification/PJON-network-services-list.md) +#### Data link layer +- [PJDL (Padded Jittering Data Link) v5.0](/src/strategies/SoftwareBitBang/specification/PJDL-specification-v5.0.md) +- **[PJDLR (Padded Jittering Data Link over Radio) v3.0](/src/strategies/OverSampling/specification/PJDLR-specification-v3.0.md)** +- [PJDLS (Padded Jittering Data Link byte Stuffed) v2.0](/src/strategies/AnalogSampling/specification/PJDLS-specification-v2.0.md) +- [TSDL (Tardy Serial Data Link) v3.0](/src/strategies/ThroughSerial/specification/TSDL-specification-v3.0.md) +- [SFSP (Secure Frame Separation Protocol) v1.0](/specification/SFSP-frame-separation-specification-v1.0.md) + +--- + +## PJDLR v3.0 +``` +Invented by Giovanni Blu Mitolo +Originally published: 10/04/2010 +Latest revision: 07/07/2019 +Related implementation: /src/strategies/OverSampling/ +Compliant versions: PJON v12.0 and following +Released into the public domain + +10/04/2010 v0.1 - First experimental release +18/01/2017 v1.0 - Packet preamble +31/03/2017 v1.1 - Response info +31/10/2018 v2.0 - Frame initializer, mode 1 +07/07/2019 v3.0 - Response made safe, response initializer +``` +PJDLR (Padded Jittering Data Link over Radio) is an asynchronous serial data link for low-data-rate applications that supports both master-slave and multi-master communication and it is optimized to obtain long range and high reliability using ASK, FSK or OOK radio transceivers. PJDLR can be easily implemented on limited microcontrollers with low clock accuracy and can operate directly using one or two input-output pins. + +### Communication modes +The proposed communication mode is the result of years of testing and optimization for ASK/FSK radio transceivers and have been selected to be easily supported also by low quality hardware. + +| MODE | Bit timing | Sync bit timing | Pad-data ratio | Speed | +| ---- | ---------- | --------------- | -------------- | ------------------- | +| 1 | 512 | 328 | 0.64 | 202B/s - 1620Bd | + +Binary timing durations are expressed in microseconds. + +### Medium access control +PJDLR specifies a variation of the carrier-sense, non-persistent random multiple access method (non-persistent CSMA). Devices can detect an ongoing transmission for this reason collisions can only occur in multi-master mode when 2 or more devices start to transmit at the same time. When a collision occurs it can be detected by the receiver because of synchronization loss or by the transmitter if an active collision avoidance procedure is implemented. + +### Byte transmission +Byte transmission is composed by 10 bits, the first two are called synchronization pad and are used to obtain sampling synchronization. The synchronization pad is composed by a high padding bit shorter than data bits and a low data bit. The following 8 data bits contain information in LSB-first (least significant bit first) order. + +The reception technique is based on 3 steps: +1. Find a high bit which duration is equal to or acceptably shorter than a high padding bit +2. Synchronize to its falling edge +3. Ensure it is followed by a low data bit + +If so reception starts, if not, interference, synchronization loss or simply absence of communication is detected. + +```cpp + _____ ___________________________ +| Pad | Byte | +|_ |___ ___ _____ | +| | | | | | | | | +|1| 0 | 1 | 0 0 | 1 | 0 | 1 1 | 0 | +|_|___|___|_____|___|___|_____|___| +``` + +### Frame transmission +Before a frame transmission, the communication medium's state is analysed, if high communication is detected and collision is avoided, if low for a duration that is longer than the response time-out plus a small random time, frame transmission starts with a frame initializer composed by 3 consequent synchronization pads followed by data bytes. The presence of the synchronization pad between each byte ensures that also a frame composed of a series of bytes with decimal value 0 can be transmitted safely without risk of collision. + +```cpp + INITIALIZER DATA + ___________ __________ _______________ ______________ +|Pad|Pad|Pad| Byte |Pad| Byte |Pad| Byte | +|_ |_ |_ | __ |_ | _ _|_ | _ | +| | | | | | | | | | | | | | | | | | | | | +|1|0|1|0|1|0|0000|11|00|1|0|00000|1|0|1|1|0|00000|1|00| +|_|_|_|_|_|_|____|__|__|_|_|_____|_|_|_|_|_|_____|_|__| +``` +When a frame is received a low performance microcontroller with an inaccurate clock can correctly synchronize with transmitter during the frame initializer and consequently each byte is received. The frame initializer is detected if 3 synchronization pads occurred and if their duration is coherent with its expected duration. Frame initialization is 100% reliable, false positives can only occur because of externally induced interference. + +### Synchronous response +A frame transmission can be optionally followed by a synchronous response sent by its recipient. Between frame transmission and a synchronous response there is a variable time which duration is influenced by latency. +```cpp +Transmission end Response + ______ ______ ______ _____ +| BYTE || BYTE || BYTE | CRC COMPUTATION / LATENCY | ACK | +|------||------||------|---------------------------|-----| +| || || | | 6 | +|______||______||______| |_____| +``` +In order to avoid other devices to detect the medium free for use and disrupt an ongoing exchange, the sender cyclically transmits a short high bit (1/2 high padding bit duration) and consequently attempts to receive a response. The receiver can transmit its response as soon as possible, and, in order to avoid false positives in case of collision, must transmit its response prepended with an additional synchronization pulse. If the response is not transmitted or not received the transmitter continues to keep busy the medium up to the maximum acceptable time between transmission and response. +```cpp +Transmission end Response + ______ ______ ______ _ _ _ _ _ _ ____ _____ +| BYTE || BYTE || BYTE | | | | | | | | | | | | |SYNC| ACK | +|------||------||------| | | | | | | | | | | | |----|-----| +| || || | | | | | | | | | | | | | | 6 | +|______||______||______|_| |_| |_| |_| |_| |_| |____|_____| +``` +The maximum time dedicated to potential response reception for a given application can be determined practically transmitting the longest supported frame with the farthest physical distance between the two devices. The highest interval between packet transmission and response measured plus a small margin is the correct timeout that should exclude response losses. Consider that the longer this timeout is, the more bandwidth is wasted if the transmission is not successful. diff --git a/hal/transport/PJON/driver/strategies/OverSampling/specification/obsolete/PJDLR-specification-v1.0.md b/hal/transport/PJON/driver/strategies/OverSampling/specification/obsolete/PJDLR-specification-v1.0.md new file mode 100644 index 000000000..2e38d81de --- /dev/null +++ b/hal/transport/PJON/driver/strategies/OverSampling/specification/obsolete/PJDLR-specification-v1.0.md @@ -0,0 +1,58 @@ + +```cpp +/* +Milan, Italy - 18/01/2017 +PJDLR (Padded jittering data link) specification is an invention and intellectual property +of Giovanni Blu Mitolo - Copyright 2010-2020 All rights reserved + +Related work: /src/strategies/SoftwareBitBang/ +Compliant implementation versions: PJON 7.0-7.1 + +New feature: Packet preamble by Fred Larsen +*/ +``` +### PJDLR (Padded jittering data link / R version) +PJDLR (Padded jittering data link) has been specified to enable a new way to transmit data in simplex and half-duplex mode using cheap and low performance microcontrollers without the need of hardware interrupts for its working procedure. It is designed to support many devices sharing the same medium, to avoid collisions and operate in spite of interference. Extended tests proved its effectiveness on different media like electricity, radio frequency and light. + +### Basic concepts +* Use a pattern of predefined initial padding bits to identify a potential byte transmission +* Use the falling edge from 1 to 0, present in padding bits, to achieve byte level synchronization +* Detect interference or absence of communication at byte level +* Enable channel analysis and collision avoidance +* Enable a collision free synchronous acknowledgement pattern + +#### Byte transmission +Every byte is prepended with 2 synchronization padding bits and transmission occurs LSB-first. The first is a shorter than standard logic 1 followed by a standard logic 0. The reception tecnique is based on finding a logic 1 as long as the first padding bit within a certain threshold, synchronizing to its falling edge and checking if it is followed by a logic 0. If this pattern is detected, reception starts, if not, interference, synchronization loss or simply absence of communication is detected at byte level. +```cpp + _____ ___________________________ +| Pad | Byte | +|_ |___ ___ _____ | +| | | | | | | | | +|1| 0 | 1 | 0 0 | 1 | 0 | 1 1 | 0 | +|_|___|___|_____|___|___|_____|___| +``` +Padding bits are adding a certain overhead to information but are reducing the need of precise time tuning because synchronization is renewed every byte. All the first padding bit duration is the synchronization window the receiver has for every incoming byte. If the duration of the first padding bit is longer than expected the received signal is considered interference. + +#### Packet transmission +Before a packet transmission, the medium is analyzed to detect ongoing communication and avoid collision. Thanks to the presence of padding bits, also a packet composed by 100 bytes, all with a decimal value of 0, can be transmitted safely without experiencing third-party collision. After assessed that the medium is free to use, a packet preamble, composed of a long 1 and a long 0, is transmitted to let a potential receiver to adjust its gain to the transmitted signal magnitude. The duration of the preamble bits have to be adjusted to match hardware sensitivity, gain refresh time and signal to noise ratio. + +```cpp + _________ ______________ _______________ ______________ ______________ ________________ +|Preamble |Pad| Byte |Pad| Byte |Pad| Byte |Pad| Byte |Pad| Byte | +|_____ |_ | __ |_ | _ _|_ | _ |_ | _ |_ | _ _ | +| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | +| 1 | 0 |1|0|0000|11|00|1|0|00000|1|0|1|1|0|00000|1|00|1|0|0|1|000000|1|0|0|1|00|1|000| +|_____|___|_|_|____|__|__|_|_|_____|_|_|_|_|_|_____|_|__|_|_|_|_|______|_|_|_|_|__|_|___| +``` +In a scenario where a stream of bytes is coming, low performance or clock inaccurate microcontroller can be correctly synchronized back with transmitter every byte (thanks to padding bits) and easily detect an interference or the end of transmission. + +#### Synchronous acknowledgement +After packet reception, CRC is calculated and a single character is transmitted: `PJON_ACK` (value 6) if the packet's content is correct or `PJON_NAK` (value 21) if an error is detected. +```cpp + Transmission Response + ________ ________________________________________ ________ _____ +|PREAMBLE| ID | HEADER | LENGTH | CONTENT | CRC | CRC COMPUTATION |PREAMBLE| ACK | +|____ | | | | | |-------> <-------|____ | | +| | | 12 | 00000100 | 5 | 64 | 72 | LATENCY | | | 6 | +|____|___|____|__________|________|_________|_____| |____|___|_____| +``` diff --git a/hal/transport/PJON/driver/strategies/OverSampling/specification/obsolete/PJDLR-specification-v1.1.md b/hal/transport/PJON/driver/strategies/OverSampling/specification/obsolete/PJDLR-specification-v1.1.md new file mode 100644 index 000000000..77e2b7f69 --- /dev/null +++ b/hal/transport/PJON/driver/strategies/OverSampling/specification/obsolete/PJDLR-specification-v1.1.md @@ -0,0 +1,68 @@ + +```cpp +/* +Milan, Italy - 31/03/2017 +PJDLR (Padded jittering data link) specification is an invention +and intellectual property of Giovanni Blu Mitolo +Copyright 2010-2020 All rights reserved + +Related work: /src/strategies/SoftwareBitBang/ +Compliant implementation versions: PJON 8.0 and following + +Packet preamble feature proposed by Fred Larsen + +Changelog: Response generalization / refactoring +*/ +``` +### PJDLR (Padded jittering data link / R version) +PJDLR (Padded jittering data link) has been specified to enable a new software emulated simplex or half-duplex data link layer supporting one or many to many communication on a single channel or medium. It can be run on cheap and low performance microcontrollers, it supports communication for many devices connected to the same medium and stable operation in spite of interference. Its procedure has been specified to obtain long range and high reliability using FSK/ASK/OOK radio transceivers as physical layer. + +#### Basic concepts +* Use a pattern of predefined initial padding bits to identify a byte +* Use the falling edge from 1 to 0, present in padding bits, to achieve byte level synchronization +* Detect interference or absence of communication at byte level +* Support string transmission using a preamble symbol +* Support collision avoidance +* Support 1 byte synchronous response to string transmission + +#### Byte transmission +Every byte is prepended with 2 synchronization padding bits and transmission occurs LSB-first. The first is a shorter than standard logic 1 followed by a standard logic 0. The reception method is based on finding a logic 1 as long as the first padding bit within a certain threshold, synchronizing to its falling edge and checking if it is followed by a logic 0. If this pattern is detected, reception starts, if not, interference, synchronization loss or simply absence of communication is detected at byte level. +```cpp + _____ ___________________________ +| Pad | Byte | +|_ |___ ___ _____ | +| | | | | | | | | +|1| 0 | 1 | 0 0 | 1 | 0 | 1 1 | 0 | +|_|___|___|_____|___|___|_____|___| + | +OS_ACCEPTANCE +(or minimum acceptable HIGH padding bit duration) +``` +Padding bits add a certain overhead but are reducing the need of precise timing because synchronization is renewed every byte. All the first high padding bit duration minus `OS_ACCEPTANCE` is the synchronization timeframe the receiver has to synchronize and correctly receive a byte. If the length of the first padding bit is less than `OS_ACCEPTANCE` the received signal is considered interference. + +#### String transmission +Before a packet transmission, the medium is analyzed to detect ongoing communication and avoid collision. Thanks to the presence of padding bits, also a packet composed by 100 bytes, all with a decimal value of 0, can be transmitted safely without risk of third-party collision. After assessed that the medium is free to use, a packet preamble, composed of a long 1 and a long 0, is transmitted to let a potential receiver to adjust its gain to the transmitted signal magnitude. The duration of the preamble bits have to be adjusted to match hardware sensitivity, gain refresh time and signal to noise ratio. + +```cpp + _________ ______________ _______________ ______________ ______________ +|Preamble |Pad| Byte |Pad| Byte |Pad| Byte |Pad| Byte | +|_____ |_ | __ |_ | _ _|_ | _ |_ | _ | +| | | | | | | | | | | | | | | | | | | | | | | | +| 1 | 0 |1|0|0000|11|00|1|0|00000|1|0|1|1|0|00000|1|00|1|0|0|1|000000| +|_____|___|_|_|____|__|__|_|_|_____|_|_|_|_|_|_____|_|__|_|_|_|_|______| +``` +In a scenario where a stream of bytes is coming, low performance or clock inaccurate microcontrollers can be correctly synchronized back with transmitter every byte (thanks to padding bits) and easily detect interference or the end of transmission. + + +#### Synchronous response +A string transmission can be optionally followed by a synchronous response by its recipient. +```cpp +Transmission Response + ________ ______ ______ ______ ______ ________ _____ +|PREAMBLE| BYTE || BYTE || BYTE || BYTE | CRC COMPUTATION |PREAMBLE| ACK | +|____ |------||------||------||------|-----------------|____ | | +| | | || || || | LATENCY | | | 6 | +|____|___|______||______||______||______| |____|___|_____| +``` + +The maximum time dedicated to potential acknowledgement reception has to be same for all connected devices, and it is defined by the use case constraints like maximum packet length and latency or physical distance between devices. diff --git a/hal/transport/PJON/driver/strategies/OverSampling/specification/obsolete/PJDLR-specification-v2.0.md b/hal/transport/PJON/driver/strategies/OverSampling/specification/obsolete/PJDLR-specification-v2.0.md new file mode 100644 index 000000000..a22f316e2 --- /dev/null +++ b/hal/transport/PJON/driver/strategies/OverSampling/specification/obsolete/PJDLR-specification-v2.0.md @@ -0,0 +1,66 @@ + +## PJDLR v2.0 +``` +Invented by Giovanni Blu Mitolo +Originally published: 10/04/2010, latest revision: 31/10/2018 +Related implementation: /src/strategies/OverSampling/ +Compliant versions: PJON v9.0 and following +Released into the public domain +``` +PJDLR (Padded Jittering Data Link over Radio) is an asynchronous serial data link for low-data-rate applications that supports one or many to many communication optimized to obtain long range and high reliability using ASK, FSK or OOK radio transceivers. PJDLR can be easily implemented on limited microcontrollers with low clock accuracy and can operate directly using one or two input-output pins. + +### Communication modes +The proposed communication mode is the result of years of testing and optimization for ASK/FSK radio transceivers and have been selected to be easily supported also by low quality hardware. + +| MODE | Bit timing | Sync bit timing | Pad-data ratio | Speed | +| ---- | ---------- | --------------- | -------------- | ------------------- | +| 1 | 512 | 328 | 0.64 | 202B/s - 1620Bd | + +Binary timing durations are expressed in microseconds. + +### Medium access control +PJDLR specifies a variation of the carrier-sense, non-persistent random multiple access method (non-persistent CSMA). Devices can detect an ongoing transmission for this reason collisions can only occur in multi-master mode when 2 or more devices start to transmit at the same time. When a collision occurs it can be detected by the receiver because of synchronization loss. + +### Byte transmission +Byte transmission is composed by 10 bits, the first two are called synchronization pad and are used to obtain sampling synchronization. The synchronization pad is composed by a high padding bit shorter than data bits and a low data bit. The following 8 data bits contain information in LSB-first (least significant bit first) order. + +The reception technique is based on 3 steps: +1. Find a high bit which duration is equal to or acceptably shorter than a high padding bit +2. Synchronize to its falling edge +3. Ensure it is followed by a low data bit +4. If so reception starts, if not, interference, synchronization loss or simply absence of communication is detected +```cpp + _____ ___________________________ +| Pad | Byte | +|_ |___ ___ _____ | +| | | | | | | | | +|1| 0 | 1 | 0 0 | 1 | 0 | 1 1 | 0 | +|_|___|___|_____|___|___|_____|___| +``` + +### Frame transmission +Before a frame transmission, the communication medium's state is analysed, if high communication is detected and collision is avoided, if low for a duration that is longer than the response time-out plus a small random time, frame transmission starts with a frame initializer composed by 3 consequent synchronization pads followed by data bytes. The presence of the synchronization pad between each byte ensures that also a frame composed of a series of bytes with decimal value 0 can be transmitted safely without risk of collision. + +```cpp + INITIALIZER DATA + ___________ __________ _______________ ______________ +|Pad|Pad|Pad| Byte |Pad| Byte |Pad| Byte | +|_ |_ |_ | __ |_ | _ _|_ | _ | +| | | | | | | | | | | | | | | | | | | | | +|1|0|1|0|1|0|0000|11|00|1|0|00000|1|0|1|1|0|00000|1|00| +|_|_|_|_|_|_|____|__|__|_|_|_____|_|_|_|_|_|_____|_|__| +``` +When a frame is received a low performance microcontroller with an inaccurate clock can correctly synchronize with transmitter during the frame initializer and consequently each byte is received. The frame initializer is detected if 3 synchronization pads occurred and if their duration is coherent with its expected duration. Frame initialization is 100% reliable, false positives can only occur because of externally induced interference. + +### Synchronous response +A frame transmission in both master-slave and multi-master modes can be optionally followed by a synchronous response of its recipient, all devices must use the same response time-out to avoid collisions. The acknowledgment reception phase must be shorter than the response time-out to be successful. +```cpp +Transmission Response + ______ ______ ______ _____ +| INIT || BYTE || BYTE | CRC COMPUTATION | ACK | +|------||------||------|-----------------| | +| || || | LATENCY | 6 | +|______||______||______| |_____| +``` + +The required response time-out for a given application can be determined practically transmitting the longest supported frame with the farthest physical distance between the two devices. The highest interval between packet transmission and acknowledgement measured plus a small margin is the correct time-out that should exclude acknowledgement losses. diff --git a/hal/transport/PJON/driver/strategies/OverSampling/specification/obsolete/padded-jittering-protocol-specification-v0.1.md b/hal/transport/PJON/driver/strategies/OverSampling/specification/obsolete/padded-jittering-protocol-specification-v0.1.md new file mode 100644 index 000000000..124563e74 --- /dev/null +++ b/hal/transport/PJON/driver/strategies/OverSampling/specification/obsolete/padded-jittering-protocol-specification-v0.1.md @@ -0,0 +1,42 @@ + +```cpp +/* +Milan, Italy - 10/04/2010 +The Padded jittering data link layer specification is an invention and intellectual property +of Giovanni Blu Mitolo - Copyright 2010-2016 All rights reserved + +Related work: /src/strategies/SoftwareBitBang/ +Compliant implementation versions: PJON 1.0-5.0 +*/ +``` +### Padded jittering data link layer +The first experimental specification of the Padded jittering data link layer has been drafted to propose a new way to transmit data with cheap and low performance microcontrollers without the necessity of hardware interrupts for its working procedure. Extended tests proved its effectiveness on different media like electricity, radio frequency and light. + +### Basic concepts +* Use a pattern of predefined initial padding bits to identify a byte transmission +* Use the falling edge from 1 to 0, present in padding bits, to achieve byte level synchronization +* Detect interference or absence of communication at byte level + +#### Byte transmission +Every byte is prepended with 2 synchronization padding bits and transmission occurs LSB-first. The first is a longer than standard logic 1 followed by a standard logic 0. The reception tecnique is based on finding a logic 1 as long as the first padding bit within a certain threshold, synchronizing to its falling edge and checking if it is followed by a logic 0. If this pattern is detected, reception starts, if not, interference, synchronization loss or simply absence of communication is detected at byte level. +```cpp + __________ ___________________________ +| SyncPad | Byte | +|______ |___ ___ _____ | +| | | | | | | | | | +| | 1 | 0 | 1 | 0 0 | 1 | 0 | 1 1 | 0 | +|_ |___|___|___|_____|___|___|_____|___| + | + ACCEPTANCE +``` +Padding bits are adding a certain overhead to information but are reducing the need of precise time tuning because synchronization is renewed every byte. All the first padding bit duration minus `ACCEPTANCE` is the synchronization window the receiver has for every incoming byte. If the length of the first padding bit is less than `ACCEPTANCE` the received signal is considered interference. + +```cpp + ________________ _________________ ________________ ________________ __________________ +|Sync | Byte |Sync | Byte |Sync | Byte |Sync | Byte |Sync | Byte | +|___ | __ |___ | _ _|___ | _ |___ | _ |___ | _ _ | +| | | | | | | | | | | | | | | | | | | | | | | | | | | | | +| 1 |0|0000|11|00| 1 |0|00000|1|0|1| 1 |0|00000|1|00| 1 |0|0|1|000000| 1 |0|0|1|00|1|000| +|___|_|____|__|__|___|_|_____|_|_|_|___|_|_____|_|__|___|_|_|_|______|___|_|_|_|__|_|___| +``` +In a scenario where a stream of byte is coming, following this strategy a low performance or clock inaccurate microcontroller can be correctly synchronized back with the transmitter every byte and easily detect an interference or the end of transmission. diff --git a/hal/transport/PJON/driver/strategies/README.md b/hal/transport/PJON/driver/strategies/README.md new file mode 100644 index 000000000..2a00f035f --- /dev/null +++ b/hal/transport/PJON/driver/strategies/README.md @@ -0,0 +1,111 @@ + +### What is a Strategy +A strategy is an abstraction layer used to physically transmit data. Thanks to the strategies PJON can operate transparently on a wide range of media and protocols. Take a look at the [strategies video introduction](https://www.youtube.com/watch?v=yPu45xoAHGg) for a brief showcase of their features. + +The table below lists the strategies available: + +| Strategy | Physical layer | Protocol | Inclusion | +| ------------- | -------------- | -------- | --------- | +| [AnalogSampling](/src/strategies/AnalogSampling) | Light | [PJDLS](/src/strategies/AnalogSampling/specification/PJDLS-specification-v2.0.md) | `#include ` | +| [Any](/src/strategies/Any) | Virtual inheritance | Any | `#include ` | +| [DualUDP](/src/strategies/DualUDP) | Ethernet/WiFi | [UDP](https://tools.ietf.org/html/rfc768) | `#include ` | +| [ESPNOW](/src/strategies/ESPNOW) | WiFi | [ESPNOW](https://www.espressif.com/en/products/software/esp-now/overview) | `#include ` | +| [EthernetTCP](/src/strategies/EthernetTCP) | Ethernet/WiFi | [TCP](https://tools.ietf.org/html/rfc793) | `#include ` | +| [GlobalUDP](/src/strategies/GlobalUDP) | Ethernet/WiFi | [UDP](https://tools.ietf.org/html/rfc768) | `#include ` | +| [LocalFile](/src/strategies/LocalFile) | File system | None | `#include ` | +| [LocalUDP](/src/strategies/LocalUDP) | Ethernet/WiFi | [UDP](https://tools.ietf.org/html/rfc768) | `#include ` | +| [MQTTTranslate](/src/strategies/MQTTTranslate) | Ethernet/WiFi | [MQTT](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.pdf) | `#include ` | +| [OverSampling](/src/strategies/OverSampling) | Radio | [PJDLR](/src/strategies/OverSampling/specification/PJDLR-specification-v3.0.md) | `#include ` | +| [SoftwareBitBang](/src/strategies/SoftwareBitBang) | Wire | [PJDL](/src/strategies/SoftwareBitBang/specification/PJDL-specification-v5.0.md) | `#include ` | +| [ThroughLoRa](/src/strategies/ThroughLoRa) | Radio | [LoRa](https://lora-alliance.org/sites/default/files/2018-07/lorawan1.0.3.pdf) | `#include ` | +| [ThroughSerial](/src/strategies/ThroughSerial) | Wire | [TSDL](/src/strategies/ThroughSerial/specification/TSDL-specification-v3.0.md) | `#include ` | + +### How the strategy is implemented +A `Strategy` is a class containing a set of methods used to physically send and receive data along with the required getters to handle retransmission and collision: + +```cpp +bool begin(uint8_t did = 0) +``` +Receives an optional parameter of type `uint8_t` (when PJON calls `begin` it passes its own device id); returns `true` if the strategy is correctly initialized. There is no doubt that the strategy should not know about the PJON's device id, although that is practically useful in many cases. + +```cpp +uint32_t back_off(uint8_t attempts) +``` +receives a paramenter of type `uint8_t` and returns the suggested delay for a given number of attempts. + +```cpp +bool can_start() +``` +Returns `true` if the medium is free for use and `false` if the medium is busy. + +```cpp +void handle_collision() +``` +Handles a collision. + +```cpp +uint8_t get_max_attempts() +``` +Returns the maximum number of attempts in case of failed transmission. +```cpp +uint16_t get_receive_time() +``` +Returns the minimum polling time required to successfully receive a frame. +```cpp +void send_frame(uint8_t *data, uint16_t length) +``` +Receives a pointer to the data and its length and sends it through the medium. The sending procedure must be blocking. + +```cpp +uint16_t receive_frame(uint8_t *data, uint16_t max_length) { ... }; +``` +Receives a pointer where to store received information and an unsigned integer signalling the maximum data length. It should return the number of bytes received or `PJON_FAIL`. + +```cpp +void send_response(uint8_t response) +``` +Send a response to the packet's transmitter. + +```cpp +uint16_t receive_response() +``` +Receives a response from the packet's receiver. + +```cpp +// Simple Serial data link layer implementation example +void send_response(uint8_t response) { + Serial.print(response); +}; +``` +Above it is demonstrated how simply other communication protocols can be used to define a new custom strategy. + +### How to implement a custom strategy +To define a new custom strategy you need to create a new folder named for example `YourStrategyName` in the `src/strategies` +directory and create the necessary file `YourStrategyName.h`: + +```cpp +class YourStrategyName { + public: + uint32_t back_off(uint8_t attempts) { }; + bool begin(uint8_t did) { }; + bool can_start() { }; + uint8_t get_max_attempts() { }; + uint16_t get_receive_time() { }; + uint16_t receive_frame(uint8_t *data, uint16_t max_length) { }; + uint16_t receive_response() { }; + void send_response(uint8_t response) { }; + void send_frame(uint8_t *data, uint16_t length) { }; +}; +``` + +If all is correct it should be possible to instantiate PJON using the new strategy: + +```cpp +PJON bus(44); +// Use PJON as always +``` + +Strategy related methods required for configuration can be defined within the strategy. For example the `SoftwareBitBang` strategy uses a dedicated setter for the communication pin: +```cpp +bus.strategy.set_pin(12); +``` diff --git a/hal/transport/PJON/driver/strategies/SoftwareBitBang/README.md b/hal/transport/PJON/driver/strategies/SoftwareBitBang/README.md new file mode 100644 index 000000000..0979285ed --- /dev/null +++ b/hal/transport/PJON/driver/strategies/SoftwareBitBang/README.md @@ -0,0 +1,104 @@ +## SoftwareBitBang + +| Medium | Pins used | Inclusion | +|--------|-----------|--------------------| +| Wire | 1 or 2 | `#include `| + +`SoftwareBitBang` is a software implementation of [PJDL (Padded Jittering Data Link)](/src/strategies/SoftwareBitBang/specification/PJDL-specification-v5.0.md). It supports simplex and half-duplex asynchronous serial communication for up to 254 devices over a single wire. The maximum length of the bus can reach between 800 and 2000 meters depending on the mode used. It is a valid alternative to 1-Wire because of its flexibility and reliability. Fault tolerance schemes can be easily implemented because communication pins can be configured at runtime. Take a look at the [video introduction](https://www.youtube.com/watch?v=GWlhKD5lz5w) for a brief showcase of its features. +```cpp +PJDL SINGLE WIRE BUS ______ + ______ ______ ______ ______ | | +| | | | | | | | |DEVICE| +|DEVICE| |DEVICE| |DEVICE| |DEVICE| |______| +|______| |______| |______| |______| | +___|__________|________|___________|______/\/\/\__| IO PIN + ___|__ __|___ ___|__ ___|__ | 110-180 Ω +| | | | | | | | | +|DEVICE| |DEVICE| |DEVICE| |DEVICE| |__/\/\/\__ GND +|______| |______| |______| |______| 8 kΩ - 5 MΩ +``` +It is suggested to add 8kΩ-5MΩ pull-down resistor as shown in the graph above to reduce externally induced interference. The longer is the length of the cable and the higher is the amount of induced interference, the lower should be the resistance of the pull-down resistor. Pins can be optionally protected against overload adding a current limiting resistor to each connected pin. The resistor value can be obtained solving the following equation `R = (operating voltage / pin max current drain)`, for example to obtain the current limiting resistor value for an Arduino Uno simply substitute its characteristics: `R = (5v / 0.030A) = 166.66Ω`. + +### Compatibility +| MCU | Clock | Supported pins | +| ---------------- |------ | ---------------- | +| ATtiny84/84A | 16MHz | 0, 1, 2, 3, 4 | +| ATtiny85 | 16MHz | 1, 2 | +| ATmega88/168/328 (Uno, Nano, Pro) | 16MHz | 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, A0, A1 | +| ATmega328PB | 16MHz | 10 | +| ATmega16u4/32u4 (Leonardo, Micro) | 16MHz | 2, 4, 8, 12 | +| ATmega2560 (Mega, Mega nano) | 16MHz | 3, 4, 7, 8, 9, 10, 12 | +| ATmega1284P | 16MHz | 18, 19, 20, 21, 22, 23, A0, A1, A2, A3, A4, A5, A6, A7 | +| SAMD (Arduino Zero) | 48MHz | D0, D1, D3, A0, A1 | +| STM32F1 | 72MHz | PB15, PB14, PB13, PB12, PB11, PB10, PB9, PB8, PB7, PB6, PB4, PB3, PA15, PA10 | +| MK20DX256 (Teensy 3.1) | 96MHz | All pins | +| ESP8266 (NodeMCU, AI-THINKER) | 80/160MHz | D1 or GPIO 5 | +| ESP32 (Heltech WiFi LoRa) | 160MHz | 12, 25 | + +### Performance +`SWBB_MODE` can be configured in 4 different modes, `1`, `2`, `3` and `4`: + +| Mode | Speed | Range | Supported MCUs | +| ---- | ----- |------ | ---------------- | +| `1` | 1.97kB/s | 2000m | ATtiny84/84A/85, ATmega88/168/328/328PB/16u4/32u4/2560/1284P, SAMD, STM32F1, MK20DX256, ESP8266, ESP32 | +| `2` | 2.21kB/s | 1600m | ATtiny84/84A/85, ATmega88/168/328/328PB/16u4/32u4/2560, STM32F1 | +| `3` | 3.10kB/s | 1200m | ATtiny84/84A/85, ATmega88/168/328, STM32F1 | +| `4` | 3.34kB/s | 800m | ATtiny84/84A/85, ATmega88/168/328, STM32F1 | + +When including and using the `SoftwareBitBang` strategy you have the complete access to the microcontroller. This happens because `SoftwareBitBang` runs a completely software-defined implementation, transforming a painful walk in a nice flight. + +Communication over a single wire enables quick and creative experimentation. The first suggested test, at the tester's risk, is to let two Arduino boards communicate [through a living body](https://www.youtube.com/watch?v=caMit7nzJsM) touching with the left hand the digital pin of the first board and with the right the pin of the other one (should be harmless). It is stunning to see it working perfectly through the human body, although it also works through water and other conductors. + +![PJDL communication over 2000m twisted pair](images/PJDL-2000m-mode4-twistedpair-8.2k-pulldown-60-series.png) + +The picture above shows a [PJDL](/src/strategies/SoftwareBitBang/specification/PJDL-specification-v5.0.md) frame transmitted over a 800m twisted pair using mode `4`. Although bits are substantially deformed the exchange occurs nominally and performance is not affected. This experiment was done and published by [Jack Anderson](https://github.com/jdaandersj) in [LANC Video Camera Control](http://jda.tel/pdf/lanc_video_camera_control.pdf) - Department of Computer Science [Loughborough University](https://www.lboro.ac.uk/departments/compsci/) (UK). + +### PJDL vs. 1-Wire + +| Protocol | Communication speed | Range | Communication mode | +| -------- | ------------------- | --------- | ---------------------- | +| PJDL | 1.97-3.34kB/s | 800-2000m | Simplex or half-duplex | +| 1-Wire | 2.03kB/s | 300m | Half-duplex | + +### Configuration +Before including the library it is possible to configure `SoftwareBitBang` using predefined constants: + +| Constant | Purpose | Supported value | +| ----------------------- |------------------------------------- | ------------------------------------------ | +| `SWBB_MODE` | Data transmission mode | 1, 2, 3, 4 | +| `SWBB_BACK_OFF_DEGREE` | Maximum back-off exponential degree | Numeric value (4 by default) | +| `SWBB_MAX_ATTEMPTS` | Maximum transmission attempts | Numeric value (20 by default) | +| `SWBB_PREAMBLE` | Preamble Length | Numeric value (1 by default), max 100 | +| `SWBB_MAX_PREAMBLE` | Maximum preamble length | Numeric value (1 by default), max 100 | + +`SoftwareBitBang` supports the use of input and output pins because separated signals may be required if additional circuitry is used for amplification or noise filtering. It also works if pins are directly connected as a simple point-to-point null-modem or cross-over serial link. + +```cpp +#include + +PJONSoftwareBitBang bus; + +void setup() { + // Set the pin 12 as the communication pin + bus.strategy.set_pin(12); + // Set pin 11 as input pin and pin 12 as output pin + bus.strategy.set_pins(11, 12); +} +``` +After the PJON object is defined with its strategy it is possible to set the communication pin accessing to the strategy present in the PJON instance. All the other necessary information is present in the general [Documentation](/documentation). + +### Why not interrupts? +In the Arduino environment the use of libraries is really extensive and often the end user is not able to go over collisions. Very often a library is using hardware resources of the microcontroller, colliding with other libraries. This happens because in general Arduino boards have limited hardware resources. Software-defined bit-banging, is a stable and reliable solution that leads to "more predictable" results than interrupt driven procedures coexisting on limited microcontrollers without the developer and the end user knowing about it. + +![PJON - Michael Teeuw application example](http://33.media.tumblr.com/0065c3946a34191a2836c405224158c8/tumblr_inline_nvrbxkXo831s95p1z_500.gif) + +PJON application example made by the user [Michael Teeuw](http://michaelteeuw.nl/post/130558526217/pjon-my-son) + +### Known issues +- A 1-5 MΩ pull down resistor could be necessary to reduce interference, see [Mitigate interference](https://github.com/gioblu/PJON/wiki/Mitigate-interference). +- When using more than one instance of `SoftwareBitBang` in the same sketch use pins part of different port groups to avoid cross-talk. +- During the execution of other tasks or delays a certain amount of packets could be potentially lost because transmitted out of the polling time of the receiver device. Thanks to the PJON packet handler after some retries the packet is received but a certain amount of bandwidth is wasted. If this situation occurs try to reduce the duration of other tasks and use a frame preamble setting `SWBB_MAX_PREAMBLE` and `SWBB_PREAMBLE` to a value between 1 and 100. The optimal preamble length is the maximum interval between each `bus.receive()` call divided by `SWBB_BIT_SPACER` +- `SoftwareBitBang` strategy can have compatibility issues with codebases that are using interrupts, reliability or bandwidth loss can occur because of the interruptions made by third party software. + +### Safety warning +In all cases, when installing or maintaining a PJON network, extreme care must be taken to avoid any danger. If devices are connected to AC power you are exposed to a high chance of being electrocuted if hardware is not installed carefully and properly. If you are not experienced enough ask the support of a skilled technician and consider that many countries prohibit uncertified installations. When a [SoftwareBitBang](/src/strategies/SoftwareBitBang) bus is installed [interference mitigation](https://github.com/gioblu/PJON/wiki/Mitigate-interference) and [protective circuitry](https://github.com/gioblu/PJON/wiki/Protective-circuitry) guidelines must be followed. If a common ground or power supply line is used its cable size and length must be carefully selected taking in consideration the overall application's power supply requirements and selected components' maximum rating. PJDL and its reference implementation [SoftwareBitBang](/src/strategies/SoftwareBitBang/README.md) are experimental, use them at your own risk. diff --git a/hal/transport/PJON/driver/strategies/SoftwareBitBang/SoftwareBitBang.h b/hal/transport/PJON/driver/strategies/SoftwareBitBang/SoftwareBitBang.h new file mode 100644 index 000000000..27e21ffff --- /dev/null +++ b/hal/transport/PJON/driver/strategies/SoftwareBitBang/SoftwareBitBang.h @@ -0,0 +1,405 @@ + +/* SoftwareBitBang + 1 or 2 wires software-defined asynchronous serial data link layer + used as a Strategy by PJON (included in version v3.0) + Compliant with PJDL (Padded Jittering Data Link) specification v5.0 + ___________________________________________________________________________ + + Copyright 2010-2020 Giovanni Blu Mitolo gioscarab@gmail.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + +#pragma once + +/* Set here the selected transmission mode - default STANDARD */ +#ifndef SWBB_MODE +#define SWBB_MODE 1 +#endif + +// Used to signal communication failure +#define SWBB_FAIL 65535 +// Used for pin handling +#define SWBB_NOT_ASSIGNED 255 + +/* Transmission speed modes (see Timing.h) + MODE 1: 1.97kB/s - 15808Bd + MODE 2: 2.21kB/s - 17696Bd + MODE 3: 3.10kB/s - 24844Bd + MODE 4: 3.34kB/s - 26755Bd */ +#include "Timing.h" + +// Recommended receive time for this strategy, in microseconds +#ifndef SWBB_RECEIVE_TIME +#define SWBB_RECEIVE_TIME 1000 +#endif + +class SoftwareBitBang +{ +public: + /* Returns the delay related to the attempts passed as parameter: */ + + uint32_t back_off(uint8_t attempts) + { + uint32_t result = attempts; + for(uint8_t d = 0; d < SWBB_BACK_OFF_DEGREE; d++) { + result *= (uint32_t)(attempts); + } + return result; + }; + + + /* Begin method, to be called on initialization: + (returns always true) */ + + bool begin(uint8_t did = 0) + { + PJON_DELAY(PJON_RANDOM(SWBB_INITIAL_DELAY) + did); + return true; + }; + + + /* Check if the channel is free for transmission: + If reading 10 bits no 1 is detected there is no active transmission */ + + bool can_start() + { + PJON_IO_MODE(_input_pin, INPUT); + // Look for ongoing transmission for 1 padding bit + 9 data bits + PJON_DELAY_MICROSECONDS(SWBB_BIT_SPACER / 2); + if(PJON_IO_READ(_input_pin)) { + return false; + } + PJON_DELAY_MICROSECONDS((SWBB_BIT_SPACER / 2)); + if(PJON_IO_READ(_input_pin)) { + return false; + } + PJON_DELAY_MICROSECONDS(SWBB_BIT_WIDTH / 2); + for(uint8_t i = 0; i < 9; i++) { + if(PJON_IO_READ(_input_pin)) { + return false; + } + PJON_DELAY_MICROSECONDS(SWBB_BIT_WIDTH); + } + if(PJON_IO_READ(_input_pin)) { + return false; + } + // Delay for the maximum expected latency and then check again + PJON_DELAY_MICROSECONDS(SWBB_LATENCY); + if(PJON_IO_READ(_input_pin)) { + return false; + } + // Delay for a small random time and then check again + PJON_DELAY_MICROSECONDS(PJON_RANDOM(SWBB_COLLISION_DELAY)); + if(PJON_IO_READ(_input_pin)) { + return false; + } + return true; + }; + + + /* Returns the maximum number of attempts for each transmission: */ + + static uint8_t get_max_attempts() + { + return SWBB_MAX_ATTEMPTS; + }; + + + /* Returns the recommended receive time for this strategy: */ + + static uint16_t get_receive_time() + { + return SWBB_RECEIVE_TIME; + }; + + + /* Handle a collision: */ + + void handle_collision() + { + PJON_DELAY_MICROSECONDS(PJON_RANDOM(SWBB_COLLISION_DELAY)); + }; + + + /* Read a byte from the pin */ + + uint8_t read_byte() + { + uint8_t byte_value = 0B00000000; + // Delay until the center of the first bit + PJON_DELAY_MICROSECONDS(SWBB_BIT_WIDTH / 2); + for(uint8_t i = 0; i < 7; i++) { + // Read in the center of the bit + byte_value += PJON_IO_READ(_input_pin) << i; + // Delay until the center of the next one + PJON_DELAY_MICROSECONDS(SWBB_BIT_WIDTH); + } + // Read in the center of the last one + byte_value += PJON_IO_READ(_input_pin) << 7; + // Delay until the end of the last bit + PJON_DELAY_MICROSECONDS(SWBB_BIT_WIDTH / 2); + return byte_value; + }; + + + /* Receive byte if in sync: */ + + uint16_t receive_byte() + { + if(sync()) { + return read_byte(); + } + return SWBB_FAIL; + }; + + + /* Receive byte response: + Transmitter emits a SWBB_BIT_WIDTH / 4 long bit and tries + to get a response cyclically for SWBB_RESPONSE_TIMEOUT microseconds. + Receiver synchronizes to the falling edge of the last incoming + bit and transmits PJON_ACK */ + + uint16_t receive_response() + { + if(_output_pin != _input_pin && _output_pin != SWBB_NOT_ASSIGNED) { + PJON_IO_WRITE(_output_pin, LOW); + } + uint16_t response = SWBB_FAIL; + uint32_t time = PJON_MICROS(); + while((uint32_t)(PJON_MICROS() - time) < _timeout) { + PJON_IO_WRITE(_input_pin, LOW); + if(sync()) { + response = receive_byte(); + } + if(response == SWBB_FAIL) { + PJON_IO_MODE(_output_pin, OUTPUT); + PJON_IO_WRITE(_output_pin, HIGH); + PJON_DELAY_MICROSECONDS(SWBB_BIT_WIDTH / 4); + PJON_IO_PULL_DOWN(_output_pin); + } else { + return response; + } + } + return response; + }; + + + /* Receive a frame: */ + + uint16_t receive_frame(uint8_t *data, uint16_t max_length) + { + uint16_t result; + if(max_length == PJON_PACKET_MAX_LENGTH) { + uint32_t time = PJON_MICROS(); + // Look for a frame initializer + if(!sync_preamble() || !sync() || !sync()) { + return SWBB_FAIL; + } + // Check its timing consistency + if( + (uint32_t)(PJON_MICROS() - time) < + (((SWBB_BIT_WIDTH * 3) + (SWBB_BIT_SPACER * 3)) - SWBB_ACCEPTANCE) + ) { + return SWBB_FAIL; + } + } // Receive one byte + result = receive_byte(); + if(result == SWBB_FAIL) { + return SWBB_FAIL; + } + *data = result; + return 1; + }; + + + /* Every byte is prepended with a synchronization pad made by 2 + padding bits. The first is a longer than standard logic 1 followed + by a standard logic 0. + __________ ___________________________ + | SyncPad | Byte | + |______ |___ ___ _____ | + | | | | | | | | | | + | | 1 | 0 | 1 | 0 0 | 1 | 0 | 1 1 | 0 | + |__|___|___|___|_____|___|___|_____|___| + | + Minimum acceptable HIGH padding bit duration + + The reception tecnique is based on finding a logic 1 as long as the + first padding bit within a certain threshold, synchronizing to its + falling edge and checking if it is followed by a logic 0. If this + pattern is recognised, reception starts, if not, interference, + synchronization loss or simply absence of communication is + detected at byte level. */ + + void send_byte(uint8_t b) + { + pulse(1); + for(uint8_t mask = 0x01; mask; mask <<= 1) { + PJON_IO_WRITE(_output_pin, b & mask); + PJON_DELAY_MICROSECONDS(SWBB_BIT_WIDTH); + } + }; + + + /* Send byte response: + Transmitter sends a SWBB_BIT_WIDTH / 4 microseconds long HIGH bit and + tries to receive a response cyclically for SWBB_RESPONSE_TIMEOUT + microseconds. Receiver synchronizes to the falling edge of the last + incoming bit and transmits its response */ + + void send_response(uint8_t response) + { + PJON_IO_PULL_DOWN(_input_pin); + uint32_t time = PJON_MICROS(); + while( // If initially low Wait for the next high + ((uint32_t)(PJON_MICROS() - time) < SWBB_BIT_WIDTH) && + !PJON_IO_READ(_input_pin) + ); + time = PJON_MICROS(); + while( // If high Wait for low + ((uint32_t)(PJON_MICROS() - time) < (SWBB_BIT_WIDTH / 4)) && + PJON_IO_READ(_input_pin) + ); // Transmit response prepended with a synchronization pad + PJON_IO_MODE(_output_pin, OUTPUT); + pulse(1); + send_byte(response); + PJON_IO_PULL_DOWN(_output_pin); + }; + + + /* The data is prepended with a frame initializer composed by 3 + synchronization pads to signal the start of a frame. + _________________ __________________________________ + | FRAME INIT | DATA 1-65535 bytes | + |_____ _____ _____|________________ _________________| + |Sync |Sync |Sync |Sync | Byte |Sync | Byte | + |___ |___ |___ |___ | __ |___ | _ _| + | | | | | | | | | | | | | | | | | | + | 1 |0| 1 |0| 1 |0| 1 |0|0000|11|00| 1 |0|00000|1|0|1| + |___|_|___|_|___|_|___|_|____|__|__|___|_|_____|_|_|_| + + Send a frame: */ + + void send_frame(uint8_t *data, uint16_t length) + { + _timeout = (length * SWBB_RESPONSE_OFFSET) + SWBB_LATENCY; + PJON_IO_MODE(_output_pin, OUTPUT); + pulse(3); // Send frame initializer + for(uint16_t b = 0; b < length; b++) { + send_byte(data[b]); // Send each byte + } + PJON_IO_PULL_DOWN(_output_pin); + }; + + + /* Check if a synchronization pad is incoming: + __________ + | SyncPad | + |______ | + | | | | + | | 1 | 0 | + |__|___|___| + | + Minimum acceptable HIGH padding bit duration + + The reception tecnique is based on finding a logic 1 as long as the + first padding bit within a certain threshold, synchronizing to its + falling edge and checking if it is followed by a logic 0. If this + pattern is recognised, synchronization may have been obtained, if + not, interference, synchronization loss or simply absence of + communication is detected at byte level: */ + + bool sync(uint32_t spacer) + { + PJON_IO_PULL_DOWN(_input_pin); + if((_output_pin != _input_pin) && (_output_pin != SWBB_NOT_ASSIGNED)) { + PJON_IO_PULL_DOWN(_output_pin); + } + uint32_t time = PJON_MICROS(); + while( + PJON_IO_READ(_input_pin) && + ((uint32_t)(PJON_MICROS() - time) <= spacer) + ); + time = PJON_MICROS() - time; + if(time < SWBB_ACCEPTANCE) { + return false; + } else { + PJON_DELAY_MICROSECONDS((SWBB_BIT_WIDTH / 2) - SWBB_READ_DELAY); + if(!PJON_IO_READ(_input_pin)) { + PJON_DELAY_MICROSECONDS(SWBB_BIT_WIDTH / 2); + return true; + } + } + return false; + }; + + bool sync() + { + return sync(SWBB_BIT_SPACER); + } + + bool sync_preamble() + { + return sync(SWBB_BIT_SPACER * SWBB_MAX_PREAMBLE); + }; + + /* Emit synchronization pulse: */ + + void pulse(uint8_t n) + { +#if SWBB_PREAMBLE != 1 + if (n == 3) { + // Transmit preamble + PJON_IO_WRITE(_output_pin, HIGH); + PJON_DELAY_MICROSECONDS(SWBB_BIT_SPACER * SWBB_PREAMBLE); + PJON_IO_WRITE(_output_pin, LOW); + PJON_DELAY_MICROSECONDS(SWBB_BIT_WIDTH); + n--; + } +#endif + while(n--) { + PJON_IO_WRITE(_output_pin, HIGH); + PJON_DELAY_MICROSECONDS(SWBB_BIT_SPACER); + PJON_IO_WRITE(_output_pin, LOW); + PJON_DELAY_MICROSECONDS(SWBB_BIT_WIDTH); + } + }; + + /* Set the communicaton pin: */ + + void set_pin(uint8_t pin) + { + PJON_IO_PULL_DOWN(pin); + _input_pin = pin; + _output_pin = pin; + }; + + + /* Set a pair of communication pins: */ + + void set_pins( + uint8_t input_pin = SWBB_NOT_ASSIGNED, + uint8_t output_pin = SWBB_NOT_ASSIGNED + ) + { + PJON_IO_PULL_DOWN(input_pin); + PJON_IO_PULL_DOWN(output_pin); + _input_pin = input_pin; + _output_pin = output_pin; + }; + +private: + uint16_t _timeout; + uint8_t _input_pin; + uint8_t _output_pin; +}; diff --git a/hal/transport/PJON/driver/strategies/SoftwareBitBang/Timing.h b/hal/transport/PJON/driver/strategies/SoftwareBitBang/Timing.h new file mode 100644 index 000000000..37b9f1e0d --- /dev/null +++ b/hal/transport/PJON/driver/strategies/SoftwareBitBang/Timing.h @@ -0,0 +1,391 @@ + +/* PJON SoftwareBitBang strategy Transmission Timing table + Copyright 2010-2020, Giovanni Blu Mitolo All rights reserved. + + Often timing in two different machines do not match, code execution + time can variate and time measurements are not perfectly equal. + Consider that durations defined below may differ from what is specified in + PJDL v5.0. This is done to accomodate machine's inner workings and + effectively produce the specified timing. + + Arduino Duemilanove/UNO/Nano is used as timing master, or the machine used + to test all new supported MCUs. + + Benchmarks can be executed using NetworkAnalysis and SpeedTest examples. + + MODE 1: 1.97kB/s - 15808Bd + MODE 2: 2.21kB/s - 17696Bd + MODE 3: 3.10kB/s - 24844Bd + MODE 4: 3.34kB/s - 26755Bd + + Use the same pin number on all connected devices to achieve maximum + timing efficiency, not all different pin combinations work nominally + because of execution timing discrepancies between physical pins. */ + +#pragma once + +/* ATmega88/168/328 - Arduino Duemilanove, Uno, Nano, Mini, Pro, Pro mini */ +#if defined(__AVR_ATmega88__) || defined(__AVR_ATmega168__) || \ + defined(__AVR_ATmega328__) || defined(__AVR_ATmega328P__) +#if SWBB_MODE == 1 +#if F_CPU == 16000000L +/* Working on pin: 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, A0, A1 */ +#define SWBB_BIT_WIDTH 40 +#define SWBB_BIT_SPACER 112 +#define SWBB_ACCEPTANCE 56 +#define SWBB_READ_DELAY 4 +#endif +#endif +#if SWBB_MODE == 2 +#if F_CPU == 16000000L +/* Working on pin: 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, A0, A1 */ +#define SWBB_BIT_WIDTH 36 +#define SWBB_BIT_SPACER 88 +#define SWBB_ACCEPTANCE 56 +#define SWBB_READ_DELAY 4 +#endif +#endif +#if SWBB_MODE == 3 +#if F_CPU == 16000000L +/* Working on pin: 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, A0, A1 */ +#define SWBB_BIT_WIDTH 24 +#define SWBB_BIT_SPACER 66 +#define SWBB_ACCEPTANCE 30 +#define SWBB_READ_DELAY 8 +#endif +#endif +#if SWBB_MODE == 4 +#if F_CPU == 16000000L +/* Working on pin: 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, A0, A1 */ +#define SWBB_BIT_WIDTH 22 +#define SWBB_BIT_SPACER 61 +#define SWBB_ACCEPTANCE 30 +#define SWBB_READ_DELAY 7 +#endif +#endif +#endif + +/* ATmega328PB ------------------------------------------------------------ */ +#if defined(__AVR_ATmega328PB__) +#if SWBB_MODE == 1 +#if F_CPU == 16000000L +/* Working on pin: 10 */ +#define SWBB_BIT_WIDTH 37 +#define SWBB_BIT_SPACER 110 +#define SWBB_ACCEPTANCE 56 +#define SWBB_READ_DELAY 4 +#endif +#endif +#if SWBB_MODE == 2 +#if F_CPU == 16000000L +/* Working on pin: 10 */ +#define SWBB_BIT_WIDTH 33 +#define SWBB_BIT_SPACER 88 +#define SWBB_ACCEPTANCE 56 +#define SWBB_READ_DELAY 8 +#endif +#endif +#endif + + +/* ATmega16/32U4 - Arduino Leonardo/Micro --------------------------------- */ +#if defined(__AVR_ATmega16U4__) || defined(__AVR_ATmega32U4__) +#if SWBB_MODE == 1 +/* Working on pin: 2, 4, 8, 12 + Fallback to default timing */ +#define SWBB_BIT_WIDTH 40 +#define SWBB_BIT_SPACER 112 +#define SWBB_ACCEPTANCE 56 +#define SWBB_READ_DELAY 8 +#endif +#if SWBB_MODE == 2 +/* Working on pin: 2, 4, 8, 12 + Fallback to default timing */ +#define SWBB_BIT_WIDTH 36 +#define SWBB_BIT_SPACER 88 +#define SWBB_ACCEPTANCE 56 +#define SWBB_READ_DELAY 12 +#endif +#endif + +/* ATmega1280/2560 - Arduino Mega/Mega-nano ------------------------------- */ +#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) +#if SWBB_MODE == 1 +/* Working on pin: 3, 4, 7, 8, 9, 10, 12 */ +#define SWBB_BIT_WIDTH 38 +#define SWBB_BIT_SPACER 110 +#define SWBB_ACCEPTANCE 62 +#define SWBB_READ_DELAY 11 +#endif +#if SWBB_MODE == 2 +/* Working on pin: 3, 4, 7, 8, 9, 10, 12 */ +#define SWBB_BIT_WIDTH 34 +#define SWBB_BIT_SPACER 86 +#define SWBB_ACCEPTANCE 58 +#define SWBB_READ_DELAY 10 +#endif +#endif + +/* ATtiny45/85 ------------------------------------------------------------ */ +#if defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__) +#if SWBB_MODE == 1 +#if F_CPU == 16000000L +/* Working on pin: 1, 2 + Fallback to default */ +#define SWBB_BIT_WIDTH 40 +#define SWBB_BIT_SPACER 112 +#define SWBB_ACCEPTANCE 56 +#define SWBB_READ_DELAY 4 +#endif +#endif +#if SWBB_MODE == 2 +#if F_CPU == 16000000L +/* Working on pin: 1, 2 + Fallback to default */ +#define SWBB_BIT_WIDTH 36 +#define SWBB_BIT_SPACER 88 +#define SWBB_ACCEPTANCE 56 +#define SWBB_READ_DELAY 4 +#endif +#endif +#endif + +/* ATtiny44/84/44A/84A ---------------------------------------------------- */ +#if defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__) || \ + defined(__AVR_ATtiny84A__) || defined(__AVR_ATtiny84A__) +#if SWBB_MODE == 1 +#if F_CPU == 16000000L +/* Working on pin: 0, 1, 2, 3, 4 + Fallback to default */ +#define SWBB_BIT_WIDTH 40 +#define SWBB_BIT_SPACER 112 +#define SWBB_ACCEPTANCE 56 +#define SWBB_READ_DELAY 4 +#endif +#endif +#if SWBB_MODE == 2 +#if F_CPU == 16000000L +/* Working on pin: 0, 1, 2, 3, 4 + Fallback to default */ +#define SWBB_BIT_WIDTH 36 +#define SWBB_BIT_SPACER 88 +#define SWBB_ACCEPTANCE 56 +#define SWBB_READ_DELAY 4 +#endif +#endif +#endif + +/* Arduino Zero ----------------------------------------------------------- */ +#if defined(ARDUINO_SAMD_ZERO) +#if SWBB_MODE == 1 +/* Added by Esben Soeltoft - 03/09/2016 + Updated by Giovanni Blu Mitolo - 31/05/2019 + Working on pin: D0, D1, D3, A0, A1 */ +#define SWBB_BIT_WIDTH 43 +#define SWBB_BIT_SPACER 115 +#define SWBB_ACCEPTANCE 40 +#define SWBB_READ_DELAY -4 +#endif +#endif + +/* NodeMCU, generic ESP8266 ----------------------------------------------- */ +#if defined(ESP8266) +#if SWBB_MODE == 1 +/* Added by github user 240974a - 09/03/2016 + Added full support to MODE 1 (80 and 160MHz) - 12/06/2018 */ +#if (F_CPU == 80000000L) || (F_CPU == 160000000L) +/* Working on pin: D1 or GPIO 5 */ +#define SWBB_BIT_WIDTH 44 +#define SWBB_BIT_SPACER 112 +#define SWBB_ACCEPTANCE 56 +#define SWBB_READ_DELAY -6 +#endif +#endif +#endif + +/* Heltech WiFi LoRa ESP32, generic ESP32 --------------------------------- */ +#if defined(ESP32) +#if SWBB_MODE == 1 +/* Added full support to MODE 1 - 28/06/2018 + Working on pin: 12 and 25 */ +#define SWBB_BIT_WIDTH 44 +#define SWBB_BIT_SPACER 112 +#define SWBB_ACCEPTANCE 56 +#define SWBB_READ_DELAY -2 +#endif +#endif + +/* MK20DX256 - Teensy ----------------------------------------------------- */ +#if defined(__MK20DX256__) +#if SWBB_MODE == 1 +/* Added by github user SticilFace - 25/04/2016 */ +#if F_CPU == 96000000L +#define SWBB_BIT_WIDTH 46 +#define SWBB_BIT_SPACER 112 +#define SWBB_ACCEPTANCE 40 +#define SWBB_READ_DELAY -10 +#endif +#endif +#endif + + +/* STM32F1 ---------------------------------------------------------------- */ +/* @jcallano 09-jul-2020 tested with pin PB15, PB14, PB13, PB12, PB11, PB10, + PB9, PB8, PB7, PB6, PB4, PB3, PA15, PA10. 5v tolerant pins on bluepill */ + +#if defined(__STM32F1__) +#if SWBB_MODE == 1 +#if F_CPU == 72000000L +#define SWBB_BIT_WIDTH 43 +#define SWBB_BIT_SPACER 115 +#define SWBB_ACCEPTANCE 60 +#define SWBB_READ_DELAY 3 +#endif +#endif +#if SWBB_MODE == 2 +#if F_CPU == 72000000L +#define SWBB_BIT_WIDTH 39 +#define SWBB_BIT_SPACER 91 +#define SWBB_ACCEPTANCE 47 +#define SWBB_READ_DELAY 3 +#endif +#endif +#if SWBB_MODE == 3 +#if F_CPU == 72000000L +#define SWBB_BIT_WIDTH 27.5 +#define SWBB_BIT_SPACER 69.5 +#define SWBB_ACCEPTANCE 33 +#define SWBB_READ_DELAY -5 +#endif +#endif +#if SWBB_MODE == 4 +#if F_CPU == 72000000L +#define SWBB_BIT_WIDTH 25 +#define SWBB_BIT_SPACER 59 +#define SWBB_ACCEPTANCE 30 // +#define SWBB_READ_DELAY 4 +#endif +#endif +#endif + +/* Avoid error if any previous defined ------------------------------------ */ +#if SWBB_MODE == 1 +#ifndef SWBB_BIT_WIDTH +#define SWBB_BIT_WIDTH 40 +#endif +#ifndef SWBB_BIT_SPACER +#define SWBB_BIT_SPACER 112 +#endif +#ifndef SWBB_ACCEPTANCE +#define SWBB_ACCEPTANCE 56 +#endif +#ifndef SWBB_READ_DELAY +#define SWBB_READ_DELAY 4 +#endif +#ifndef SWBB_LATENCY +#define SWBB_LATENCY 13 +#endif +#endif +#if SWBB_MODE == 2 +#ifndef SWBB_BIT_WIDTH +#define SWBB_BIT_WIDTH 36 +#endif +#ifndef SWBB_BIT_SPACER +#define SWBB_BIT_SPACER 88 +#endif +#ifndef SWBB_ACCEPTANCE +#define SWBB_ACCEPTANCE 56 +#endif +#ifndef SWBB_READ_DELAY +#define SWBB_READ_DELAY 4 +#endif +#ifndef SWBB_LATENCY +#define SWBB_LATENCY 10 +#endif +#endif +#if SWBB_MODE == 3 +#ifndef SWBB_BIT_WIDTH +#define SWBB_BIT_WIDTH 24 +#endif +#ifndef SWBB_BIT_SPACER +#define SWBB_BIT_SPACER 66 +#endif +#ifndef SWBB_ACCEPTANCE +#define SWBB_ACCEPTANCE 30 +#endif +#ifndef SWBB_READ_DELAY +#define SWBB_READ_DELAY 8 +#endif +#ifndef SWBB_LATENCY +#define SWBB_LATENCY 8 +#endif +#endif +#if SWBB_MODE == 4 +#ifndef SWBB_BIT_WIDTH +#define SWBB_BIT_WIDTH 22 +#endif +#ifndef SWBB_BIT_SPACER +#define SWBB_BIT_SPACER 61 +#endif +#ifndef SWBB_ACCEPTANCE +#define SWBB_ACCEPTANCE 30 +#endif +#ifndef SWBB_READ_DELAY +#define SWBB_READ_DELAY 7 +#endif +#ifndef SWBB_LATENCY +#define SWBB_LATENCY 5 +#endif +#endif + + +/* Frame preamble length (by default set to 1 x SWBB_BIT_SPACER) + Maximum allowed value is 100 or a preamble of 100 x SWBB_BIT_SPACER + When devices execute other tasks frames may be lost unheard. The preamble + can be used to avoid transmission failure. To avoid retransmissions the + preamble length should be slightly longer than the task's duration. */ + +#ifndef SWBB_PREAMBLE +#define SWBB_PREAMBLE 1 +#endif + +/* Maximum frame preamble length (by default set to 1 x SWBB_BIT_SPACER) + Maximum allowed value is 100 or a preamble of 100 x SWBB_BIT_SPACER + To have full interoperability set SWBB_MAX_PREAMBLE = 100 */ + +#ifndef SWBB_MAX_PREAMBLE +#define SWBB_MAX_PREAMBLE 1 +#endif + +/* Synchronous acknowledgement response offset. + If (latency + CRC computation) > (SWBB_RESPONSE_OFFSET * length) + synchronous acknowledgement reliability could be affected or disrupted + set a higher SWBB_RESPONSE_OFFSET if necessary. */ + +#ifndef SWBB_RESPONSE_OFFSET +#define SWBB_RESPONSE_OFFSET 20 +#endif + +/* Maximum initial delay in milliseconds: */ + +#ifndef SWBB_INITIAL_DELAY +#define SWBB_INITIAL_DELAY 1000 +#endif + +/* Maximum delay in case of collision in microseconds: */ + +#ifndef SWBB_COLLISION_DELAY +#define SWBB_COLLISION_DELAY 16 +#endif + +/* Maximum transmission attempts */ + +#ifndef SWBB_MAX_ATTEMPTS +#define SWBB_MAX_ATTEMPTS 20 +#endif + +/* Back-off exponential degree */ + +#ifndef SWBB_BACK_OFF_DEGREE +#define SWBB_BACK_OFF_DEGREE 4 +#endif diff --git a/hal/transport/PJON/driver/strategies/SoftwareBitBang/images/PJDL-2000m-mode4-twistedpair-8.2k-pulldown-60-series.png b/hal/transport/PJON/driver/strategies/SoftwareBitBang/images/PJDL-2000m-mode4-twistedpair-8.2k-pulldown-60-series.png new file mode 100644 index 000000000..2f6915a35 Binary files /dev/null and b/hal/transport/PJON/driver/strategies/SoftwareBitBang/images/PJDL-2000m-mode4-twistedpair-8.2k-pulldown-60-series.png differ diff --git a/hal/transport/PJON/driver/strategies/SoftwareBitBang/specification/PJDL-specification-v5.0.md b/hal/transport/PJON/driver/strategies/SoftwareBitBang/specification/PJDL-specification-v5.0.md new file mode 100644 index 000000000..4c4be9e0b --- /dev/null +++ b/hal/transport/PJON/driver/strategies/SoftwareBitBang/specification/PJDL-specification-v5.0.md @@ -0,0 +1,129 @@ + +### Specifications index + +#### Network layer +- [PJON (Padded Jittering Operative Network) v4.0](/specification/PJON-protocol-specification-v4.0.md) +- [Network services list](/specification/PJON-network-services-list.md) +#### Data link layer +- **[PJDL (Padded Jittering Data Link) v5.0](/src/strategies/SoftwareBitBang/specification/PJDL-specification-v5.0.md)** +- [PJDLR (Padded Jittering Data Link over Radio) v3.0](/src/strategies/OverSampling/specification/PJDLR-specification-v3.0.md) +- [PJDLS (Padded Jittering Data Link byte Stuffed) v2.0](/src/strategies/AnalogSampling/specification/PJDLS-specification-v2.0.md) +- [TSDL (Tardy Serial Data Link) v3.0](/src/strategies/ThroughSerial/specification/TSDL-specification-v3.0.md) +- [SFSP (Secure Frame Separation Protocol) v1.0](/specification/SFSP-frame-separation-specification-v1.0.md) + +--- + +## PJDL v5.0 +``` +Invented by Giovanni Blu Mitolo with the support of +Fred Larsen, Julio Aguirre, Gerhard Sittig and Jack Anderson +Publication date: 10/04/2010 Latest revision: 24/07/2020 +Related implementation: /src/strategies/SoftwareBitBang/ +Compatible versions: PJON v13.0 and following +Released into the public domain + +10/04/2010 0.1 - First experimental release +12/02/2017 1.0 - Frame initializer, response made safe +31/03/2017 1.1 - Physical layer info +24/09/2017 2.0 - Modes 1, 2, 3 +29/12/2018 3.0 - Medium access control info, mode 4 +03/07/2019 4.0 - Response initializer +10/03/2020 4.1 - Maximum range experimentally determined +17/07/2020 5.0 - Timeout, tolerance and preamble added +``` +PJDL (Padded Jittering Data Link) is an asynchronous serial data link for low-data-rate applications that supports both master-slave and multi-master communication over a common conductive medium. PJDL can be easily implemented on limited microcontrollers with low clock accuracy and can operate directly using a single input-output pin. + +### Physical layer +The medium's maximum length is limited by the wiring resistance, by the voltage level used and by externally induced interference. The maximum length of the bus can reach between 800 and 2000 meters depending on the mode used. +```cpp +PJDL SINGLE WIRE BUS ______ + ______ ______ ______ ______ | | +| | | | | | | | |DEVICE| +|DEVICE| |DEVICE| |DEVICE| |DEVICE| |______| +|______| |______| |______| |______| | +___|__________|________|___________|______/\/\/\__| IO PIN + ___|__ __|___ ___|__ ___|__ | 110-180 Ω +| | | | | | | | | +|DEVICE| |DEVICE| |DEVICE| |DEVICE| |__/\/\/\__ GND +|______| |______| |______| |______| 8 kΩ - 5 MΩ +``` +It is suggested to add 8kΩ-5MΩ pull-down resistor as shown in the graph above to reduce externally induced interference. The longer is the length of the cable and the higher is the amount of induced interference, the lower should be the resistance of the pull-down resistor. Pins can be optionally protected against overload adding a current limiting resistor to each connected pin. The resistor value can be obtained solving the following equation `R = (operating voltage / pin max current drain)`, for example to obtain the current limiting resistor value for an Arduino Uno simply substitute its characteristics: `R = (5v / 0.030A) = 166.66Ω`. + +### Communication modes +The proposed communication modes are the result of years of testing and optimization and have been selected to be easily supported by limited microcontrollers: + +| Mode | Bandwidth | Range | Preamble bit | Pad bit | Data bit | Keep busy bit | Latency | Timeout | +| ---- | ------------------ | ----- | ------------ | ------- | -------- | ------------- | ------- | ------- | +| 1 | 1.97kB/s - 15808Bd | 2000m | 11000µs | 110µs | 44µs | 11µs | 13µs | 20µs/B | +| 2 | 2.21kB/s - 17696Bd | 1600m | 9200µs | 92µs | 40µs | 10µs | 10µs | 20µs/B | +| 3 | 3.10kB/s - 24844Bd | 1200m | 7000µs | 70µs | 28µs | 7µs | 8µs | 20µs/B | +| 4 | 3.34kB/s - 26755Bd | 800m | 6500µs | 65µs | 26µs | 6.5µs | 5µs | 20µs/B | + +The following table specifies the exclusive acceptable tolerance of each bit type: + +| Mode | Preamble bit | Padding bit | Data bit nonet | Keep busy bit | +| ---- | -------------- | ----------- | -------------- | ------------- | +| 1 | -11000us +0us | -5us +17us | -5us +17us | -5µs +10µs | +| 2 | -9200us +0us | -4us +16us | -4us +16us | -5µs +10µs | +| 3 | -7000us +0us | -3us +11us | -3us +11us | -3µs +10µs | +| 4 | -6500us +0us | -3us +10us | -3us +10us | -3µs +10µs | + +Padding bit, data bit and keep busy bit have higher positive tolerance to accept bit-banged signals that are generally longer than expected. + +### Medium access control +PJDL specifies a variation of the carrier-sense, non-persistent random multiple access method (non-persistent CSMA). Devices can detect an ongoing transmission for this reason collisions can only occur in multi-master mode when 2 or more devices start to transmit at the same time. When a collision occurs it can be detected by the receiver because of synchronization loss or by the transmitter if an active collision avoidance procedure is implemented. + +### Byte transmission +Byte transmission is composed by 10 bits, the first two are called synchronization pad and are used to obtain sampling synchronization. The synchronization pad is composed by a high padding bit 2.5 times longer than data bits and a low data bit. The following 8 data bits contain information in LSB-first (least significant bit first) order. + +The reception technique is based on 3 steps: +1. Find a high bit which matches a padding bit +2. Synchronize with its falling edge +3. Ensure it is followed by a low data bit + +If so reception starts, if not, interference, synchronization loss or simply absence of communication is detected. While receiving a sequence of bytes a synchronization pad is acceptable even if prepended by a 0 of up to the maximum positive data bit nonet tolerance. + +```cpp + ___________ ___________________________ +| SYNC PAD | DATA | +|_______ |___ ___ _____ | +| | | | | | | | | +| 1 | 0 | 1 | 0 0 | 1 | 0 | 1 1 | 0 | +|_______|___|___|_____|___|___|_____|___| +``` +The synchronization pad adds overhead although it includes synchronization along with the data and eliminates the need of a dedicated clock line. The presence of the synchronization pad between each byte also ensures that a frame composed of a series of bytes with decimal value 0 can be transmitted safely without risk of collision. + +### Frame transmission +Before a frame transmission the communication medium's state is analysed, if high communication is detected and collision is avoided, if low for a duration of one byte plus the latency and a small random time, frame transmission starts with a frame preamble and a frame initializer composed by 3 consecutive synchronization pads followed by data bytes. The synchronization pad is used for both byte and frame initialization to reduce the implementation complexity. PJDL frames do not have an intrinsic length limit. +```cpp + ________ __________ _________________ ________________ +|ANALYSIS| PREAMBLE | FRAME INIT | DATA BYTES | +|________|__________|_____ _____ _____|________________| +| | |Sync |Sync |Sync |Sync | Byte | +| |__________|___ |___ |___ |___ | __ | +| | | | | | | | | | | | | | +|00000000| 1 | 1 |0| 1 |0| 1 |0| 1 |0|0000|11|00| +|________|__________|___|_|___|_|___|_|___|_|____|__|__| +``` +When a frame is received a low performance microcontroller with an inaccurate clock can correctly identify a preamble, synchronize with transmitter during the frame initializer and consequently each byte is received. Frame initialization is 100% reliable, false positives can only occur because of externally induced interference. If the implementation applies polling, the preamble can be used to reduce the chances of transmission failure when the receiver's polling frequency is too low to detect incoming frames. The preamble's maximum length is 100 times the length of a padding bit. + +### Synchronous response +A frame transmission can be optionally followed by a synchronous response sent by its recipient. Between frame transmission and a synchronous response there is a variable time which duration is influenced by latency. +```cpp +Transmission end Response + ______ ______ ______ _____ +| BYTE || BYTE || BYTE | CRC COMPUTATION / LATENCY | ACK | +|------||------||------|---------------------------|-----| +| || || | | 6 | +|______||______||______| |_____| +``` +In order to avoid other devices to detect the medium free for use and disrupt an ongoing exchange, the sender cyclically transmits a high 1/4 data bit and consequently attempts to receive a response for up to twice the maximum expected latency. The receiver must synchronize to the falling edge of the last high bit and, in order to avoid false positives in case of collision, must transmit its response prepended with an additional synchronization pad. If the response is not transmitted or not received the transmitter continues to keep busy the medium up to the response timeout. +```cpp +Transmission end Bus is kept busy Response + ______ ______ ______ _ _ _ _ _ _ ____ _____ +| BYTE || BYTE || BYTE | | | | | | | | | | | | |SYNC| ACK | +|------||------||------| | | | | | | | | | | | |----|-----| +| || || | | | | | | | | | | | | | | 6 | +|______||______||______|_| |_| |_| |_| |_| |_| |____|_____| +``` +The response timeout is determined multiplying 20µs by the length of the frame and then adding the maximum expected latency. diff --git a/hal/transport/PJON/driver/strategies/SoftwareBitBang/specification/obsolete/PJDL-specification-v1.0.md b/hal/transport/PJON/driver/strategies/SoftwareBitBang/specification/obsolete/PJDL-specification-v1.0.md new file mode 100644 index 000000000..86e772fcc --- /dev/null +++ b/hal/transport/PJON/driver/strategies/SoftwareBitBang/specification/obsolete/PJDL-specification-v1.0.md @@ -0,0 +1,70 @@ + +```cpp +/* +Milan, Italy - 10/04/2010 +PJDL (Padded jittering data link) specification is an invention and intellectual property +of Giovanni Blu Mitolo - Copyright 2010-2020 All rights reserved + +Related work: /src/strategies/SoftwareBitBang/ +Compliant implementation versions: PJON 6.0-7.1 +*/ +``` +### PJDL (Padded Jittering Data Link) +PJDL (Padded Jittering Data Link) has been specified to enable a new way to transmit data in simplex and half-duplex mode using cheap and low performance microcontrollers, totally software emulated, without the need of hardware interrupts for its working procedure. It is designed to support many devices sharing the same medium, to avoid collisions and operate in spite of interference. Extended tests proved its effectiveness on different media like electricity, radio frequency and light. + +### Basic concepts +* Use a pattern of predefined initial padding bits to identify a potential byte transmission +* Use the falling edge from 1 to 0, present in padding bits, to achieve byte level synchronization +* Detect interference or absence of communication at byte level +* Enable channel analysis and collision avoidance +* Enable a collision free synchronous acknowledgement pattern + +#### Byte transmission +Every byte is prepended with 2 synchronization padding bits and transmission occurs LSB-first. The first is a longer than standard logic 1 followed by a standard logic 0. The reception tecnique is based on finding a logic 1 as long as the first padding bit within a certain threshold, synchronizing to its falling edge and checking if it is followed by a logic 0. If this pattern is detected, reception starts, if not, interference, synchronization loss or simply absence of communication is detected at byte level. +```cpp + __________ ___________________________ +| SyncPad | Byte | +|______ |___ ___ _____ | +| | | | | | | | | | +| | 1 | 0 | 1 | 0 0 | 1 | 0 | 1 1 | 0 | +|_ |___|___|___|_____|___|___|_____|___| + | + SWBB_ACCEPTANCE (or minimum acceptable HIGH padding bit duration) +``` +Padding bits are adding a certain overhead to information but are reducing the need of precise timing because synchronization is renewed every byte. All the first padding bit duration minus `SWBB_ACCEPTANCE` is the synchronization window the receiver has for every incoming byte. If the length of the first padding bit is less than `SWBB_ACCEPTANCE` the received signal is considered interference. + +#### Packet transmission +Before a packet transmission, the medium is analyzed to detect ongoing communication and avoid collision. Thanks to the presence of padding bits, also a packet composed by 100 bytes, all with a decimal value of 0, can be transmitted safely without risk of third-party collision. +```cpp + ________________ _________________ ________________ ________________ __________________ +|Sync | Byte |Sync | Byte |Sync | Byte |Sync | Byte |Sync | Byte | +|___ | __ |___ | _ _|___ | _ |___ | _ |___ | _ _ | +| | | | | | | | | | | | | | | | | | | | | | | | | | | | | +| 1 |0|0000|11|00| 1 |0|00000|1|0|1| 1 |0|00000|1|00| 1 |0|0|1|000000| 1 |0|0|1|00|1|000| +|___|_|____|__|__|___|_|_____|_|_|_|___|_|_____|_|__|___|_|_|_|______|___|_|_|_|__|_|___| +``` +In a scenario where a stream of bytes is coming, low performance or clock inaccurate microcontrollers can be correctly synchronized back with transmitter every byte (thanks to padding bits) and easily detect interference or the end of transmission. + +#### Synchronous acknowledgement +After packet reception, CRC is calculated and a single character is transmitted: `PJON_ACK` (value 6) if the packet's content is correct or `PJON_NAK` (value 21) if an error is detected. +```cpp +Transmission Response + ________________________________________ _____ +| ID | HEADER | LENGTH | CONTENT | CRC | CRC COMPUTATION | ACK | +|----|----------|--------|---------|-----|-------> <-------|-----| +| 12 | 00000100 | 5 | 64 | 72 | LATENCY | 6 | +|____|__________|________|_________|_____| |_____| +``` + +Between a packet transmission and a synchronous acknowledgement transmission from the packet's receiver there is a variable timeframe influenced in its duration by medium latency and CRC computation time. In order to avoid other devices to consider the medium free and start transmitting in the middle of a transmission and a response, the packet's transmitter cyclically transmits a `BIT_WIDTH / 4` HIGH bit and consequently attempts to receive a response. On the other side the receiver can synchronize its acknowledgement transmission after the last incoming HIGH bit and try more than once if necessary. +```cpp +Transmission Response + ________________________________________ _ _ _ _ _ _ _ _____ +| ID | HEADER | LENGTH | CONTENT | CRC | | | | | | | | | | | | | | | ACK | +|----|----------|--------|---------|-----| | | | | | | | | | | | | | |-----| +| 12 | 00000100 | 500 | | 72 | | | | | | | | | | | | | | | 6 | +|____|__________|________|_________|_____|_| |_| |_| |_| |_| |_| |_| |_____| + +``` + +The maximum time dedicated to potential acknowledgement reception and consequent channel jittering is defined by the use case constraints like maximum packet length and devices distance. Thanks to the presence of the jittering wave, many differently configured devices can coexist on the same medium with no risk of collision. diff --git a/hal/transport/PJON/driver/strategies/SoftwareBitBang/specification/obsolete/PJDL-specification-v1.1.md b/hal/transport/PJON/driver/strategies/SoftwareBitBang/specification/obsolete/PJDL-specification-v1.1.md new file mode 100644 index 000000000..6c2e30ee9 --- /dev/null +++ b/hal/transport/PJON/driver/strategies/SoftwareBitBang/specification/obsolete/PJDL-specification-v1.1.md @@ -0,0 +1,87 @@ + +```cpp +/* +Milan, Italy - 31/03/2017 +PJDL (Padded jittering data link) specification +is an invention and intellectual property of Giovanni Blu Mitolo +Copyright 2010-2020 All rights reserved + +Related work: /src/strategies/SoftwareBitBang/ +Compliant implementation versions: PJON 8.0 and following + +Changelog: Response generalization / refactoring +*/ +``` +### PJDL (Padded Jittering Data Link) +PJDL (Padded Jittering Data Link) has been specified to enable a new software emulated simplex or half-duplex data link layer supporting one or many to many communication on a single channel or medium. It can be run on cheap and low performance microcontrollers, it supports communication for many devices connected to the same wire and stable operation in spite of interference. The main bus maximum length limit is related to its electric resistance, if at destination a high bit voltage is lower than the input-output port threshold voltage, its value can be detected erroneously. It has been tested with up to 50 meters long insulated wire of different qualities demonstrating the ability to achieve reliable communication also in this scenario. +```cpp + ______ ______ ______ ______ ______ +| | | | | | | | | | +|DEVICE| |DEVICE| |DEVICE| |DEVICE| |DEVICE| +|______| |______| |______| |______| |______| +___|___________|___________|___________|___________|___ + ___|__ ___|__ ___|__ ___|__ | SINGLE WIRE BUS +| | | | | | | | | +|DEVICE| |DEVICE| |DEVICE| |DEVICE| |___/\/\/\___ GND +|______| |______| |______| |______| 1-5 MΩ +``` +It is suggested to add 1-5 MΩ pull-down resistor as shown in the graph above to protect MCU pins and to reduce interference. + +### Basic concepts +* Use a pattern of predefined initial padding bits to identify a potential byte transmission +* Use the falling edge from 1 to 0, present in padding bits, to achieve byte level synchronization +* Detect interference or absence of communication at byte level +* Support collision avoidance +* Support string transmission +* Support 1 byte synchronous response to string transmission + +#### Byte transmission +Every byte is prepended with 2 synchronization padding bits and transmission occurs LSB-first. The first is a longer than standard logic 1 followed by a standard logic 0. The reception method is based on finding a logic 1 as long as the first padding bit within a certain threshold, synchronizing to its falling edge and checking if it is followed by a logic 0. If this pattern is detected, reception starts, if not, interference, synchronization loss or simply absence of communication is detected at byte level. +```cpp + __________ ___________________________ +| SyncPad | Byte | +|______ |___ ___ _____ | +| | | | | | | | | | +| | 1 | 0 | 1 | 0 0 | 1 | 0 | 1 1 | 0 | +|__|___|___|___|_____|___|___|_____|___| + | +SWBB_ACCEPTANCE +(or minimum acceptable HIGH padding bit duration) +``` +Padding bits add a certain overhead but are reducing the need of precise timing because synchronization is renewed every byte. All the first high padding bit duration minus `SWBB_ACCEPTANCE` is the synchronization timeframe the receiver has to synchronize and correctly receive a byte. If the length of the first padding bit is less than `SWBB_ACCEPTANCE` the received signal is considered interference. + +#### String transmission +Before a packet transmission, the medium is analyzed to detect ongoing communication and avoid collision. Thanks to the presence of padding bits, also a packet composed by 100 bytes, all with a decimal value of 0, can be transmitted safely without risk of third-party collision. +```cpp + ________________ _________________ ________________ +|Sync | Byte |Sync | Byte |Sync | Byte | +|___ | __ |___ | _ _|___ | _ | +| | | | | | | | | | | | | | | | | +| 1 |0|0000|11|00| 1 |0|00000|1|0|1| 1 |0|00000|1|00| +|___|_|____|__|__|___|_|_____|_|_|_|___|_|_____|_|__| +``` +In a scenario where a stream of bytes is coming, low performance or clock inaccurate microcontrollers can be correctly synchronized back with transmitter every byte (thanks to padding bits) and easily detect interference or the end of transmission. + +#### Synchronous response +A string transmission can be optionally followed by a synchronous response by its recipient. +```cpp +Transmission Response + ______ ______ ______ ______ ______ _____ +| BYTE || BYTE || BYTE || BYTE || BYTE | CRC COMPUTATION | ACK | +|------||------||------||------||------|-----------------|-----| +| || || || || | LATENCY | 6 | +|______||______||______||______||______| |_____| +``` + +Between a string transmission and a synchronous response there is a variable timeframe related to latency and computation time. In order to avoid other devices to consider the channel free and disrupt an ongoing exchange, sender cyclically transmits a `BIT_WIDTH / 4` HIGH bit and consequently attempts to receive a response. On the other side receiver can synchronize its response transmission after the last incoming high bit and try more than once if necessary. +```cpp +Transmission Response + ______ ______ ______ ______ ______ _ _ _ _ _ _____ +| BYTE || BYTE || BYTE || BYTE || BYTE | | | | | | | | | | | ACK | +|------||------||------||------||------| | | | | | | | | | |-----| +| || || || || | | | | | | | | | | | 6 | +|______||______||______||______||______|_| |_| |_| |_| |_| |_____| + +``` + +The maximum time dedicated to potential acknowledgement reception and consequent channel jittering is defined by the use case constraints like maximum packet length and devices distance. Thanks to the presence of the jittering wave, many differently configured devices can coexist on the same medium with no risk of collision. diff --git a/hal/transport/PJON/driver/strategies/SoftwareBitBang/specification/obsolete/PJDL-specification-v2.0.md b/hal/transport/PJON/driver/strategies/SoftwareBitBang/specification/obsolete/PJDL-specification-v2.0.md new file mode 100644 index 000000000..b0566b129 --- /dev/null +++ b/hal/transport/PJON/driver/strategies/SoftwareBitBang/specification/obsolete/PJDL-specification-v2.0.md @@ -0,0 +1,109 @@ + +```cpp +/* +Milan, Italy +Originally published: 10/04/2010 +latest revision: 24/09/2017 +PJDL (Padded Jittering Data Link) v2.0 specification +Invented by Giovanni Blu Mitolo, +released into the public domain + +Related implementation: /strategies/SoftwareBitBang/ +Compliant versions: PJON 9.0 and following + +Changelog: +- Added frame separation +- Added communication modes specification +*/ +``` + +### PJDL v2.0 +PJDL (Padded Jittering Data Link) is a simplex or half-duplex data link layer, that can be easily software emulated, enabling one or many to many communication over a single conductive medium or bus, connected to device's input-output ports, in both master-slave and multi-master configuration. It has been engineered to have limited minimum requirements, and to be efficiently executed on limited microcontrollers with low clock accuracy. No additional hardware is required to apply PJDL, and, being implemented in less than 350 lines of code, it is easily portable to many different architectures. Bus maximum length is limited by its electric resistance; it has been tested with up to 50m long insulated wires and results demonstrate the same high performance achieved with shorter lengths. + +### Basic concepts +* Define a synchronization pad initializer to identify a byte +* Use synchronization pad's falling edge to achieve byte level synchronization +* Use 3 consequent synchronization pads identify a frame +* Detect interference or absence of communication at byte level +* Support collision avoidance and detection +* Support error detection +* Support 1 byte synchronous response to frame + +```cpp + ______ ______ ______ ______ ______ +| | | | | | | | | | +|DEVICE| |DEVICE| |DEVICE| |DEVICE| |DEVICE| +|______| |______| |______| |______| |______| +___|___________|___________|___________|___________|___ + ___|__ ___|__ ___|__ ___|__ | SINGLE WIRE BUS +| | | | | | | | | +|DEVICE| |DEVICE| |DEVICE| |DEVICE| |___/\/\/\___ GND +|______| |______| |______| |______| 1-5 MΩ +``` +It is suggested to add 1-5 MΩ pull-down resistor as shown in the graph above to protect MCU pins and to reduce interference. The higher is the proximity of sources of induced interference, the higher is the required resistance of the pull-down resistor. + +#### Byte transmission +Each byte is prepended with a synchronization pad and transmission occurs LSB-first. The first bit is a longer than standard logic 1 followed by a standard logic 0. The reception method is based on finding a logic 1 as long as the first padding bit within a certain threshold, synchronizing to its falling edge and checking if it is followed by a logic 0. If this pattern is detected, reception starts, if not, interference, synchronization loss or simply absence of communication is detected at byte level. +```cpp + __________ ___________________________ +| SyncPad | Byte | +|______ |___ ___ _____ | +| | | | | | | | | | +| | 1 | 0 | 1 | 0 0 | 1 | 0 | 1 1 | 0 | +|__|___|___|___|_____|___|___|_____|___| + | +Minimum acceptable HIGH padding bit duration +``` +Padding bits add a certain overhead but are reducing the need of precise timing because synchronization is renewed every byte. The first padding bit duration is the synchronization timeframe the receiver has to receive a byte. If the length of the first padding bit is less than the minimum acceptable duration, the received signal is considered interference. The minimum acceptable duration of the first padding bit must be lower than a padding bit duration; a large minimum acceptable duration reduces the chances of occurring false positives, a small minimum acceptable duration instead mitigates timing inaccuracy, for this reason it is suggested to evaluate its setting depending on requirements and available resources. + +#### Frame transmission +Before a frame transmission, the communication medium is analysed, if logic 1 is present ongoing communication is detected and collision avoided, if logic 0 is detected for a duration longer than a byte transmission plus its synchronization pad and a small random timeframe, frame transmission starts with 3 synchronization pads, followed by data bytes. The presence of synchronization pads with their logic 1 between each byte ensures that also a frame composed of a series of bytes with decimal value 0 can be transmitted safely without risk of third-party collision. +```cpp + ________ _________________ __________________________________ +|ANALYSIS| FRAME INIT | DATA 1-65535 bytes | +|________|_____ _____ _____|________________ _________________| +| |Sync |Sync |Sync |Sync | Byte |Sync | Byte | +| |___ |___ |___ |___ | __ |___ | _ _| +| | | | | | | | | | | | | | | | | | | +|00000000| 1 |0| 1 |0| 1 |0| 1 |0|0000|11|00| 1 |0|00000|1|0|1| +|________|___|_|___|_|___|_|___|_|____|__|__|___|_|_____|_|_|_| +``` +In a scenario where a frame is received, low performance microcontrollers with inaccurate clock can correctly synchronize with transmitter during the frame initializer, and consequently each byte is received. The frame initializer is detected if 3 synchronizations occurred and if its duration is equal or higher than: + +`initializer expected duration - (sync pad bit 1 duration - sync pad bit 1 minimum acceptable duration)` + +To ensure 100% reliability separating frames the sync pad minimum acceptable duration must be higher than 1 standard bit duration. Selecting a correct `sync pad bit 1 / standard bit` ratio, called pad-bit ratio, frame initializer is 100% reliable, false positives cannot occur if not because of externally induced interference. Sync pad bit 1 duration must not be an exact multiple of a standard bit, for this reason pad-bit ratio of 2.0, 3.0 or 4.0 must be avoided because consecutive bits can be interpreted as a frame initializer. + +#### Synchronous response +A frame transmission can be optionally followed by a synchronous response by its recipient. This feature is available for both master-slave and multi-master configuration. +```cpp +Transmission Response + ______ ______ ______ ______ _____ +| INIT || BYTE || BYTE || BYTE | CRC COMPUTATION | ACK | +|------||------||------||------|-----------------|-----| +| || || || | LATENCY | 6 | +|______||______||______||______| |_____| +``` + +Between frame transmission and a synchronous response there is a variable timeframe influenced by latency and CRC computation time. In order to avoid other devices to consider the medium free and disrupt an ongoing exchange, sender cyclically transmits a shorter than one bit logic 1 (which exact length depends on practical requirements) and consequently attempts to receive a response. On the other side receiver can synchronize its response transmission after the last incoming high bit, detect if acknowledgement was lost by transmitter and try again if necessary. +```cpp +Transmission Response + ______ ______ ______ ______ _ _ _ _ _ _____ +| INIT || BYTE || BYTE || BYTE | | | | | | | | | | | ACK | +|------||------||------||------| | | | | | | | | | |-----| +| || || || | | | | | | | | | | | 6 | +|______||______||______||______|_| |_| |_| |_| |_| |_____| + +``` + +The maximum time dedicated to potential acknowledgement reception and consequent medium jittering is estimated adding the maximum frame length CRC computation time to the expected latency. Thanks to the presence of the jittering wave, many differently configured devices can coexist on the same medium with no risk of collision. + +#### Communication modes +The proposed communication modes are the result of years of testing and optimization and have been selected to be easily supported by limited microcontrollers. + +| MODE | Bit timing | Sync bit timing | Pad-bit ratio | Speed | +| ---- | ---------- | --------------- | ------------- | ------------------- | +| 1 | 40 | 112 | 2.8 | 2.118kB/s - 16944Bd | +| 2 | 32 | 84 | 2.625 | 2.688kB/s - 21504Bd | + +Binary timing durations are expressed in microseconds. diff --git a/hal/transport/PJON/driver/strategies/SoftwareBitBang/specification/obsolete/PJDL-specification-v3.0.md b/hal/transport/PJON/driver/strategies/SoftwareBitBang/specification/obsolete/PJDL-specification-v3.0.md new file mode 100644 index 000000000..5df65c340 --- /dev/null +++ b/hal/transport/PJON/driver/strategies/SoftwareBitBang/specification/obsolete/PJDL-specification-v3.0.md @@ -0,0 +1,104 @@ + +## PJDL v3.0 +``` +Invented by Giovanni Blu Mitolo +Originally published: 10/04/2010, latest revision: 05/04/2019 +Related implementation: /src/strategies/SoftwareBitBang/ +Compliant versions: PJON 9.0 and following +Released into the public domain +``` +PJDL (Padded Jittering Data Link) is an asynchronous serial data link for low-data-rate applications that supports one or many to many communication over a common conductive medium. PJDL can be easily implemented on limited microcontrollers with low clock accuracy and can operate directly using a single input-output pin. + +### Physical layer +The medium's maximum length is limited by the wiring resistance, by the voltage level used and by externally induced interference. It has been tested with up to 100 meters long insulated wires and results demonstrate the same performance achieved with shorter lengths. The maximum range is still unknown. +```cpp +PJDL SINGLE WIRE BUS ______ + ______ ______ ______ ______ | | +| | | | | | | | |DEVICE| +|DEVICE| |DEVICE| |DEVICE| |DEVICE| |______| +|______| |______| |______| |______| | +___|__________|________|___________|_______/\/\/\__| IO PIN + ___|__ __|___ ___|__ ___|__ | 110-180 Ω +| | | | | | | | | +|DEVICE| |DEVICE| |DEVICE| |DEVICE| |__/\/\/\__ GND +|______| |______| |______| |______| 1-5 MΩ +``` +It is suggested to add 1-5 MΩ pull-down resistor as shown in the graph above to reduce externally induced interference. Pins can be optionally protected against overload adding a current limiting resistor to each connected pin. The resistor value can be obtained solving the following equation `R = (operating voltage / pin max current drain)`, for example to obtain the current limiting resistor value for an Arduino Uno simply substitute its characteristics: `R = (5v / 0.030A) = 166.66Ω`. + +### Communication modes +The proposed communication modes are the result of years of testing and optimization and have been selected to be easily supported by limited microcontrollers. + +| MODE | Data bit duration | Padding bit duration | Pad-data ratio | Bandwidth | +| ---- | ----------------- | -------------------- | --------------- | ------------------ | +| 1 | 44 | 116 | 2.636 | 1.95kB/s - 15625Bd | +| 2 | 40 | 92 | 2.300 | 2.21kB/s - 17696Bd | +| 3 | 28 | 88 | 3.142 | 2.94kB/s - 23529Bd | +| 4 | 26 | 60 | 2.307 | 3.40kB/s - 27210Bd | + +Durations are expressed in microseconds. + +### Medium access control +PJDL specifies a variation of the carrier-sense, non-persistent random multiple access method (non-persistent CSMA). Devices can detect an ongoing transmission for this reason collisions can only occur in multi-master mode when 2 or more devices start to transmit at the same time. When a collision occurs it can be detected by the receiver because of synchronization loss. + +### Byte transmission +Byte transmission is composed by 10 bits, the first two are called synchronization pad and are used to obtain sampling synchronization. The synchronization pad is composed by a high padding bit longer than data bits and a low data bit. The following 8 data bits contain information in LSB-first (least significant bit first) order. + +The reception technique is based on 3 steps: +1. Find a high bit which duration is equal to or acceptably shorter than a high padding bit +2. Synchronize to its falling edge +3. Ensure it is followed by a low data bit +4. If so reception starts, if not, interference, synchronization loss or simply absence of communication is detected + +```cpp + ___________ ___________________________ +| SYNC-PAD | DATA | +|_______ |___ ___ _____ | +| | | | | | | | | | +| | 1 | 0 | 1 | 0 0 | 1 | 0 | 1 1 | 0 | +|__|____|___|___|_____|___|___|_____|___| + | +Minimum acceptable padding bit duration +``` +The synchronization pad adds overhead although it includes synchronization along with the data and eliminates the need of a dedicated clock line. The minimum acceptable padding bit duration is the time in which a receiver initiating polling can correctly receive a byte. If the duration of the padding bit is shorter than the minimum acceptable duration the received signal is discarded. The minimum acceptable duration of the padding bit must be shorter than a padding bit duration; a large minimum acceptable duration reduces the chances of false positive's occurrences, a small minimum acceptable duration instead mitigates timing inaccuracies. The presence of the synchronization pad between each byte also ensures that a frame composed of a series of bytes with decimal value 0 can be transmitted safely without risk of collision. + +### Frame transmission +Before a frame transmission the communication medium's state is analysed, if high communication is detected and collision is avoided, if low for a duration of one byte plus a small random time, frame transmission starts with an initializer composed by 3 consecutive synchronization pads followed by data bytes. The synchronization pad is used for both byte and frame initialization to reduce the implementation complexity. +```cpp + ________ _________________ __________________________________ +|ANALYSIS| FRAME INIT | DATA 1-65535 bytes | +|________|_____ _____ _____|________________ _________________| +| |Sync |Sync |Sync |Sync | Byte |Sync | Byte | +| |___ |___ |___ |___ | __ |___ | _ _| +| | | | | | | | | | | | | | | | | | | +|00000000| 1 |0| 1 |0| 1 |0| 1 |0|0000|11|00| 1 |0|00000|1|0|1| +|________|___|_|___|_|___|_|___|_|____|__|__|___|_|_____|_|_|_| +``` +When a frame is received a low performance microcontroller with an inaccurate clock can correctly synchronize with transmitter during the frame initializer and consequently each byte is received. On receiver's side a frame reception starts if 3 synchronization pads are detected and if their duration is equal or higher than: + +`frame initializer expected duration - (sync bit 1 duration - sync bit 1 minimum acceptable duration)` + +To ensure 100% reliability the padding bit must be longer than data bits. Frame initialization is 100% reliable, false positives can only occur because of externally induced interference. The padding bit duration must not be an exact multiple of the duration of one data bit, for this reason a `padding bit / data bit` ratio or pad-data ratio of 1, 2, 3 or 4 must be avoided because one or multiple consecutive data bits may be erroneously interpreted as a padding bit. + +### Synchronous response +A frame transmission can be optionally followed by a synchronous response sent by its recipient. +```cpp +Transmission Response + ______ ______ ______ ______ _____ +| INIT || BYTE || BYTE || BYTE | CRC COMPUTATION | ACK | +|------||------||------||------|-----------------|-----| +| || || || | LATENCY | 6 | +|______||______||______||______| |_____| +``` + +Between frame transmission and a synchronous response there is a variable time which duration is influenced by latency. In order to avoid other devices to detect the medium free for use and disrupt an ongoing exchange, the sender cyclically transmits a short high bit (1/4 data bit duration) and consequently attempts to receive a response. On the other side the receiver can synchronize its response transmission after the end of the last short high bit. If the acknowledgement is not transmitted or not received the transmitter continues to keep busy the medium up to the maximum acceptable time between transmission and response. +```cpp +Transmission Response + ______ ______ ______ ______ _ _ _ _ _ _____ +| INIT || BYTE || BYTE || BYTE | | | | | | | | | | | ACK | +|------||------||------||------| | | | | | | | | | |-----| +| || || || | | | | | | | | | | | 6 | +|______||______||______||______|_| |_| |_| |_| |_| |_____| + +``` + +The maximum time dedicated to potential acknowledgement reception for a given application can be determined practically transmitting the longest supported frame with the farthest physical distance between the two devices. The highest interval between packet transmission and acknowledgement measured plus a small margin is the correct timeout that should exclude acknowledgement losses. Consider that the longer this timeout is, the more bandwidth is wasted if the transmission is not successful. diff --git a/hal/transport/PJON/driver/strategies/SoftwareBitBang/specification/obsolete/PJDL-specification-v4.0.md b/hal/transport/PJON/driver/strategies/SoftwareBitBang/specification/obsolete/PJDL-specification-v4.0.md new file mode 100644 index 000000000..9fc04ef6e --- /dev/null +++ b/hal/transport/PJON/driver/strategies/SoftwareBitBang/specification/obsolete/PJDL-specification-v4.0.md @@ -0,0 +1,110 @@ + +## PJDL v4.0 +``` +Invented by Giovanni Blu Mitolo +Originally published: 10/04/2010 +Latest revision: 07/07/2019 +Related implementation: /src/strategies/SoftwareBitBang/ +Compliant versions: PJON v12.0 and following +Released into the public domain + +10/04/2010 0.1 - First experimental release +12/02/2017 1.0 - Frame initializer, response made safe +31/03/2017 1.1 - Physical layer info +24/09/2017 2.0 - Modes 1, 2, 3 +29/12/2018 3.0 - Medium access control info, mode 4 +03/07/2019 4.0 - Response initializer +``` +PJDL (Padded Jittering Data Link) is an asynchronous serial data link for low-data-rate applications that supports both master-slave and multi-master communication over a common conductive medium. PJDL can be easily implemented on limited microcontrollers with low clock accuracy and can operate directly using a single input-output pin. + +### Physical layer +The medium's maximum length is limited by the wiring resistance, by the voltage level used and by externally induced interference. It has been tested with up to 100 meters long insulated wires and results demonstrate the same performance achieved with shorter lengths. The maximum range is still unknown. +```cpp +PJDL SINGLE WIRE BUS ______ + ______ ______ ______ ______ | | +| | | | | | | | |DEVICE| +|DEVICE| |DEVICE| |DEVICE| |DEVICE| |______| +|______| |______| |______| |______| | +___|__________|________|___________|______/\/\/\__| IO PIN + ___|__ __|___ ___|__ ___|__ | 110-180 Ω +| | | | | | | | | +|DEVICE| |DEVICE| |DEVICE| |DEVICE| |__/\/\/\__ GND +|______| |______| |______| |______| 1-5 MΩ +``` +It is suggested to add 1-5 MΩ pull-down resistor as shown in the graph above to reduce externally induced interference. Pins can be optionally protected against overload adding a current limiting resistor to each connected pin. The resistor value can be obtained solving the following equation `R = (operating voltage / pin max current drain)`, for example to obtain the current limiting resistor value for an Arduino Uno simply substitute its characteristics: `R = (5v / 0.030A) = 166.66Ω`. + +### Communication modes +The proposed communication modes are the result of years of testing and optimization and have been selected to be easily supported by limited microcontrollers. + +| MODE | Data bit duration | Padding bit duration | Pad-data ratio | Bandwidth | +| ---- | ----------------- | -------------------- | --------------- | ------------------ | +| 1 | 44 | 116 | 2.636 | 1.95kB/s - 15625Bd | +| 2 | 40 | 92 | 2.300 | 2.21kB/s - 17696Bd | +| 3 | 28 | 88 | 3.142 | 2.94kB/s - 23529Bd | +| 4 | 26 | 60 | 2.307 | 3.40kB/s - 27210Bd | + +Durations are expressed in microseconds. + +### Medium access control +PJDL specifies a variation of the carrier-sense, non-persistent random multiple access method (non-persistent CSMA). Devices can detect an ongoing transmission for this reason collisions can only occur in multi-master mode when 2 or more devices start to transmit at the same time. When a collision occurs it can be detected by the receiver because of synchronization loss or by the transmitter if an active collision avoidance procedure is implemented. + +### Byte transmission +Byte transmission is composed by 10 bits, the first two are called synchronization pad and are used to obtain sampling synchronization. The synchronization pad is composed by a high padding bit longer than data bits and a low data bit. The following 8 data bits contain information in LSB-first (least significant bit first) order. + +The reception technique is based on 3 steps: +1. Find a high bit which duration is equal to or acceptably shorter than a high padding bit +2. Synchronize to its falling edge +3. Ensure it is followed by a low data bit + +If so reception starts, if not, interference, synchronization loss or simply absence of communication is detected. + +```cpp + ___________ ___________________________ +| SYNC-PAD | DATA | +|_______ |___ ___ _____ | +| | | | | | | | | | +| | 1 | 0 | 1 | 0 0 | 1 | 0 | 1 1 | 0 | +|__|____|___|___|_____|___|___|_____|___| + | +Minimum acceptable padding bit duration +``` +The synchronization pad adds overhead although it includes synchronization along with the data and eliminates the need of a dedicated clock line. The minimum acceptable padding bit duration is the time in which a receiver initiating polling can correctly receive a byte. If the duration of the padding bit is shorter than the minimum acceptable duration the received signal is discarded. The minimum acceptable duration of the padding bit must be shorter than a padding bit duration; a large minimum acceptable duration reduces the chances of false positive's occurrences, a small minimum acceptable duration instead mitigates timing inaccuracies. The presence of the synchronization pad between each byte also ensures that a frame composed of a series of bytes with decimal value 0 can be transmitted safely without risk of collision. + +### Frame transmission +Before a frame transmission the communication medium's state is analysed, if high communication is detected and collision is avoided, if low for a duration of one byte plus a small random time, frame transmission starts with an initializer composed by 3 consecutive synchronization pads followed by data bytes. The synchronization pad is used for both byte and frame initialization to reduce the implementation complexity. +```cpp + ________ _________________ __________________________________ +|ANALYSIS| FRAME INIT | DATA 1-65535 bytes | +|________|_____ _____ _____|________________ _________________| +| |Sync |Sync |Sync |Sync | Byte |Sync | Byte | +| |___ |___ |___ |___ | __ |___ | _ _| +| | | | | | | | | | | | | | | | | | | +|00000000| 1 |0| 1 |0| 1 |0| 1 |0|0000|11|00| 1 |0|00000|1|0|1| +|________|___|_|___|_|___|_|___|_|____|__|__|___|_|_____|_|_|_| +``` +When a frame is received a low performance microcontroller with an inaccurate clock can correctly synchronize with transmitter during the frame initializer and consequently each byte is received. On receiver's side a frame reception starts if 3 synchronization pads are detected and if their duration is equal or higher than: + +`frame initializer duration - (padding bit duration - padding bit minimum acceptable duration)` + +To ensure 100% reliability the padding bit must be longer than data bits. Frame initialization is 100% reliable, false positives can only occur because of externally induced interference. The padding bit duration must not be an exact multiple of the duration of one data bit, for this reason a `padding bit / data bit` ratio or pad-data ratio of 1, 2, 3 or 4 must be avoided because one or multiple consecutive data bits may be erroneously interpreted as a padding bit. + +### Synchronous response +A frame transmission can be optionally followed by a synchronous response sent by its recipient. Between frame transmission and a synchronous response there is a variable time which duration is influenced by latency. +```cpp +Transmission end Response + ______ ______ ______ _____ +| BYTE || BYTE || BYTE | CRC COMPUTATION / LATENCY | ACK | +|------||------||------|---------------------------|-----| +| || || | | 6 | +|______||______||______| |_____| +``` +In order to avoid other devices to detect the medium free for use and disrupt an ongoing exchange, the sender cyclically transmits a short high bit (1/4 data bit duration) and consequently attempts to receive a response. The receiver must synchronize its response to the falling edge of the last short high bit, and, in order to avoid false positives in case of collision, must transmit its response prepended with an additional synchronization pulse. If the response is not transmitted or not received the transmitter continues to keep busy the medium up to the maximum acceptable time between transmission and response. +```cpp +Transmission end Response + ______ ______ ______ _ _ _ _ _ _ ____ _____ +| BYTE || BYTE || BYTE | | | | | | | | | | | | |SYNC| ACK | +|------||------||------| | | | | | | | | | | | |----|-----| +| || || | | | | | | | | | | | | | | 6 | +|______||______||______|_| |_| |_| |_| |_| |_| |____|_____| +``` +The maximum time dedicated to potential response reception for a given application can be determined practically transmitting the longest supported frame with the farthest physical distance between the two devices. The highest interval between packet transmission and response measured plus a small margin is the correct timeout that should exclude response losses. Consider that the longer this timeout is, the more bandwidth is wasted if the transmission is not successful. diff --git a/hal/transport/PJON/driver/strategies/SoftwareBitBang/specification/obsolete/PJDL-specification-v4.1.md b/hal/transport/PJON/driver/strategies/SoftwareBitBang/specification/obsolete/PJDL-specification-v4.1.md new file mode 100644 index 000000000..bf96684e9 --- /dev/null +++ b/hal/transport/PJON/driver/strategies/SoftwareBitBang/specification/obsolete/PJDL-specification-v4.1.md @@ -0,0 +1,126 @@ + +### Specifications index + +#### Network layer +- [PJON (Padded Jittering Operative Network) v3.2](/specification/PJON-protocol-specification-v3.2.md) +- [Acknowledge specification v1.0](/specification/PJON-protocol-acknowledge-specification-v1.0.md) +- [Network services list](/specification/PJON-network-services-list.md) +#### Data link layer +- **[PJDL (Padded Jittering Data Link) v4.1](/src/strategies/SoftwareBitBang/specification/PJDL-specification-v4.1.md)** +- [PJDLR (Padded Jittering Data Link over Radio) v3.0](/src/strategies/OverSampling/specification/PJDLR-specification-v3.0.md) +- [PJDLS (Padded Jittering Data Link byte Stuffed) v2.0](/src/strategies/AnalogSampling/specification/PJDLS-specification-v2.0.md) +- [TSDL (Tardy Serial Data Link) v2.1](/src/strategies/ThroughSerial/specification/TSDL-specification-v2.1.md) +- [SFSP (Secure Frame Separation Protocol) v1.0](/specification/SFSP-frame-separation-specification-v1.0.md) + +--- + +## PJDL v4.1 +``` +Invented by Giovanni Blu Mitolo +Originally published: 10/04/2010 +Latest revision: 10/03/2020 +Related implementation: /src/strategies/SoftwareBitBang/ +Compliant versions: PJON v12.0 and following +Released into the public domain + +10/04/2010 0.1 - First experimental release +12/02/2017 1.0 - Frame initializer, response made safe +31/03/2017 1.1 - Physical layer info +24/09/2017 2.0 - Modes 1, 2, 3 +29/12/2018 3.0 - Medium access control info, mode 4 +03/07/2019 4.0 - Response initializer +10/03/2020 4.1 - Maximum range experimentally determined +``` +PJDL (Padded Jittering Data Link) is an asynchronous serial data link for low-data-rate applications that supports both master-slave and multi-master communication over a common conductive medium. PJDL can be easily implemented on limited microcontrollers with low clock accuracy and can operate directly using a single input-output pin. + +### Physical layer +The medium's maximum length is limited by the wiring resistance, by the voltage level used and by externally induced interference. The maximum length of the bus can reach between 800 and 2000 meters depending on the mode used. +```cpp +PJDL SINGLE WIRE BUS ______ + ______ ______ ______ ______ | | +| | | | | | | | |DEVICE| +|DEVICE| |DEVICE| |DEVICE| |DEVICE| |______| +|______| |______| |______| |______| | +___|__________|________|___________|______/\/\/\__| IO PIN + ___|__ __|___ ___|__ ___|__ | 110-180 Ω +| | | | | | | | | +|DEVICE| |DEVICE| |DEVICE| |DEVICE| |__/\/\/\__ GND +|______| |______| |______| |______| 8 kΩ - 5 MΩ +``` +It is suggested to add 8kΩ-5MΩ pull-down resistor as shown in the graph above to reduce externally induced interference. The longer is the length of the cable and the higher is the amount of induced interference, the lower should be the resistance of the pull-down resistor. Pins can be optionally protected against overload adding a current limiting resistor to each connected pin. The resistor value can be obtained solving the following equation `R = (operating voltage / pin max current drain)`, for example to obtain the current limiting resistor value for an Arduino Uno simply substitute its characteristics: `R = (5v / 0.030A) = 166.66Ω`. + +### Communication modes +The proposed communication modes are the result of years of testing and optimization and have been selected to be easily supported by limited microcontrollers. + +| MODE | Data bit duration | Padding bit duration | Pad-data ratio | Bandwidth | Range | +| ---- | ----------------- | -------------------- | --------------- | ------------------ | ----- | +| 1 | 44 | 116 | 2.636 | 1.95kB/s - 15625Bd | 2000m | +| 2 | 40 | 92 | 2.300 | 2.21kB/s - 17696Bd | 1600m | +| 3 | 28 | 88 | 3.142 | 2.94kB/s - 23529Bd | 1200m | +| 4 | 26 | 60 | 2.307 | 3.40kB/s - 27210Bd | 800m | + +Durations are expressed in microseconds. + +### Medium access control +PJDL specifies a variation of the carrier-sense, non-persistent random multiple access method (non-persistent CSMA). Devices can detect an ongoing transmission for this reason collisions can only occur in multi-master mode when 2 or more devices start to transmit at the same time. When a collision occurs it can be detected by the receiver because of synchronization loss or by the transmitter if an active collision avoidance procedure is implemented. + +### Byte transmission +Byte transmission is composed by 10 bits, the first two are called synchronization pad and are used to obtain sampling synchronization. The synchronization pad is composed by a high padding bit longer than data bits and a low data bit. The following 8 data bits contain information in LSB-first (least significant bit first) order. + +The reception technique is based on 3 steps: +1. Find a high bit which duration is equal to or acceptably shorter than a high padding bit +2. Synchronize to its falling edge +3. Ensure it is followed by a low data bit + +If so reception starts, if not, interference, synchronization loss or simply absence of communication is detected. + +```cpp + ___________ ___________________________ +| SYNC-PAD | DATA | +|_______ |___ ___ _____ | +| | | | | | | | | | +| | 1 | 0 | 1 | 0 0 | 1 | 0 | 1 1 | 0 | +|__|____|___|___|_____|___|___|_____|___| + | +Minimum acceptable padding bit duration +``` +The synchronization pad adds overhead although it includes synchronization along with the data and eliminates the need of a dedicated clock line. The minimum acceptable padding bit duration is the time in which a receiver initiating polling can correctly receive a byte. If the duration of the padding bit is shorter than the minimum acceptable duration the received signal is discarded. The minimum acceptable duration of the padding bit must be shorter than a padding bit duration; a large minimum acceptable duration reduces the chances of false positive's occurrences, a small minimum acceptable duration instead mitigates timing inaccuracies. The presence of the synchronization pad between each byte also ensures that a frame composed of a series of bytes with decimal value 0 can be transmitted safely without risk of collision. + +### Frame transmission +Before a frame transmission the communication medium's state is analysed, if high communication is detected and collision is avoided, if low for a duration of one byte plus a small random time, frame transmission starts with an initializer composed by 3 consecutive synchronization pads followed by data bytes. The synchronization pad is used for both byte and frame initialization to reduce the implementation complexity. +```cpp + ________ _________________ __________________________________ +|ANALYSIS| FRAME INIT | DATA 1-65535 bytes | +|________|_____ _____ _____|________________ _________________| +| |Sync |Sync |Sync |Sync | Byte |Sync | Byte | +| |___ |___ |___ |___ | __ |___ | _ _| +| | | | | | | | | | | | | | | | | | | +|00000000| 1 |0| 1 |0| 1 |0| 1 |0|0000|11|00| 1 |0|00000|1|0|1| +|________|___|_|___|_|___|_|___|_|____|__|__|___|_|_____|_|_|_| +``` +When a frame is received a low performance microcontroller with an inaccurate clock can correctly synchronize with transmitter during the frame initializer and consequently each byte is received. On receiver's side a frame reception starts if 3 synchronization pads are detected and if their duration is equal or higher than: + +`frame initializer duration - (padding bit duration - padding bit minimum acceptable duration)` + +To ensure 100% reliability the padding bit must be longer than data bits. Frame initialization is 100% reliable, false positives can only occur because of externally induced interference. The padding bit duration must not be an exact multiple of the duration of one data bit, for this reason a `padding bit / data bit` ratio or pad-data ratio of 1, 2, 3 or 4 must be avoided because one or multiple consecutive data bits may be erroneously interpreted as a padding bit. + +### Synchronous response +A frame transmission can be optionally followed by a synchronous response sent by its recipient. Between frame transmission and a synchronous response there is a variable time which duration is influenced by latency. +```cpp +Transmission end Response + ______ ______ ______ _____ +| BYTE || BYTE || BYTE | CRC COMPUTATION / LATENCY | ACK | +|------||------||------|---------------------------|-----| +| || || | | 6 | +|______||______||______| |_____| +``` +In order to avoid other devices to detect the medium free for use and disrupt an ongoing exchange, the sender cyclically transmits a short high bit (1/4 data bit duration) and consequently attempts to receive a response. The receiver must synchronize its response to the falling edge of the last short high bit, and, in order to avoid false positives in case of collision, must transmit its response prepended with an additional synchronization pulse. If the response is not transmitted or not received the transmitter continues to keep busy the medium up to the maximum acceptable time between transmission and response. +```cpp +Transmission end Response + ______ ______ ______ _ _ _ _ _ _ ____ _____ +| BYTE || BYTE || BYTE | | | | | | | | | | | | |SYNC| ACK | +|------||------||------| | | | | | | | | | | | |----|-----| +| || || | | | | | | | | | | | | | | 6 | +|______||______||______|_| |_| |_| |_| |_| |_| |____|_____| +``` +The maximum time dedicated to potential response reception for a given application can be determined practically transmitting the longest supported frame with the farthest physical distance between the two devices. The highest interval between packet transmission and response measured plus a small margin is the correct timeout that should exclude response losses. Consider that the longer this timeout is, the more bandwidth is wasted if the transmission is not successful. diff --git a/hal/transport/PJON/driver/strategies/SoftwareBitBang/specification/obsolete/padded-jittering-protocol-specification-v0.1.md b/hal/transport/PJON/driver/strategies/SoftwareBitBang/specification/obsolete/padded-jittering-protocol-specification-v0.1.md new file mode 100644 index 000000000..0835c5331 --- /dev/null +++ b/hal/transport/PJON/driver/strategies/SoftwareBitBang/specification/obsolete/padded-jittering-protocol-specification-v0.1.md @@ -0,0 +1,44 @@ + +```cpp +/* +Milan, Italy - 10/04/2010 +The Padded jittering data link layer specification is an invention and intellectual property +of Giovanni Blu Mitolo - Copyright 2010-2020 All rights reserved + +Related work: /src/strategies/SoftwareBitBang/ +Compliant implementation versions: PJON 1.0-5.0 +*/ +``` +### Padded jittering data link layer +The first experimental specification of the Padded jittering data link layer has been drafted to propose a new way to transmit data with cheap and low performance microcontrollers without the necessity of hardware interrupts for its working procedure. Thanks to the imposed requirement of non-concurrent and single-task execution, the Padded jittering data link has a strong resilience also if the communication medium is affected by interference, high capacitance and resistance. Extended tests proved its effectiveness on different media like electricity, radio frequency and light. + +### Basic concepts +* Use a pattern of predefined initial padding bits to identify a potential byte transmission +* Use the falling edge from 1 to 0, present in padding bits, to achieve byte level synchronization +* Detect interference or absence of communication at byte level +* Propose a collision free synchronous acknowledgement pattern + +#### Byte transmission +Every byte is prepended with 2 synchronization padding bits and transmission occurs LSB-first. The first is a longer than standard logic 1 followed by a standard logic 0. The reception tecnique is based on finding a logic 1 as long as the first padding bit within a certain threshold, synchronizing to its falling edge and checking if it is followed by a logic 0. If this pattern is detected, reception starts, if not, interference, synchronization loss or simply absence of communication is detected at byte level. +```cpp + __________ ___________________________ +| SyncPad | Byte | +|______ |___ ___ _____ | +| | | | | | | | | | +| | 1 | 0 | 1 | 0 0 | 1 | 0 | 1 1 | 0 | +|_ |___|___|___|_____|___|___|_____|___| + | + ACCEPTANCE +``` +Padding bits are adding a certain overhead to information but are reducing the need of precise time tuning because synchronization is renewed every byte. All the first padding bit duration minus `ACCEPTANCE` is the synchronization window the receiver has for every incoming byte. If the length of the first padding bit is less than `ACCEPTANCE` the received signal is considered interference. + +#### String transmission +```cpp + ________________ _________________ ________________ ________________ __________________ +|Sync | Byte |Sync | Byte |Sync | Byte |Sync | Byte |Sync | Byte | +|___ | __ |___ | _ _|___ | _ |___ | _ |___ | _ _ | +| | | | | | | | | | | | | | | | | | | | | | | | | | | | | +| 1 |0|0000|11|00| 1 |0|00000|1|0|1| 1 |0|00000|1|00| 1 |0|0|1|000000| 1 |0|0|1|00|1|000| +|___|_|____|__|__|___|_|_____|_|_|_|___|_|_____|_|__|___|_|_|_|______|___|_|_|_|__|_|___| +``` +In a scenario where a stream of byte is coming, following this approach a low performance or clock inaccurate microcontroller can be correctly synchronized back with the transmitter every byte and easily detect an interference or the end of transmission. diff --git a/hal/transport/PJON/driver/strategies/ThroughLoRa/README.md b/hal/transport/PJON/driver/strategies/ThroughLoRa/README.md new file mode 100644 index 000000000..0db79c349 --- /dev/null +++ b/hal/transport/PJON/driver/strategies/ThroughLoRa/README.md @@ -0,0 +1,221 @@ +## ThroughLoRa + +| Medium | Pins used | Inclusion | +|--------|-----------|--------------------| +| LoRa radio | 6 | `#include `| + +With `ThroughLora` strategy, PJON can run through a software or hardware SPI in order to communicate with supported LoRa modules. See [Supported Shields/Modules](https://github.com/gioblu/PJON/tree/master/src/strategies/ThroughLoRa#supported-shieldsmodules). + +This strategy is a wrapper around [Arduino LoRa library](https://github.com/sandeepmistry/arduino-LoRa) created by [Sandeep Mistry](https://github.com/sandeepmistry) so all the credit to the specific LoRa implementation goes to him. + +### Compatibility +- ATmega88/168/328 16MHz (Diecimila, Duemilanove, Uno, Nano, Mini, Lillypad) +- ATmega2560 16MHz (Arduino Mega) +- ATmega16u4/32u4 16MHz (Arduino Leonardo) +- STM32F103 ([Blue Pill](http://wiki.stm32duino.com/index.php?title=Blue_Pill)) + +### Getting started +1. Pass the `ThroughLora` type as PJON template parameter to instantiate a PJON object ready to communicate through this Strategy. +2. Configure the shield/module pins according to the correct connection. See [Hardware connection](#hardware-connection). +3. Initialize the module with its specified frequency. + +```cpp + +#include + +PJONThroughLora bus; //PJON Lora bus instance + +bus.strategy.setPins(10,9,2); //CS pin, Reset pin, Interrupt pin +bus.strategy.setFrequency(868100000UL); //initialize 868 MHZ module +``` + +### Supported Shields/Modules +| Manufacturer | Hardware | +| ------------------------------------- | ------------------------------------------------------------ | +| [Dragino](http://www.dragino.com/) | [Dragino Lora Shield](http://www.dragino.com/products/module/item/102-lora-shield.html) | +| [HopeRF](http://www.hoperf.com/) | [RFM95W](http://www.hoperf.com/rf_transceiver/lora/RFM95W.html) / [RFM96W](http://www.hoperf.com/rf_transceiver/lora/RFM96W.html) / [RFM98W](http://www.hoperf.com/rf_transceiver/lora/RFM98W.html) | +| [Modtronix](http://modtronix.com/) | [inAir4](http://modtronix.com/inair4.html) / [inAir9](http://modtronix.com/inair9.html) / [inAir9B](http://modtronix.com/inair9b.html) | +| [Adafruit](https://www.adafruit.com/) | [Adafruit Feather 32u4 LoRa Radio (RFM9x)](https://learn.adafruit.com/adafruit-feather-32u4-radio-with-lora-radio-module/overview) | + +### Hardware connection +| General Wiring | Arduino | +| -------------- | ------- | +| VCC | 3.3V | +| GND | GND | +| SCK | SCK | +| MISO | MISO | +| MOSI | MOSI | +| NSS | 10 | +| NRESET | 9 | +| DIO0 | 2 | + +- `NSS`, `NRESET`, and `DIO0` pins can be changed by using `PJON.strategy.setPins(ss, reset, dio0)`. +- `DIO0` pin is optional, it is only needed for receive callback mode. If `DIO0` pin is used, it **must** be interrupt capable via [`attachInterrupt(...)`](https://www.arduino.cc/en/Reference/AttachInterrupt). + +### Usage Example +Here are listed basic examples of a transmitter and receiver code. After you include the necessary code to initialize the Lora module you can use the normal PJON functions to handle data communication. + +Keep in mind that to use the LoRa startegy you must download the [Arduino LoRa library](https://github.com/sandeepmistry/arduino-LoRa). + +More examples can be found in https://github.com/gioblu/PJON/tree/master/examples/ARDUINO/Local/ThroughLoRa + +### Transmitter +```cpp + +#include + +PJONThroughLora bus(45); + +void setup() { + // Obligatory to initialize Radio with correct frequency + bus.strategy.setFrequency(868100000UL); + bus.begin(); + // Send B to device 44 every second + bus.send_repeatedly(44, "B", 1, 1000000); +}; + +void loop() { + bus.update(); +}; +``` + +### Receiver + +```cpp +#include + +// bus(selected device id) +PJONThroughLora bus(44); + +void setup() { + pinMode(13, OUTPUT); + digitalWrite(13, LOW); // Initialize LED 13 to be off + Serial.begin(9600); + + // Obligatory to initialize Radio with correct frequency + bus.strategy.setFrequency(868100000UL); + // Optional + bus.strategy.setSignalBandwidth(250E3); + bus.begin(); + bus.set_receiver(receiver_function); +}; + +void receiver_function(uint8_t *payload, uint16_t length, const PJON_Packet_Info &packet_info) { + /* Make use of the payload before sending something, the buffer where payload points to is + overwritten when a new message is dispatched */ + if (payload[0] == 'B') { + digitalWrite(13, HIGH); + delay(30); + digitalWrite(13, LOW); + } +}; + +void loop() { + bus.receive(1000); +}; +``` + +This API is a specific wrapper around the [Arduino-LoRa library](https://github.com/sandeepmistry/arduino-LoRa) + +All the LoRa API is accessible through the exposed strategy in a PJON instance. Ex: `bus.strategy` + +### Setup + +Initialize the library with the specified frequency. + +```cpp +bus.strategy.setFrequency(frequency); +``` + +- `frequency` - frequency in Hz (`433E6`, `866E6`, `915E6`) + +Returns `1` on success, `0` on failure. + +### Set pins + +Override the default `NSS`, `NRESET`, and `DIO0` pins used by the library. **Must** be called before `LoRa.begin()`. + +```cpp +bus.strategy.setPins(ss, reset, dio0); +``` + +- `ss` - new slave select pin to use, defaults to `10` +- `reset` - new reset pin to use, defaults to `9` +- `dio0` - new DIO0 pin to use, defaults to `2`, **must** be interrupt capable via [attachInterrupt(...)](https://www.arduino.cc/en/Reference/AttachInterrupt). + +This call is optional and only needs to be used if you need to change the default pins used. + +### Configuration + +```cpp +int rssi = bus.strategy.packetRssi(); +``` +Returns the RSSI of the received packet. + +```cpp +float snr = bus.strategy.packetSnr(); +``` +Returns the estimated SNR of the received packet in dB. + +```cpp +bus.strategy.setSignalBandwidth(signalBandwidth); +``` +Change the signal bandwidth of the radio, `signalBandwidth` represents the signal bandwidth in Hz, defaults to `125E3`. Supported values are `7.8E3`, `10.4E3`, `15.6E3`, `20.8E3`, `31.25E3`, `41.7E3`, `62.5E3`, `125E3`, and `250E3`. + +```cpp +bus.strategy.setSpreadingFactor(spreadingFactor); +``` +Change the spreading factor of the radio. `spreadingFactor` represents the spreading factor, defaults to `7`. Supported values are between `6` and `12`. If a spreading factor of `6` is set, implicit header mode must be used to transmit and receive packets. + +```cpp +bus.strategy.setCodingRate4(codingRateDenominator); +``` +Change the coding rate of the radio. `codingRateDenominator` represents denominator of the coding rate, defaults to `5`. Supported values are between `5` and `8`, these correspond to coding rates of `4/5` and `4/8`. The coding rate numerator is fixed at `4`. + +```cpp +bus.strategy.setPreambleLength(preambleLength); +``` +Change the preamble length of the radio. `preambleLength` represents preamble length in symbols, defaults to `8`. Supported values are between `6` and `65535`. + +```cpp +bus.strategy.setSyncWord(syncWord); +``` +Change the sync word of the radio. `syncWord` represents byte value to use as the sync word, defaults to `0x34` + +```cpp +bus.strategy.setCRC(false); //To disable CRC +bus.strategy.setCRC(true); // To enable CRC +``` +Enable or disable CRC usage, by default a CRC is not used. + +```cpp +LoRa.setTxPower(txPower); //Configure the radio TX power + +LoRa.setTxPower(txPower, outputPin); //Configure the radio TX power with extra boost pin +``` +Change the TX power of the radio. + +- `txPower` - TX power in dB, defaults to `17` +- `outputPin` - (optional) PA output pin, supported values are `PA_OUTPUT_RFO_PIN` and `PA_OUTPUT_PA_BOOST_PIN`, defaults to `PA_OUTPUT_PA_BOOST_PIN`. + +Supported values are between `2` and `17` for `PA_OUTPUT_PA_BOOST_PIN`, `0` and `14` for `PA_OUTPUT_RFO_PIN`. + +Most modules have the PA output pin connected to PA BOOST. + +```cpp +bus.strategy.idle(); +``` +Put the radio in idle (standby) mode. + +```cpp +bus.strategy.sleep(); +``` +Put the radio in sleep mode. + +```cpp +byte b = bus.strategy.getRandom(); +``` +Generate a random byte, based on the Wideband RSSI measurement. + +### Safety warning +In all cases, when installing or maintaining a PJON network, extreme care must be taken to avoid any danger. Before any practical test or hardware purchase for a wireless [ThroughLoRa](/src/strategies/ThroughLoRa) radio setup, compliance with government requirements and regulations must be ensured. diff --git a/hal/transport/PJON/driver/strategies/ThroughLoRa/ThroughLora.h b/hal/transport/PJON/driver/strategies/ThroughLoRa/ThroughLora.h new file mode 100644 index 000000000..695523728 --- /dev/null +++ b/hal/transport/PJON/driver/strategies/ThroughLoRa/ThroughLora.h @@ -0,0 +1,260 @@ + +/* ThroughLora data link + Proposed and developed by Matheus Eduardo Garbelini 2017-2018 + ____________________________________________________________________________ + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + To use this strategy, please download the LoRa third party Library from + https://github.com/sandeepmistry/arduino-LoRa/ */ + +#pragma once + +#include +#include "Timing.h" + +// Recommended receive time for this strategy, in microseconds +#ifndef TL_RECEIVE_TIME +#define TL_RECEIVE_TIME 0 +#endif + +class ThroughLora +{ +public: + /* Returns the suggested delay related to the attempts + passed as parameter: */ + + uint32_t back_off(uint8_t attempts) + { + uint32_t result = attempts; + for(uint8_t d = 0; d < TL_BACK_OFF_DEGREE; d++) { + result *= (uint32_t)(attempts); + } + return result; + }; + + int packetRssi() + { + return LoRa.packetRssi(); + }; + + float packetSnr() + { + return LoRa.packetSnr(); + }; + + bool setFrequency(uint32_t frequency) + { + return LoRa.begin(frequency); + }; + + void setSignalBandwidth(uint32_t bandwidth) + { + LoRa.setSignalBandwidth(bandwidth); + }; + + void setPins(uint8_t cs_pin, uint8_t reset_pin, uint8_t dio0_pin) + { + LoRa.setPins(cs_pin, reset_pin, dio0_pin); + }; + + void setSpreadingFactor(uint8_t spreadingFactor) + { + LoRa.setSpreadingFactor(spreadingFactor); + }; + + void setCodingRate4(uint8_t codingRate) + { + LoRa.setCodingRate4(codingRate); + }; + + void setPreambleLength(uint16_t preambleLength) + { + LoRa.setPreambleLength(preambleLength); + }; + + void setSyncWord(uint8_t syncWord) + { + LoRa.setSyncWord(syncWord); + }; + + void setCRC(bool enableCRC) + { + if (enableCRC) { + LoRa.enableCrc(); + } else { + LoRa.disableCrc(); + } + }; + + void setTxPower(uint8_t txPower) + { + LoRa.setTxPower(txPower); + }; + + void setTxPower(uint8_t txPower, uint8_t boostPin) + { + LoRa.setTxPower(txPower, boostPin); + }; + + void idle() + { + LoRa.idle(); + }; + + void sleep() + { + LoRa.sleep(); + }; + + uint8_t getRandom() + { + return LoRa.random(); + }; + + /* Begin method, to be called on initialization: + (returns always true) */ + + bool begin(uint8_t did = 0) + { + PJON_DELAY_MICROSECONDS(PJON_RANDOM(TL_INITIAL_DELAY) + did); + return true; + }; + + /* Check if the channel is free for transmission: */ + + bool can_start() + { + if(LoRa.parsePacket() > 0) { + return false; + } + return true; + }; + + /* Returns the maximum number of attempts for each transmission: */ + + static uint8_t get_max_attempts() + { + return TL_MAX_ATTEMPTS; + }; + + /* Returns the recommended receive time for this strategy: */ + + static uint16_t get_receive_time() + { + return TL_RECEIVE_TIME; + }; + + /* Handle a collision: */ + + void handle_collision() + { + PJON_DELAY_MICROSECONDS(PJON_RANDOM(TL_COLLISION_DELAY)); + }; + + /* The last 5 bytes of the frame are used as a unique identifier within + the response. PJON has CRC8 or CRC32 at the end of the packet, encoding + a CRC (that is a good hashing algorithm) and using 40 bits looks enough + to provide a relatively safe response that should be nearly flawless + (yield few false positives per millennia). */ + + void prepare_response(const uint8_t *buffer, uint16_t position) + { + for(int8_t i = 0; i < TL_RESPONSE_LENGTH; i++) + _response[i] = + buffer[(position - ((TL_RESPONSE_LENGTH - 1) - i)) - 1]; + }; + + /* Receive byte response (not supported) */ + + uint16_t receive_response() + { + uint32_t time = PJON_MICROS(); + while((uint32_t)(PJON_MICROS() - time) < TL_RESPONSE_TIME_OUT) { + uint8_t frame_size = LoRa.parsePacket(); + if(frame_size == TL_RESPONSE_LENGTH) { + for(uint8_t i = 0; i < TL_RESPONSE_LENGTH; i++) + if(LoRa.read() != _response[i]) { + return PJON_FAIL; + } + return PJON_ACK; + } + } + return PJON_FAIL; + }; + + /* Receive a frame: */ + + uint16_t receive_frame(uint8_t *data, uint16_t max_length) + { + uint8_t frameSize = LoRa.parsePacket(); + /* Filter frames if too short to contain a PJON packet + or if too long to be received */ + if((frameSize > 5) && (frameSize <= max_length)) { + while(LoRa.available()) { + *data = LoRa.read(); + data++; + } + prepare_response(data, frameSize); + return frameSize; + } else { + return PJON_FAIL; + } + }; + + /* Send a byte and wait for its transmission end */ + + void send_byte(uint8_t b) + { + LoRa.write(b); + }; + + /* Send byte response to the packet's transmitter (not supported) */ + + void send_response(uint8_t response) + { + if(response == PJON_ACK) { + start_tx(); + for(uint8_t i = 0; i < TL_RESPONSE_LENGTH; i++) { + send_byte(_response[i]); + } + end_tx(); + } + }; + + /* Send a frame: */ + + void send_frame(uint8_t *data, uint8_t length) + { + start_tx(); + for(uint8_t b = 0; b < length; b++) { + send_byte(data[b]); + } + prepare_response(data, length); + end_tx(); + }; + + void start_tx() + { + LoRa.beginPacket(); + }; + + void end_tx() + { + LoRa.endPacket(); + }; + +private: + uint32_t _last_reception_time; + uint8_t _response[TL_RESPONSE_LENGTH]; +}; diff --git a/hal/transport/PJON/driver/strategies/ThroughLoRa/Timing.h b/hal/transport/PJON/driver/strategies/ThroughLoRa/Timing.h new file mode 100644 index 000000000..66e3a1024 --- /dev/null +++ b/hal/transport/PJON/driver/strategies/ThroughLoRa/Timing.h @@ -0,0 +1,56 @@ +/* ThroughLora data link layer + Proposed and developed by Matheus Eduardo Garbelini + ____________________________________________________________________________ + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + +#pragma once + +/* Maximum 1 second random initial delay */ +#ifndef TL_INITIAL_DELAY +#define TL_INITIAL_DELAY 1000 +#endif + +/* Maximum 32 microseconds random delay in case of collision */ +#ifndef TL_COLLISION_DELAY +#define TL_COLLISION_DELAY 64 +#endif + +/* Set 100 milliseconds as the maximum timeframe between transmission and + synchronous acknowledgement response. This value is strictly related to the + maximum time needed by receiver to receive, compute and transmit a response. + Higher if necessary. */ + +#ifndef TL_RESPONSE_TIME_OUT +#define TL_RESPONSE_TIME_OUT 100000 +#endif + +/* Maximum transmission attempts (re-transmission not supported) */ +#ifndef TL_MAX_ATTEMPTS +#define TL_MAX_ATTEMPTS 5 +#endif + +/* Back-off exponential degree (re-transmission not supported) */ +#ifndef TL_BACK_OFF_DEGREE +#define TL_BACK_OFF_DEGREE 5 +#endif + +/* Response length (the response is composed by the last TL_RESPONSE_LENGTH + bytes of the packet received). By default should be relatively safe. + (Few false positives per millennia) + If you are ready to trade safety for bandwidth reduce it, consider that + setting TL_RESPONSE_LENGTH < 4 reduces reliability and leads to higher + chances of detecting a false positive. */ +#ifndef TL_RESPONSE_LENGTH +#define TL_RESPONSE_LENGTH 5 +#endif diff --git a/hal/transport/PJON/driver/strategies/ThroughSerial/README.md b/hal/transport/PJON/driver/strategies/ThroughSerial/README.md new file mode 100644 index 000000000..074321d60 --- /dev/null +++ b/hal/transport/PJON/driver/strategies/ThroughSerial/README.md @@ -0,0 +1,65 @@ +## ThroughSerial + +| Medium | Pins used | Inclusion | +|--------|-----------|--------------------| +| Wires | 2 | `#include `| + +With `ThroughSerial` strategy, PJON can run through a software or hardware Serial port working out of the box with many Arduino compatible serial transceivers, like RS485 or radio modules like HC-12 (HCMODU0054). It complies with [TSDL v3.0](/src/strategies/ThroughSerial/specification/TSDL-specification-v3.0.md). + +This strategy is based upon the obsolete blocking implementation although reception is now asynchronous and completely non-blocking. It is not required to call `bus.receive()` with any delay, just call it frequently to see if there are any packets that have been received. + +### Why PJON over Serial? +Serial communication is fast and reliable but it is often useless without all the features PJON contains. `ThroughAsyncSerial` has been developed to enable PJON communication through a serial data link. Adding PJON on top of Serial it is possible to leverage of the PJON protocol layer features like acknowledge, addressing, multiplexing, packet handling, 8 or 32-bit CRC and traffic control. + +`ThroughSerial` has been developed primarily to be used in master-slave mode. `ThroughSerial` in multi-master mode, being unable to detect or avoid collisions, operates using the slotted ALOHA medium access method. Of all contention based random multiple access methods, slotted ALOHA, which maximum data throughput is only 36.8% of the available bandwidth, is one of the least efficient and should not be applied in networks where many devices often need to arbitrarily transmit data. + +`ThroughSerial` performs well if used with ESP8266 and ESP32 where blocking procedures can strongly degrade functionality. The reception phase is entirely non-blocking. Sending and acknowledgement however are still blocking. + +### Configuration +Before including the library it is possible to configure `ThroughSerial` using predefined constants: + +| Constant | Purpose | Supported value | +| ----------------------- |------------------------------------ | ------------------------------------------ | +| `TS_READ_INTERVAL` | minimum interval between receptions | Duration in microseconds (100 by default) | +| `TS_BYTE_TIME_OUT` | Maximum byte reception time-out | Duration in microseconds (1000000 by default) | +| `TS_RESPONSE_TIME_OUT` | Maximum response time-out | Duration in microseconds (45000 by default) | +| `TS_BACK_OFF_DEGREE` | Maximum back-off exponential degree | Numeric value (4 by default) | +| `TS_MAX_ATTEMPTS` | Maximum transmission attempts | Numeric value (20 by default) | + +Use `PJONThroughSerial` to instantiate a PJON object ready to communicate using `ThroughSerial` strategy: +```cpp +#include +PJONThroughSerial bus; +``` +Call the `begin` method on the `Serial` or `SoftwareSerial` object you want to use for PJON communication and pass it to the `set_serial` method: +```cpp +#include +PJONThroughSerial bus; + +void setup() { + Serial.begin(9600); + bus.strategy.set_serial(&Serial); +} +``` +There is a default reception interval of 100 microseconds used to allow data to accumulate in the hardware UART buffer. This value is configurable using `bus.strategy.set_read_interval(100)` passing an arbitrary interval in microseconds. The read interval may require adjustment depending on UART RX buffer size and baud rate. +```cpp +bus.strategy.set_read_interval(100); +``` +For a simple use with RS485 serial modules a transmission enable pin setter has been added: +```cpp +bus.strategy.set_enable_RS485_pin(11); +``` +If separate enable setters are needed use: +```cpp +// Set RS485 reception enable pin +bus.strategy.set_RS485_rxe_pin(11); +// Set RS485 transmission enable pin +bus.strategy.set_RS485_txe_pin(12); +``` +See the [BlinkTest](../../../examples/ARDUINO/Local/ThroughSerial/BlinkTest) and [BlinkWithResponse](https://github.com/gioblu/PJON/tree/master/examples/ARDUINO/Local/ThroughSerial/BlinkWithResponse) examples, if you need to interface devices using RS485 see the [RS485-Blink](../../../examples/ARDUINO/Local/ThroughSerial/RS485-Blink) example. HC-12 wireless module supports the synchronous acknowledgement, see [HC-12-Blink](../../../examples/ARDUINO/Local/ThroughSerial/HC-12-Blink), [HC-12-SendAndReceive](../../../examples/ARDUINO/Local/ThroughSerial/HC-12-SendAndReceive) and [HC-12-LocalChat](../../../examples/ARDUINO/Local/ThroughSerial/HC-12-LocalChat) examples. + +All the other necessary information is present in the general [Documentation](/documentation). + +### Known issues +- Transmission is still blocking, will be made non-blocking in the next versions. +- acknowledgement procedure is still blocking, will be made non-blocking in the next versions. diff --git a/hal/transport/PJON/driver/strategies/ThroughSerial/ThroughSerial.h b/hal/transport/PJON/driver/strategies/ThroughSerial/ThroughSerial.h new file mode 100644 index 000000000..c8d412dee --- /dev/null +++ b/hal/transport/PJON/driver/strategies/ThroughSerial/ThroughSerial.h @@ -0,0 +1,505 @@ + +/* ThroughSerial data link layer + used as a Strategy by the PJON framework (included in version v11.2) + Compliant with TSDL (Tardy Serial Data Link) specification v2.0 + + Contributors: + - sticilface async reception development + - Fred Larsen, Development, testing and debugging + - Zbigniew Zasieczny, collision avoidance multi-drop RS485 (latency) + and SoftwareSerial compatibility + - Franketto (Arduino forum user) RS485 TX enable pin compatibility + - Endre Karlson separate RS485 enable pins handling, flush timing hack + ___________________________________________________________________________ + + ThroughSerial, based on ThroughSerial, developed by sticilface + copyright 2018 by Giovanni Blu Mitolo All rights reserved + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + +#pragma once + +// START symbol 10010101 - 0x95 - • +#define TS_START 149 +// END symbol 11101010 - 0xea - ê +#define TS_END 234 +// ESCAPE symbol 10111011 - 0xBB - » +#define TS_ESC 187 + +// Used to signal communication failure +#define TS_FAIL 65535 +// Used for unused pin handling +#define TS_NOT_ASSIGNED 255 + +#include "Timing.h" + +enum TS_state_t : uint8_t { + TS_WAITING, + TS_RECEIVING, + TS_WAITING_ESCAPE, + TS_WAITING_END, + TS_DONE +}; + +// Recommended receive time for this strategy, in microseconds +#ifndef TS_RECEIVE_TIME +#define TS_RECEIVE_TIME 0 +#endif + +class ThroughSerial +{ +public: + uint8_t buffer[PJON_PACKET_MAX_LENGTH] = {0}; + uint16_t position = 0; + TS_state_t state = TS_WAITING; + PJON_SERIAL_TYPE serial; + + /* Returns suggested delay related to the attempts passed as parameter: */ + + uint32_t back_off(uint8_t attempts) + { + uint32_t result = attempts; + for(uint8_t d = 0; d < TS_BACK_OFF_DEGREE; d++) { + result *= (uint32_t)(attempts); + } + return result + PJON_RANDOM(TS_COLLISION_DELAY); + }; + + + /* Begin method, to be called on initialization: + (returns always true) */ + + bool begin(uint8_t did = 0) + { + PJON_DELAY(PJON_RANDOM(TS_INITIAL_DELAY) + did); + _last_reception_time = PJON_MICROS(); + return true; + }; + + + /* Check if the channel is free for transmission: */ + + bool can_start() + { + PJON_DELAY_MICROSECONDS(PJON_RANDOM(TS_COLLISION_DELAY)); + if( + (state != TS_WAITING) || + PJON_SERIAL_AVAILABLE(serial) || + ((uint32_t)(PJON_MICROS() - _last_reception_time) < TS_TIME_IN) + ) { + return false; + } + return true; + }; + + + /* Function called when a frame reception fails */ + + uint16_t fail(TS_state_t s) + { + state = s; + return (uint16_t)TS_FAIL; + } + + + /* Returns the maximum number of attempts for each transmission: */ + + static uint8_t get_max_attempts() + { + return TS_MAX_ATTEMPTS; + }; + + + /* Returns the recommended receive time for this strategy: */ + + static uint16_t get_receive_time() + { + return TS_RECEIVE_TIME; + }; + + + /* Handle a collision: */ + + void handle_collision() + { + PJON_DELAY_MICROSECONDS(PJON_RANDOM(TS_COLLISION_DELAY)); + }; + + + /* Receive Byte */ + + int16_t receive_byte() + { + int16_t value = PJON_SERIAL_READ(serial); + if(value == -1) { + return -1; + } + _last_reception_time = PJON_MICROS(); + return value; + }; + + + /* It returns the state of the previous transmission: */ + + uint16_t receive_response() + { + if(_fail) { + return TS_FAIL; + } + uint32_t time = PJON_MICROS(); + uint8_t i = 0; + while((uint32_t)(PJON_MICROS() - time) < TS_RESPONSE_TIME_OUT) { + if(PJON_SERIAL_AVAILABLE(serial)) { + int16_t read = PJON_SERIAL_READ(serial); + _last_reception_time = PJON_MICROS(); + if(read >= 0) { + if(_response[i++] != read) { + return TS_FAIL; + } + if(i == TS_RESPONSE_LENGTH) { + return PJON_ACK; + } + } + } +#if defined(_WIN32) + PJON_DELAY_MICROSECONDS(TS_RESPONSE_TIME_OUT / 10); +#endif + } + return TS_FAIL; + }; + + + /* Receive a string: */ + + uint16_t receive_frame(uint8_t *data, uint16_t max_length) + { + if( // Reception attempts are spaced by an interval + _last_call_time && + (uint32_t)(PJON_MICROS() - _last_call_time) < _read_interval + ) { + return TS_FAIL; + } + + _last_call_time = PJON_MICROS(); + + if( // If reception timeout is reached discard data + ( + (state == TS_RECEIVING) || + (state == TS_WAITING_END) || + (state == TS_WAITING_ESCAPE) + ) && + ((uint32_t)(PJON_MICROS() - _last_reception_time) > TS_BYTE_TIME_OUT) + ) { + return fail(TS_WAITING); + } + + switch(state) { + case TS_WAITING: { + while(PJON_SERIAL_AVAILABLE(serial)) { + int16_t value = receive_byte(); + if(value == -1) { + return TS_FAIL; + } + if(value == TS_START) { + position = 0; + return fail(TS_RECEIVING); + } + }; + break; + } + case TS_RECEIVING: { + while(PJON_SERIAL_AVAILABLE(serial)) { + int16_t value = receive_byte(); + if((value == TS_START) || (value == -1)) { + return fail(TS_WAITING); + } + if(value == TS_ESC) { + if(!PJON_SERIAL_AVAILABLE(serial)) { + return fail(TS_WAITING_ESCAPE); + } else { + value = receive_byte(); + if(value == -1) { + return fail(TS_WAITING); + } + value = value ^ TS_ESC; + if( + (value != TS_START) && + (value != TS_ESC) && + (value != TS_END) + ) { + return fail(TS_WAITING); + } + buffer[position++] = (uint8_t)value; + continue; + } + } + if(max_length == 1) { + return fail(TS_WAITING_END); + } + if(position + 1 >= PJON_PACKET_MAX_LENGTH) { + return fail(TS_WAITING); + } + if(value == TS_END) { + return fail(TS_DONE); + } + buffer[position++] = (uint8_t)value; + } + return TS_FAIL; + } + + case TS_WAITING_ESCAPE: { + if(PJON_SERIAL_AVAILABLE(serial)) { + int16_t value = receive_byte(); + if(value == -1) { + return fail(TS_WAITING); + } + value = value ^ TS_ESC; + if( + (value != TS_START) && + (value != TS_ESC) && + (value != TS_END) + ) { + return fail(TS_WAITING); + } + buffer[position++] = (uint8_t)value; + return fail(TS_RECEIVING); + } + break; + } + + case TS_WAITING_END: { + if(PJON_SERIAL_AVAILABLE(serial)) { + int16_t value = receive_byte(); + if(value == -1) { + return fail(TS_WAITING); + } + if(value == TS_END) { + return fail(TS_DONE); + } else { + return fail(TS_WAITING); + } + } + break; + } + + case TS_DONE: { + memcpy(&data[0], &buffer[0], position); + prepare_response(buffer, position); + state = TS_WAITING; + return position; + } + + }; + return TS_FAIL; + }; + + + /* Send a byte and wait for its transmission end */ + + void send_byte(uint8_t b) + { + uint32_t time = PJON_MICROS(); + int16_t result = 0; + while( + ((result = PJON_SERIAL_WRITE(serial, b)) != 1) && + ((uint32_t)(PJON_MICROS() - time) < TS_BYTE_TIME_OUT) + ); + if(result != 1) { + _fail = true; + } + }; + + + /* The last 5 bytes of the frame are used as a unique identifier within + the response. PJON has CRC8 or CRC32 at the end of the packet, encoding + a CRC (that is a good hashing algorithm) and using 40 bits looks enough + to provide a relatively safe response that should be nearly flawless + (yield few false positives per millennia). */ + + void prepare_response(const uint8_t *buffer, uint16_t position) + { + uint8_t raw = 0; + for(int8_t i = 0; i < TS_RESPONSE_LENGTH; i++) { + raw = buffer[(position - ((TS_RESPONSE_LENGTH - 1) - i)) - 1]; + _response[i] = ( + (raw == TS_START) || (raw == TS_ESC) || (raw == TS_END) + ) ? (raw - 1) : raw; // Avoid encoding symbols + } + }; + + + /* Send byte response to the packet's transmitter */ + + void send_response(uint8_t response) + { + if(response == PJON_ACK) { + start_tx(); + wait_RS485_pin_change(); + for(uint8_t i = 0; i < TS_RESPONSE_LENGTH; i++) { + send_byte(_response[i]); + } + PJON_SERIAL_FLUSH(serial); + wait_RS485_pin_change(); + end_tx(); + } + }; + + + /* Send a string: */ + + void send_frame(uint8_t *data, uint16_t length) + { + _fail = false; + start_tx(); + uint16_t overhead = 2; + // Add frame flag + send_byte(TS_START); + for(uint16_t b = 0; b < length; b++) { + if(_fail) { + return; + } + // Byte-stuffing + if( + (data[b] == TS_START) || + (data[b] == TS_ESC) || + (data[b] == TS_END) + ) { + send_byte(TS_ESC); + send_byte(data[b] ^ TS_ESC); + overhead++; + } else { + send_byte(data[b]); + } + } + send_byte(TS_END); + /* On RPI flush fails to wait until all bytes are transmitted + here RPI forced to wait blocking using delayMicroseconds */ +#if defined(RPI) || defined(LINUX) + if(_bd) + PJON_DELAY_MICROSECONDS( + ((1000000 / (_bd / 8)) + _flush_offset) * (overhead + length) + ); +#endif + PJON_SERIAL_FLUSH(serial); + end_tx(); + // Prepare expected response for the receive_response call + prepare_response(data, length); + }; + + + /* Pass the Serial port where you want to operate with */ + + void set_serial(PJON_SERIAL_TYPE serial_port) + { + serial = serial_port; + }; + + + /* RS485 enable pins handling: */ + + void start_tx() + { + if(_enable_RS485_txe_pin != TS_NOT_ASSIGNED) { + PJON_IO_WRITE(_enable_RS485_txe_pin, HIGH); + if(_enable_RS485_rxe_pin != TS_NOT_ASSIGNED) { + PJON_IO_WRITE(_enable_RS485_rxe_pin, HIGH); + } + wait_RS485_pin_change(); + } + }; + + void end_tx() + { + if(_enable_RS485_txe_pin != TS_NOT_ASSIGNED) { + wait_RS485_pin_change(); + PJON_IO_WRITE(_enable_RS485_txe_pin, LOW); + if(_enable_RS485_rxe_pin != TS_NOT_ASSIGNED) { + PJON_IO_WRITE(_enable_RS485_rxe_pin, LOW); + } + } + }; + +#if defined(RPI) || defined(LINUX) + /* Pass baudrate to ThroughSerial + (needed only for RPI flush hack): */ + + void set_baud_rate(uint32_t baud) + { + _bd = baud; + }; + + + /* Set flush timing offset in microseconds between expected and real + serial byte transmission: */ + + void set_flush_offset(uint16_t offset) + { + _flush_offset = offset; + }; +#endif + + /* Sets the interval between each read attempt from serial + (TS_READ_INTERVAL or 100 microseconds by default) to allow the buffer + to fill and to reduce the computation time consumed while polling for + incoming data. */ + + uint32_t get_read_interval() + { + return _read_interval; + }; + + void set_read_interval(uint32_t t) + { + _read_interval = t; + }; + + /* RS485 enable pins setters: */ + + void set_enable_RS485_pin(uint8_t pin) + { + set_RS485_txe_pin(pin); + }; + + void set_RS485_rxe_pin(uint8_t pin) + { + _enable_RS485_rxe_pin = pin; + PJON_IO_MODE(_enable_RS485_rxe_pin, OUTPUT); + } + + void set_RS485_txe_pin(uint8_t pin) + { + _enable_RS485_txe_pin = pin; + PJON_IO_MODE(_enable_RS485_txe_pin, OUTPUT); + } + + void wait_RS485_pin_change() + { + if(_enable_RS485_txe_pin != TS_NOT_ASSIGNED) { + PJON_DELAY(_RS485_delay); + } + }; + +private: +#if defined(RPI) || defined(LINUX) + uint16_t _flush_offset = TS_FLUSH_OFFSET; + uint32_t _bd; +#endif + bool _fail = false; + uint8_t _response[TS_RESPONSE_LENGTH]; + uint32_t _last_reception_time = 0; + uint32_t _last_call_time = 0; + uint8_t _enable_RS485_rxe_pin = TS_NOT_ASSIGNED; + uint8_t _enable_RS485_txe_pin = TS_NOT_ASSIGNED; + uint32_t _RS485_delay = TS_RS485_DELAY; + uint32_t _read_interval = TS_READ_INTERVAL; +}; diff --git a/hal/transport/PJON/driver/strategies/ThroughSerial/Timing.h b/hal/transport/PJON/driver/strategies/ThroughSerial/Timing.h new file mode 100644 index 000000000..75c67ed92 --- /dev/null +++ b/hal/transport/PJON/driver/strategies/ThroughSerial/Timing.h @@ -0,0 +1,96 @@ +/* ThroughSerial digital communication data link layer + used as a Strategy by the PJON framework (included in version v4.1) + + Contributors: + - Fred Larsen, Development, testing and debugging + - Zbigniew Zasieczny, collision avoidance multi-drop RS485 (latency) + and SoftwareSerial compatibility + - Franketto (Arduino forum user) RS485 TX enable pin compatibility + ____________________________________________________________________________ + + Based on ThroughSerial, developed by sticilface + copyright 2018 by Giovanni Blu Mitolo All rights reserved + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + +#pragma once + +/* Maximum 1 second random initial delay */ +#ifndef TS_INITIAL_DELAY +#define TS_INITIAL_DELAY 1000 +#endif + +/* Maximum 64 microseconds random delay in case of collision */ +#ifndef TS_COLLISION_DELAY +#define TS_COLLISION_DELAY 64 +#endif + +/* Set 45 milliseconds as the maximum timeframe between transmission and + synchronous acknowledgement response. Its optimal configuration is + strictly related to the maximum time needed by receiver to receive, compute + and transmit back a response. Set TS_RESPONSE_TIME_OUT to 0 and do not use + the acknowledgement feature if the system operates in master-slave mode and + or applies the request-response scheme. */ +#ifndef TS_RESPONSE_TIME_OUT +#define TS_RESPONSE_TIME_OUT 45000 +#endif + +/* Minum duration of channel free for use before transmission, used to avoid + disrupting an ongoing acknowledgement exchange. Set TS_TIME_IN to 0 and do + not use the acknowledgement feature if the system operates in master-slave + mode and or applies the request-response scheme. */ +#ifndef TS_TIME_IN +#define TS_TIME_IN TS_RESPONSE_TIME_OUT + TS_COLLISION_DELAY +#endif + +/* Set 100 microseconds as the interval between each byte read. + Depending on the latency, baud rate and computation time the + optimal TS_READ_INTERVAL value may variate. + Always set: TS_READ_INTERVAL > (byte transmission time + latency) */ +#ifndef TS_READ_INTERVAL +#define TS_READ_INTERVAL 100 +#endif + +/* Byte reception timeout (Default 1 second) */ +#ifndef TS_BYTE_TIME_OUT +#define TS_BYTE_TIME_OUT 1000000 +#endif + +/* Response length (the response is composed by the last TS_RESPONSE_LENGTH + bytes of the packet received). Setting TS_RESPONSE_LENGTH < 4 when using + ThroughSerial in multi-master mode reduces reliability and leads to higher + chances of detecting a false positive. */ +#ifndef TS_RESPONSE_LENGTH +#define TS_RESPONSE_LENGTH 1 +#endif + +/* Maximum transmission attempts */ +#ifndef TS_MAX_ATTEMPTS +#define TS_MAX_ATTEMPTS 20 +#endif + +/* Back-off exponential degree */ +#ifndef TS_BACK_OFF_DEGREE +#define TS_BACK_OFF_DEGREE 4 +#endif + +/* Delay before enabling and disabling RS485 DE and or RE pin */ +#ifndef TS_RS485_DELAY +#define TS_RS485_DELAY 1 +#endif + +/* Force blocking sending hack (adds a delay for each character sent). */ + +#ifndef TS_FLUSH_OFFSET +#define TS_FLUSH_OFFSET 152 +#endif diff --git a/hal/transport/PJON/driver/strategies/ThroughSerial/specification/TSDL-specification-v3.0.md b/hal/transport/PJON/driver/strategies/ThroughSerial/specification/TSDL-specification-v3.0.md new file mode 100644 index 000000000..8b38ca264 --- /dev/null +++ b/hal/transport/PJON/driver/strategies/ThroughSerial/specification/TSDL-specification-v3.0.md @@ -0,0 +1,51 @@ +### Specifications index + +#### Network layer +- [PJON (Padded Jittering Operative Network) v4.0](/specification/PJON-protocol-specification-v4.0.md) +- [Network services list](/specification/PJON-network-services-list.md) +#### Data link layer +- [PJDL (Padded Jittering Data Link) v5.0](/src/strategies/SoftwareBitBang/specification/PJDL-specification-v5.0.md) +- [PJDLR (Padded Jittering Data Link over Radio) v3.0](/src/strategies/OverSampling/specification/PJDLR-specification-v3.0.md) +- [PJDLS (Padded Jittering Data Link byte Stuffed) v2.0](/src/strategies/AnalogSampling/specification/PJDLS-specification-v2.0.md) +- **[TSDL (Tardy Serial Data Link) v3.0](/src/strategies/ThroughSerial/specification/TSDL-specification-v3.0.md)** +- [SFSP (Secure Frame Separation Protocol) v1.0](/specification/SFSP-frame-separation-specification-v1.0.md) + +--- + +## TSDL v3.0 +``` +Invented by Giovanni Blu Mitolo +Originally published: 20/11/2017, latest revision: 09/05/2020 +Related implementation: /src/strategies/ThroughSerial/ +Compliant versions: PJON v13.0 and following +Released into the public domain +``` +TSDL (Tardy Serial Data Link) is a simplex or half-duplex serial data link that supports both master-slave and multi-master modes. It supports collision avoidance, reliable frame separation and optional synchronous response to frame transmissions. +```cpp + ______ TX RX ______ +| |-------| | +|DEVICE| |DEVICE| +|______|-------|______| + RX TX +``` +TSDL can be used to establish a point-to-point link between two devices if used with a bare serial link, or to support one or many to many communication using radio or RS485 transceivers. + +### Medium access control +TSDL operates in master-slave mode applying the request-response procedure and specifies a variation of slotted ALOHA medium access method for multi-master mode. Before a frame transmission the serial buffer is read, if not empty ongoing communication is detected and collision avoided, if empty for a duration longer than the response time-out plus a short random time, frame transmission starts in which the packet is entirely transmitted. Of all contention based random multiple access methods, slotted ALOHA, which maximum data throughput is only 36.8% of the available bandwidth, is one of the least efficient, therefore TSDL in multi-master mode should not be used in networks where many devices often need to arbitrarily transmit data. + +### Frame transmission +Before a frame transmission the communication medium is analysed, if any data is received communication is detected and collision is avoided, if logic 0 is detected for a duration longer than the response time-out plus a small random time, data is transmitted encapsulated in a [SFSP (Secure Frame Separation Protocol) v1.0](/specification/SFSP-frame-separation-specification-v1.0.md) frame. + +### Synchronous response +A frame transmission in both master-slave and multi-master modes can be optionally followed by a synchronous response of its recipient, all devices must use the same response time-out to avoid collisions. The acknowledgment reception phase must occur before the response time-out to be successful. The synchronous response is composed by the last 5 bytes of the received frame that are used by both devices as the frame identifier. If any SFSP flag occurs in the response payload its value is reduced by 1. PJON has a CRC8 or CRC32 at the end of the packet, encoding a CRC (that is a good hashing algorithm) and using 40 bits looks enough to provide a relatively safe response that should yield few false positives per millennia. +```cpp + Transmission Response + _____ ____ ____ ____ ____ ____ ___ _____ + |START|BYTE|BYTE|BYTE|BYTE|BYTE|END| | ACK | + |-----|----|----|----|----|----|---|--|-----| + | 149 | C | I | A | O | ! |234| |CIAO!| + |_____|____|____|____|____|____|___| |_____| +BITS: | 8 | 8 | 8 | 8 | 8 | 8 | 8 | | 40 | + |_____|____|____|____|____|____|___| |_____| +``` +The required response time-out for a given application can be determined practically transmitting the longest supported frame with the farthest physical distance between the two devices. The highest interval between packet transmission and acknowledgement measured plus a small margin is the correct time-out that should exclude acknowledgement losses. diff --git a/hal/transport/PJON/driver/strategies/ThroughSerial/specification/obsolete/TSDL-specification-v1.0.md b/hal/transport/PJON/driver/strategies/ThroughSerial/specification/obsolete/TSDL-specification-v1.0.md new file mode 100644 index 000000000..0dc73942b --- /dev/null +++ b/hal/transport/PJON/driver/strategies/ThroughSerial/specification/obsolete/TSDL-specification-v1.0.md @@ -0,0 +1,62 @@ + +```cpp +/* +Milan, Italy +Originally published: 27/09/2017 +TSDL (Tardy Serial Data Link) v1.0 specification +Invented by Giovanni Blu Mitolo, +released into the public domain + +Related implementation: /src/strategies/ThroughSerial/ +Compliant versions: PJON v9.0 and following +*/ +``` +### TSDL v1.0 + +TSDL (Tardy Serial Data Link) is a simplex or half-duplex serial data link that supports both master-slave and multi-master configuration. It supports collision avoidance, reliable frame separation through byte stuffing and optional synchronous response to frame transmissions. + +### Basic concepts +* Use start, end and escape flags to support frame separation +* Support collision avoidance +* Support optional 1 byte synchronous response to frame transmissions + +```cpp + ______ TX RX ______ +| |-------| | +|DEVICE| |DEVICE| +|______|-------|______| + RX TX +``` +TSDL can be used to establish a point-to-point link between two devices if used with a bare serial link, or supporting one or many to many communication with physical layers that are supporting this feature, like serial radio or RS485 transceivers. + +#### Frame transmission +Before a frame transmission, the serial buffer is read, if not empty ongoing communication is detected and collision avoided, if empty for a duration longer than the time-in before transmission, frame transmission starts with `START` flag, followed by data bytes, if necessary escaped with `ESC` flag and terminates the frame with an `END` flag. +```cpp + ______________________________ + | DATA 1-65535 bytes | + _______ |______ _____ _______ ______| _____ +| START | | BYTE || ESC || START || BYTE | | END | +|-------| |------||-----||-------||------| |-----| +| 149 | | 23 || 76 || 149 || 52 | | 234 | +|_______| |______||_____||_______||______| |_____| + | + Flags inside data are escaped + +START: 149 - 10010101 - 0x95 - • +END: 234 - 11101010 - 0xea - ê +ESC: 187 - 10111011 - 0xBB - » +``` +`START` and `END` flag bytes are special characters that signal when a frame begins and ends. +Whenever any of the special character appears in the data, transmitter inserts a special `ESC` character before it, that will be ignored and excluded from data during the reception process. Any corrupted special character or data byte causes the receiver to discard the frame and be ready to receive the next one nominally. + +#### Synchronous response +A frame transmission can be optionally followed by a synchronous response by its recipient. +```cpp +Transmission Response + _______ ______ ______ _____ _____ +| START || BYTE || BYTE || END | CRC COMPUTATION | ACK | +|-------||------||------||-----|-----------------|-----| +| 149 || H || I || 234 | LATENCY | 6 | +|_______||______||______||_____| |_____| +``` +Between frame transmission and a synchronous response there is a variable timeframe influenced by latency and CRC computation time. The maximum time dedicated to potential acknowledgement reception must be shorter than the transmission time-in, to avoid other devices to disrupt a response exchange, and it is estimated adding the maximum frame length CRC computation time to the expected latency. diff --git a/hal/transport/PJON/driver/strategies/ThroughSerial/specification/obsolete/TSDL-specification-v2.0.md b/hal/transport/PJON/driver/strategies/ThroughSerial/specification/obsolete/TSDL-specification-v2.0.md new file mode 100644 index 000000000..81e989319 --- /dev/null +++ b/hal/transport/PJON/driver/strategies/ThroughSerial/specification/obsolete/TSDL-specification-v2.0.md @@ -0,0 +1,46 @@ + +```cpp +/* +Milan, Italy +Originally published: 20/11/2017 +TSDL (Tardy Serial Data Link) v2.0 specification +Invented by Giovanni Blu Mitolo, +released into the public domain + +Related implementation: /src/strategies/ThroughSerial/ +Compliant versions: PJON v10.0 and following +Changelog: +- Frame separation provided by SFSP v1.0 +*/ +``` + +### TSDL v2.0 +TSDL (Tardy Serial Data Link) is a simplex or half-duplex serial data link that supports both master-slave and multi-master configuration. It supports collision avoidance, reliable frame separation and optional synchronous response to frame transmissions. +```cpp + ______ TX RX ______ +| |-------| | +|DEVICE| |DEVICE| +|______|-------|______| + RX TX +``` +TSDL can be used to establish a point-to-point link between two devices if used with a bare serial link, or supporting one or many to many communication with physical layers that are supporting this feature, like serial radio or RS485 transceivers. + +### Basic concepts +* Frame separation support provided by [SFSP v1.0](/specification/SFSP-frame-separation-specification-v1.0.md) +* Support collision avoidance +* Support optional 1 byte synchronous response to frame transmissions + +#### Collision avoidance +Before a frame transmission, the serial buffer is read, if not empty ongoing communication is detected and collision avoided, if empty for a duration longer than the time-in (that should be common on all connected devices) before transmission, plus a short random time, frame transmission starts in which the packet is entirely transmitted. + +#### Synchronous response +A frame transmission can be optionally followed by a synchronous response by its recipient. +```cpp +Transmission Response + _______ ______ ______ _____ _____ +| START || BYTE || BYTE || END | CRC COMPUTATION | ACK | +|-------||------||------||-----|-----------------|-----| +| 149 || H || I || 234 | LATENCY | 6 | +|_______||______||______||_____| |_____| +``` +Between frame transmission and a synchronous response there is a variable timeframe influenced by latency and CRC computation time. The maximum time dedicated to potential acknowledgement reception must be shorter than the transmission time-in (to avoid other devices to disrupt a response exchange) and it is estimated adding the maximum frame length CRC computation time to the expected latency. diff --git a/hal/transport/PJON/driver/strategies/ThroughSerial/specification/obsolete/TSDL-specification-v2.1.md b/hal/transport/PJON/driver/strategies/ThroughSerial/specification/obsolete/TSDL-specification-v2.1.md new file mode 100644 index 000000000..0dd678337 --- /dev/null +++ b/hal/transport/PJON/driver/strategies/ThroughSerial/specification/obsolete/TSDL-specification-v2.1.md @@ -0,0 +1,49 @@ +### Specifications index + +#### Network layer +- [PJON (Padded Jittering Operative Network) v4.0](/specification/PJON-protocol-specification-v4.0.md) +- [Network services list](/specification/PJON-network-services-list.md) +#### Data link layer +- [PJDL (Padded Jittering Data Link) v4.1](/src/strategies/SoftwareBitBang/specification/PJDL-specification-v4.1.md) +- [PJDLR (Padded Jittering Data Link over Radio) v3.0](/src/strategies/OverSampling/specification/PJDLR-specification-v3.0.md) +- [PJDLS (Padded Jittering Data Link byte Stuffed) v2.0](/src/strategies/AnalogSampling/specification/PJDLS-specification-v2.0.md) +- **[TSDL (Tardy Serial Data Link) v2.1](/src/strategies/ThroughSerial/specification/TSDL-specification-v2.1.md)** +- [SFSP (Secure Frame Separation Protocol) v1.0](/specification/SFSP-frame-separation-specification-v1.0.md) + +--- + +## TSDL v2.1 +``` +Invented by Giovanni Blu Mitolo +Originally published: 20/11/2017, latest revision: 9/11/2018 +Related implementation: /src/strategies/ThroughSerial/ +Compliant versions: PJON v10.0 and following +Released into the public domain +``` +TSDL (Tardy Serial Data Link) is a simplex or half-duplex serial data link that supports both master-slave and multi-master modes. It supports collision avoidance, reliable frame separation and optional synchronous response to frame transmissions. +```cpp + ______ TX RX ______ +| |-------| | +|DEVICE| |DEVICE| +|______|-------|______| + RX TX +``` +TSDL can be used to establish a point-to-point link between two devices if used with a bare serial link, or to support one or many to many communication using radio or RS485 transceivers. + +### Medium access control +TSDL operates in master-slave mode applying the request-response procedure. Being impossible to implement a carrier-sense procedure over a serial port TSDL uses variation of slotted ALOHA medium access method for multi-master mode. Before a frame transmission the serial buffer is read, if not empty ongoing communication is detected and collision avoided, if empty for a duration longer than the response time-out plus a short random time, frame transmission starts in which the packet is entirely transmitted. Of all contention based random multiple access methods, slotted ALOHA, which maximum data throughput is only 36.8% of the available bandwidth, is one of the least efficient, therefore TSDL in multi-master mode should not be used in networks where many devices often need to arbitrarily transmit data. + +### Frame transmission +Before a frame transmission the communication medium is analysed, if any data is received communication is detected and collision is avoided, if logic 0 is detected for a duration longer than the response time-out plus a small random time, data is transmitted encapsulated in a [SFSP (Secure Frame Separation Protocol) v1.0](/specification/SFSP-frame-separation-specification-v1.0.md) frame. + +### Synchronous response +A frame transmission in both master-slave and multi-master modes can be optionally followed by a synchronous response of its recipient, all devices must use the same response time-out to avoid collisions. The acknowledgment reception phase must be shorter than the response time-out to be successful. +```cpp +Transmission Response + _______ ______ ______ _____ _____ +| START || BYTE || BYTE || END | CRC COMPUTATION | ACK | +|-------||------||------||-----|-----------------|-----| +| 149 || H || I || 234 | LATENCY | 6 | +|_______||______||______||_____| |_____| +``` +The required response time-out for a given application can be determined practically transmitting the longest supported frame with the farthest physical distance between the two devices. The highest interval between packet transmission and acknowledgement measured plus a small margin is the correct time-out that should exclude acknowledgement losses. diff --git a/hal/transport/PJON/driver/utils/crc/PJON_CRC32.h b/hal/transport/PJON/driver/utils/crc/PJON_CRC32.h new file mode 100644 index 000000000..208dfc9d0 --- /dev/null +++ b/hal/transport/PJON/driver/utils/crc/PJON_CRC32.h @@ -0,0 +1,55 @@ +#pragma once + +/* CRC32 table-less implementation + See: http://www.hackersdelight.org/hdcodetxt/crc.c.txt + + Polynomial + + 0x82608edb = x^32+x^26+x^23+x^22+x^16+x^12+x^11+x^10+x^8+x^7+x^5+x^4+x^2+x+1 + (0x82608edb; 0x104c11db7) <=> (0xedb88320; 0x1db710641) + | + bit-reversed polynomial implicit +1 notation + or reverse reciprocal notation */ + +struct PJON_crc32 { + + static inline uint32_t compute( + const uint8_t *data, + uint16_t length, + uint32_t previousCrc32 = 0 + ) + { + uint8_t bits; + uint32_t crc = ~previousCrc32; // same as previousCrc32 ^ 0xFFFFFFFF + uint8_t * current = (uint8_t*) data; + while(length--) { + crc ^= *current++; + bits = 8; + while(bits--) { + if(crc & 1) { + crc = (crc >> 1) ^ 0xEDB88320; + } else { + crc = crc >> 1; + } + } + } + return ~crc; // same as crc ^ 0xFFFFFFFF + }; + + + static inline bool compare( + const uint32_t computed, + const uint8_t *received + ) + { + for(uint8_t i = 4; i > 0; i--) + if( + (uint8_t)(computed >> (8 * (i - 1))) != + (uint8_t)(received[3 - (i - 1)]) + ) { + return false; + } + return true; + }; + +}; diff --git a/hal/transport/PJON/driver/utils/crc/PJON_CRC8.h b/hal/transport/PJON/driver/utils/crc/PJON_CRC8.h new file mode 100644 index 000000000..3368373bf --- /dev/null +++ b/hal/transport/PJON/driver/utils/crc/PJON_CRC8.h @@ -0,0 +1,36 @@ + +#pragma once + +/* Compute CRC8 with a table-less implementation: + Copyright Giovanni Blu Mitolo giorscarab@gmail.com 2018 + + CRC8 C2, source Baicheva98 (implicit + 1 notation) + 0x97 = (x + 1)(x^7 + x^6 + x^5 + x^2 + 1)^2 + Chosen because it has the largest possible length (119 bit) at which + HD=4 can be achieved with 8-bit CRC. */ + +struct PJON_crc8 { + + static inline uint8_t roll(uint8_t input_byte, uint8_t crc) + { + for(uint8_t i = 8; i; i--, input_byte >>= 1) { + uint8_t result = (crc ^ input_byte) & 0x01; + crc >>= 1; + if(result) { + crc ^= 0x97; + } + } + return crc; + }; + + + static inline uint8_t compute(const uint8_t *input_byte, uint16_t length) + { + uint8_t crc = 0; + for(uint16_t b = 0; b < length; b++) { + crc = roll(input_byte[b], crc); + } + return crc; + }; + +}; diff --git a/keywords.txt b/keywords.txt index d63941799..70f5885e2 100755 --- a/keywords.txt +++ b/keywords.txt @@ -239,6 +239,12 @@ MY_RS485_HWSERIAL LITERAL1 MY_RS485_MAX_MESSAGE_LENGTH LITERAL1 MY_RS485_SOH_COUNT LITERAL1 +# PJON +MY_PJON LITERAL1 +MY_PJON_PIN LITERAL1 +MY_DEBUG_VERBOSE_PJON LITERAL1 +MY_PJON_MAX_RETRIES LITERAL1 + # Gateway / MQTT MY_GATEWAY_CLIENT_MODE LITERAL1 MY_GATEWAY_ENC28J60 LITERAL1 diff --git a/tests/Arduino/sketches/pjon_transport/pjon_transport.ino b/tests/Arduino/sketches/pjon_transport/pjon_transport.ino new file mode 100644 index 000000000..53e1741d2 --- /dev/null +++ b/tests/Arduino/sketches/pjon_transport/pjon_transport.ino @@ -0,0 +1,24 @@ +/* + * The MySensors Arduino library handles the wireless radio link and protocol + * between your home built sensors/actuators and HA controller of choice. + * The sensors forms a self healing radio network with optional repeaters. Each + * repeater and gateway builds a routing tables in EEPROM which keeps track of the + * network topology allowing messages to be routed to nodes. + * + * Created by Henrik Ekblad + * Copyright (C) 2013-2019 Sensnology AB + * Full contributor list: https://github.com/mysensors/MySensors/graphs/contributors + * + * Documentation: http://www.mysensors.org + * Support Forum: http://forum.mysensors.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * version 2 as published by the Free Software Foundation. + * + ******************************* + */ +#define MY_DEBUG +#define MY_PJON +#define MY_DEBUG_VERBOSE_PJON +#include \ No newline at end of file