From 36df8d009ff8bc0ece8c4c9c6475febcea85cd46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Voborsk=C3=BD?= Date: Sat, 28 Dec 2019 14:10:11 +0100 Subject: [PATCH] Add DCC decoder module (#2905) --- app/driver/NmraDcc.c | 1161 ++++++++++++++++++++++++++++++++++ app/include/driver/NmraDcc.h | 479 ++++++++++++++ app/include/user_modules.h | 1 + app/modules/dcc.c | 290 +++++++++ docs/modules/dcc.md | 118 ++++ lua_examples/dcc/dcc.lua | 86 +++ mkdocs.yml | 1 + 7 files changed, 2136 insertions(+) create mode 100644 app/driver/NmraDcc.c create mode 100644 app/include/driver/NmraDcc.h create mode 100644 app/modules/dcc.c create mode 100644 docs/modules/dcc.md create mode 100644 lua_examples/dcc/dcc.lua diff --git a/app/driver/NmraDcc.c b/app/driver/NmraDcc.c new file mode 100644 index 00000000..9fb70cb7 --- /dev/null +++ b/app/driver/NmraDcc.c @@ -0,0 +1,1161 @@ +//------------------------------------------------------------------------ +// +// Model Railroading with Arduino - NmraDcc.cpp +// +// Copyright (c) 2008 - 2017 Alex Shepherd +// +// This source file is subject of the GNU general public license 2, +// that is available at the world-wide-web at +// http://www.gnu.org/licenses/gpl.txt +// +//------------------------------------------------------------------------ +// +// file: NmraDcc.cpp +// author: Alex Shepherd +// webpage: http://mrrwa.org/ +// history: 2008-03-20 Initial Version +// 2011-06-26 Migrated into Arduino library from OpenDCC codebase +// 2014 Added getAddr to NmraDcc Geoff Bunza +// 2015-11-06 Martin Pischky (martin@pischky.de): +// Experimental Version to support 14 speed steps +// and new signature of notifyDccSpeed and notifyDccFunc +// 2015-12-16 Version without use of Timer0 by Franz-Peter Müller +// 2016-07-16 handle glitches on DCC line +// 2016-08-20 added ESP8266 support by Sven (littleyoda) +// 2017-01-19 added STM32F1 support by Franz-Peter +// 2017-11-29 Ken West (kgw4449@gmail.com): +// Minor fixes to pass NMRA Baseline Conformance Tests. +// 2018-12-17 added ESP32 support by Trusty (thierry@lapajaparis.net) +// 2019-02-17 added ESP32 specific changes by Hans Tanner +// +//------------------------------------------------------------------------ +// +// purpose: Provide a simplified interface to decode NMRA DCC packets +// and build DCC Mobile and Stationary Decoders +// +//------------------------------------------------------------------------ + +// NodeMCU Lua port by @voborsky + +// #define NODE_DEBUG + +#include +#include +#include +#include "platform.h" +#include "user_interface.h" +#include "task/task.h" +#include "driver/NmraDcc.h" + +#define BYTE_TO_BINARY_PATTERN "%c%c%c%c%c%c%c%c" +#define BYTE_TO_BINARY(byte) \ + (byte & 0x80 ? '1' : '0'), \ + (byte & 0x40 ? '1' : '0'), \ + (byte & 0x20 ? '1' : '0'), \ + (byte & 0x10 ? '1' : '0'), \ + (byte & 0x08 ? '1' : '0'), \ + (byte & 0x04 ? '1' : '0'), \ + (byte & 0x02 ? '1' : '0'), \ + (byte & 0x01 ? '1' : '0') + + + +//------------------------------------------------------------------------ +// DCC Receive Routine +// +// Howto: uses two interrupts: a rising edge in DCC polarity triggers INTx +// in INTx handler, Timer0 CompareB with a delay of 80us is started. +// On Timer0 CompareB Match the level of DCC is evaluated and +// parsed. +// +// |<-----116us----->| +// +// DCC 1: _________XXXXXXXXX_________XXXXXXXXX_________ +// ^-INTx +// |----87us--->| +// ^Timer-INT: reads zero +// +// DCC 0: _________XXXXXXXXXXXXXXXXXX__________________ +// ^-INTx +// |----------->| +// ^Timer-INT: reads one +// +// new DCC Receive Routine without Timer0 ........................................................ +// +// Howto: uses only one interrupt at the rising or falling edge of the DCC signal +// The time between two edges is measured to determine the bit value +// Synchronising to the edge of the first part of a bit is done after recognizing the start bit +// During synchronizing each part of a bit is detected ( Interruptmode 'change' ) +// +// |<-----116us----->| +// DCC 1: _________XXXXXXXXX_________XXXXXXXXX_________ +// |<--------146us------>| +// ^-INTx ^-INTx +// less than 138us: its a one-Bit +// +// +// |<-----------------232us----------->| +// DCC 0: _________XXXXXXXXXXXXXXXXXX__________________XXXXXXXX__________ +// |<--------146us------->| +// ^-INTx ^-INTx +// greater than 138us: its a zero bit +// +// +// +// +//------------------------------------------------------------------------ + +#define abs(a) ((a) > 0 ? (a) : (0-a)) + + +#define MAX_ONEBITFULL 146 +#define MAX_PRAEAMBEL 146 +#define MAX_ONEBITHALF 82 +#define MIN_ONEBITFULL 82 +#define MIN_ONEBITHALF 35 +#define MAX_BITDIFF 18 + + + +#ifdef NODE_DEBUG + #define PULLUP PLATFORM_GPIO_PULLUP + #define OUTPUT PLATFORM_GPIO_OUTPUT + #define HIGH PLATFORM_GPIO_HIGH + #define LOW PLATFORM_GPIO_LOW + + #define MODE_TP1 platform_gpio_mode( 5, OUTPUT, PULLUP ); // GPIO 14 + #define SET_TP1 platform_gpio_write(5, HIGH); + #define CLR_TP1 platform_gpio_write(5, LOW); + #define MODE_TP2 platform_gpio_mode( 6, OUTPUT, PULLUP ); // GPIO 12 + #define SET_TP2 platform_gpio_write(6, HIGH); + #define CLR_TP2 platform_gpio_write(6, LOW); + #define MODE_TP3 platform_gpio_mode( 7, OUTPUT, PULLUP ); // GPIO 13 + #define SET_TP3 platform_gpio_write(7, HIGH); + #define CLR_TP3 platform_gpio_write(7, LOW); + #define MODE_TP4 platform_gpio_mode( 8, OUTPUT, PULLUP ); // GPIO 15 + #define SET_TP4 platform_gpio_write(8, HIGH); + #define CLR_TP4 platform_gpio_write(8, LOW); +#else + #define MODE_TP1 + #define SET_TP1 + #define CLR_TP1 + #define MODE_TP2 + #define SET_TP2 + #define CLR_TP2 + #define MODE_TP3 + #define SET_TP3 + #define CLR_TP3 + #define MODE_TP4 + #define SET_TP4 + #define CLR_TP4 +#endif + +static uint8_t ISREdge; // Holder of the Next Edge we're looking for: RISING or FALLING +static int16_t bitMax, bitMin; + +DCC_MSG Msg ; + +typedef enum +{ + WAIT_PREAMBLE = 0, + WAIT_START_BIT, + WAIT_DATA, + WAIT_END_BIT +} +DccRxWaitState ; + +typedef enum +{ + OPS_INS_RESERVED = 0, + OPS_INS_VERIFY_BYTE, + OPS_INS_BIT_MANIPULATION, + OPS_INS_WRITE_BYTE +} +OpsInstructionType; + +struct DccRx_t +{ + DccRxWaitState State ; + uint8_t BitCount ; + uint8_t TempByte ; + DCC_MSG PacketBuf; + DCC_MSG PacketCopy; +} +DccRx ; + +typedef struct +{ + uint8_t Flags ; + uint8_t OpsModeAddressBaseCV ; + uint8_t inServiceMode ; + long LastServiceModeMillis ; + uint8_t PageRegister ; // Used for Paged Operations in Service Mode Programming + uint8_t DuplicateCount ; + DCC_MSG LastMsg ; + uint8_t IntPin; + uint8_t IntBitmask; + int16_t myDccAddress; // Cached value of DCC Address from CVs + uint8_t inAccDecDCCAddrNextReceivedMode; +#ifdef DCC_DEBUG + uint8_t IntCount; + uint8_t TickCount; +#endif +} +DCC_PROCESSOR_STATE ; + +DCC_PROCESSOR_STATE DccProcState ; + +task_handle_t DataReady_taskid; + +static uint32_t ICACHE_RAM_ATTR InterruptHandler (uint32_t ret_gpio_status) +{ + // This function really is running at interrupt level with everything + // else masked off. It should take as little time as necessary. + + uint32 gpio_status = GPIO_REG_READ(GPIO_STATUS_ADDRESS); + if ((gpio_status & DccProcState.IntBitmask) == 0) { + return ret_gpio_status; + } + + GPIO_REG_WRITE(GPIO_STATUS_W1TC_ADDRESS, gpio_status & DccProcState.IntBitmask); + uint32_t actMicros = system_get_time(); + ret_gpio_status &= ~(DccProcState.IntBitmask); + + // Bit evaluation without Timer 0 ------------------------------ + uint8_t DccBitVal; + static int8_t bit1, bit2 ; + static unsigned long lastMicros = 0; + static uint8_t halfBit; + unsigned long bitMicros; + SET_TP3; + bitMicros = actMicros-lastMicros; + if ( bitMicros < bitMin ) { + // too short - my be false interrupt due to glitch or false protocol -> ignore + CLR_TP3; + return ret_gpio_status; //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> abort IRQ + } + DccBitVal = ( bitMicros < bitMax ); + lastMicros = actMicros; + #ifdef NODE_DEBUG + if(DccBitVal) {SET_TP2;} else {CLR_TP2;}; + #endif + #ifdef DCC_DEBUG + DccProcState.TickCount++; + #endif + + switch( DccRx.State ) + { + case WAIT_PREAMBLE: + if( DccBitVal ) + { + SET_TP1; + DccRx.BitCount++; + if( DccRx.BitCount > 10 ) { + DccRx.State = WAIT_START_BIT ; + // While waiting for the start bit, detect halfbit lengths. We will detect the correct + // sync and detect whether we see a false (e.g. motorola) protocol + + gpio_pin_intr_state_set(GPIO_ID_PIN(pin_num[DccProcState.IntPin]), GPIO_PIN_INTR_ANYEDGE); + halfBit = 0; + bitMax = MAX_ONEBITHALF; + bitMin = MIN_ONEBITHALF; + CLR_TP1; + } + } else { + SET_TP1; + DccRx.BitCount = 0 ; + CLR_TP1; + } + break; + + case WAIT_START_BIT: + // we are looking for first half "0" bit after preamble + switch ( halfBit ) { + case 0: //SET_TP1; + // check first part + if ( DccBitVal ) { + // is still 1-bit (Preamble) + halfBit=1; + bit1=bitMicros; + } else { + // was "0" half bit, maybe the startbit + SET_TP1; + halfBit = 4; + CLR_TP1; + } + break; + case 1: //SET_TP1; // previous halfbit was '1' + if ( DccBitVal ) { + // its a '1' halfBit -> we are still in the preamble + halfBit = 0; + bit2=bitMicros; + DccRx.BitCount++; + if( abs(bit2-bit1) > MAX_BITDIFF ) { + // the length of the 2 halfbits differ too much -> wrong protokoll + CLR_TP2; + CLR_TP3; + DccRx.State = WAIT_PREAMBLE; + bitMax = MAX_PRAEAMBEL; + bitMin = MIN_ONEBITFULL; + DccRx.BitCount = 0; + SET_TP4; + gpio_pin_intr_state_set(GPIO_ID_PIN(pin_num[DccProcState.IntPin]), ISREdge); + SET_TP3; + CLR_TP4; + } + } else { + // first '0' half detected in second halfBit + // wrong sync or not a DCC protokoll + CLR_TP3; + halfBit = 3; + SET_TP3; + } + break; + case 3: //SET_TP1; // previous halfbit was '0' in second halfbit + if ( DccBitVal ) { + // its a '1' halfbit -> we got only a half '0' bit -> cannot be DCC + DccRx.State = WAIT_PREAMBLE; + bitMax = MAX_PRAEAMBEL; + bitMin = MIN_ONEBITFULL; + DccRx.BitCount = 0; + } else { + // we got two '0' halfbits -> it's the startbit + // but sync is NOT ok, change IRQ edge. + if ( ISREdge == GPIO_PIN_INTR_POSEDGE ) ISREdge = GPIO_PIN_INTR_NEGEDGE; else ISREdge = GPIO_PIN_INTR_POSEDGE; + DccRx.State = WAIT_DATA ; + bitMax = MAX_ONEBITFULL; + bitMin = MIN_ONEBITFULL; + DccRx.PacketBuf.Size = 0; + DccRx.PacketBuf.PreambleBits = 0; + for(uint8_t i = 0; i< MAX_DCC_MESSAGE_LEN; i++ ) + DccRx.PacketBuf.Data[i] = 0; + + DccRx.PacketBuf.PreambleBits = DccRx.BitCount; + DccRx.BitCount = 0 ; + DccRx.TempByte = 0 ; + } + SET_TP4; + gpio_pin_intr_state_set(GPIO_ID_PIN(pin_num[DccProcState.IntPin]), ISREdge); + CLR_TP1; + CLR_TP4; + break; + case 4: SET_TP1; // previous (first) halfbit was 0 + // if this halfbit is 0 too, we got the startbit + if ( DccBitVal ) { + // second halfbit is 1 -> unknown protokoll + DccRx.State = WAIT_PREAMBLE; + bitMax = MAX_PRAEAMBEL; + bitMin = MIN_ONEBITFULL; + DccRx.BitCount = 0; + } else { + // we got the startbit + DccRx.State = WAIT_DATA ; + bitMax = MAX_ONEBITFULL; + bitMin = MIN_ONEBITFULL; + DccRx.PacketBuf.Size = 0; + DccRx.PacketBuf.PreambleBits = 0; + for(uint8_t i = 0; i< MAX_DCC_MESSAGE_LEN; i++ ) + DccRx.PacketBuf.Data[i] = 0; + + DccRx.PacketBuf.PreambleBits = DccRx.BitCount; + DccRx.BitCount = 0 ; + DccRx.TempByte = 0 ; + } + + CLR_TP1; + SET_TP4; + gpio_pin_intr_state_set(GPIO_ID_PIN(pin_num[DccProcState.IntPin]), ISREdge); + CLR_TP4; + break; + + } + break; + + case WAIT_DATA: + DccRx.BitCount++; + DccRx.TempByte = ( DccRx.TempByte << 1 ) ; + if( DccBitVal ) + DccRx.TempByte |= 1 ; + + if( DccRx.BitCount == 8 ) + { + if( DccRx.PacketBuf.Size == MAX_DCC_MESSAGE_LEN ) // Packet is too long - abort + { + DccRx.State = WAIT_PREAMBLE ; + bitMax = MAX_PRAEAMBEL; + bitMin = MIN_ONEBITFULL; + DccRx.BitCount = 0 ; + } + else + { + DccRx.State = WAIT_END_BIT ; + DccRx.PacketBuf.Data[ DccRx.PacketBuf.Size++ ] = DccRx.TempByte ; + } + } + break; + + case WAIT_END_BIT: + DccRx.BitCount++; + if( DccBitVal ) // End of packet? + { + CLR_TP3; + DccRx.State = WAIT_PREAMBLE ; + bitMax = MAX_PRAEAMBEL; + bitMin = MIN_ONEBITFULL; + DccRx.PacketCopy = DccRx.PacketBuf ; + uint8_t param; + task_post_high(DataReady_taskid, (os_param_t) ¶m); + SET_TP3; + } + else // Get next Byte + // KGW - Abort immediately if packet is too long. + if( DccRx.PacketBuf.Size == MAX_DCC_MESSAGE_LEN ) // Packet is too long - abort + { + DccRx.State = WAIT_PREAMBLE ; + bitMax = MAX_PRAEAMBEL; + bitMin = MIN_ONEBITFULL; + DccRx.BitCount = 0 ; + } + else + { + DccRx.State = WAIT_DATA ; + + DccRx.BitCount = 0 ; + DccRx.TempByte = 0 ; + } + } + + CLR_TP1; + CLR_TP3; + return ret_gpio_status; +} + +uint8_t validCV( uint16_t CV, uint8_t Writable ) +{ + if( notifyCVResetFactoryDefault && (CV == CV_MANUFACTURER_ID ) && Writable ) + notifyCVResetFactoryDefault(); + + if( notifyCVValid ) + return notifyCVValid( CV, Writable ) ; + return 0; +} + +uint8_t readCV( unsigned int CV ) +{ + if( notifyCVRead ) + return notifyCVRead( CV ) ; + return 0; +} + +uint8_t writeCV( unsigned int CV, uint8_t Value) +{ + switch( CV ) + { + case CV_29_CONFIG: + // copy addressmode Bit to Flags + DccProcState.Flags = ( DccProcState.Flags & ~FLAGS_CV29_BITS) | (Value & FLAGS_CV29_BITS); + // no break, because myDccAdress must also be reset + case CV_ACCESSORY_DECODER_ADDRESS_LSB: // Also same CV for CV_MULTIFUNCTION_PRIMARY_ADDRESS + case CV_ACCESSORY_DECODER_ADDRESS_MSB: + case CV_MULTIFUNCTION_EXTENDED_ADDRESS_MSB: + case CV_MULTIFUNCTION_EXTENDED_ADDRESS_LSB: + DccProcState.myDccAddress = -1; // Assume any CV Write Operation might change the Address + } + + if( notifyCVWrite ) + return notifyCVWrite( CV, Value ) ; + return 0; +} + +uint16_t getMyAddr(void) +{ + uint8_t CV29Value ; + + if( DccProcState.myDccAddress != -1 ) // See if we can return the cached value + return( DccProcState.myDccAddress ); + + CV29Value = readCV( CV_29_CONFIG ) ; + + if( CV29Value & CV29_ACCESSORY_DECODER ) // Accessory Decoder? + { + if( CV29Value & CV29_OUTPUT_ADDRESS_MODE ) + DccProcState.myDccAddress = ( readCV( CV_ACCESSORY_DECODER_ADDRESS_MSB ) << 8 ) | readCV( CV_ACCESSORY_DECODER_ADDRESS_LSB ); + else + DccProcState.myDccAddress = ( ( readCV( CV_ACCESSORY_DECODER_ADDRESS_MSB ) & 0b00000111) << 6 ) | ( readCV( CV_ACCESSORY_DECODER_ADDRESS_LSB ) & 0b00111111) ; + } + else // Multi-Function Decoder? + { + if( CV29Value & CV29_EXT_ADDRESSING ) // Two Byte Address? + DccProcState.myDccAddress = ( ( readCV( CV_MULTIFUNCTION_EXTENDED_ADDRESS_MSB ) - 192 ) << 8 ) | readCV( CV_MULTIFUNCTION_EXTENDED_ADDRESS_LSB ) ; + + else + DccProcState.myDccAddress = readCV( 1 ) ; + } + + return DccProcState.myDccAddress ; +} + +void processDirectOpsOperation( uint8_t Cmd, uint16_t CVAddr, uint8_t Value ) +{ + // is it a Byte Operation + if( Cmd & 0x04 ) + { + // Perform the Write Operation + if( Cmd & 0x08 ) + { + if( validCV( CVAddr, 1 ) ) + { + writeCV( CVAddr, Value ); + } + } + } + // Perform the Bit-Wise Operation + else + { + uint8_t BitMask = (1 << (Value & 0x07) ) ; + uint8_t BitValue = Value & 0x08 ; + uint8_t BitWrite = Value & 0x10 ; + + uint8_t tempValue = readCV( CVAddr ) ; // Read the Current CV Value + + // Perform the Bit Write Operation + if( BitWrite ) + { + if( validCV( CVAddr, 1 ) ) + { + if( BitValue ) + tempValue |= BitMask ; // Turn the Bit On + + else + tempValue &= ~BitMask ; // Turn the Bit Off + + writeCV( CVAddr, tempValue ); + } + } + } +} + +#ifdef NMRA_DCC_PROCESS_MULTIFUNCTION +void processMultiFunctionMessage( uint16_t Addr, DCC_ADDR_TYPE AddrType, uint8_t Cmd, uint8_t Data1, uint8_t Data2 ) +{ + uint8_t speed ; + uint16_t CVAddr ; + DCC_DIRECTION dir ; + DCC_SPEED_STEPS speedSteps ; + + uint8_t CmdMasked = Cmd & 0b11100000 ; + + // NODE_DBG("[dcc_processMultiFunctionMessage] Addr: %d, Type: %d, Cmd: %d ("BYTE_TO_BINARY_PATTERN"), Data: %d, %d, CmdMasked="BYTE_TO_BINARY_PATTERN"\n", Addr, AddrType, Cmd, BYTE_TO_BINARY(Cmd), Data1, Data2, BYTE_TO_BINARY(CmdMasked)); + + // If we are an Accessory Decoder + if( DccProcState.Flags & FLAGS_DCC_ACCESSORY_DECODER ) + { + // NODE_DBG("[dcc_processMultiFunctionMessage] DccProcState.Flags & FLAGS_DCC_ACCESSORY_DECODER\n"); + // and this isn't an Ops Mode Write or we are NOT faking the Multifunction Ops mode address in CV 33+34 or + // it's not our fake address, then return + if( ( CmdMasked != 0b11100000 ) || ( DccProcState.OpsModeAddressBaseCV == 0 ) ) + return ; + + uint16_t FakeOpsAddr = readCV( DccProcState.OpsModeAddressBaseCV ) | ( readCV( DccProcState.OpsModeAddressBaseCV + 1 ) << 8 ) ; + uint16_t OpsAddr = Addr & 0x3FFF ; + + if( OpsAddr != FakeOpsAddr ) + return ; + } + + // We are looking for FLAGS_MY_ADDRESS_ONLY but it does not match and it is not a Broadcast Address then return + else if( ( DccProcState.Flags & FLAGS_MY_ADDRESS_ONLY ) && ( Addr != getMyAddr() ) && ( Addr != 0 ) ) + return ; + + NODE_DBG("[dcc_processMultiFunctionMessage] CmdMasked: %x\n", CmdMasked); + switch( CmdMasked ) + { + case 0b00000000: // Decoder Control + switch( Cmd & 0b00001110 ) + { + case 0b00000000: + if( notifyDccReset && ( Cmd & 0b00000001 ) ) // Hard Reset + if( notifyDccReset) + notifyDccReset( 1 ) ; + break ; + + case 0b00000010: // Factory Test + break ; + + case 0b00000110: // Set Decoder Flags + break ; + + case 0b00001010: // Set Advanced Addressing + break ; + + case 0b00001110: // Decoder Acknowledgment + break ; + + default: // Reserved + ; + } + break ; + + case 0b00100000: // Advanced Operations + switch( Cmd & 0b00011111 ) + { + case 0b00011111: + if( notifyDccSpeed ) + { + switch( Data1 & 0b01111111 ) + { + case 0b00000000: // 0=STOP + speed = 1 ; // => 1 + break ; + + case 0b00000001: // 1=EMERGENCY_STOP + speed = 0 ; // => 0 + break ; + + default: // 2..127 + speed = (Data1 & 0b01111111) ; + } + dir = (DCC_DIRECTION) ((Data1 & 0b10000000) >> 7) ; + notifyDccSpeed( Addr, AddrType, speed, dir, SPEED_STEP_128 ) ; + } + } + break; + + case 0b01000000: + case 0b01100000: + //TODO should we cache this info in DCC_PROCESSOR_STATE.Flags ? +#ifdef NMRA_DCC_ENABLE_14_SPEED_STEP_MODE + speedSteps = (readCV( CV_29_CONFIG ) & CV29_F0_LOCATION) ? SPEED_STEP_28 : SPEED_STEP_14 ; +#else + speedSteps = SPEED_STEP_28 ; +#endif + if( notifyDccSpeed ) + { + switch( Cmd & 0b00011111 ) + { + case 0b00000000: // 0 0000 = STOP + case 0b00010000: // 1 0000 = STOP + speed = 1 ; // => 1 + break ; + + case 0b00000001: // 0 0001 = EMERGENCY STOP + case 0b00010001: // 1 0001 = EMERGENCY STOP + speed = 0 ; // => 0 + break ; + + default: +#ifdef NMRA_DCC_ENABLE_14_SPEED_STEP_MODE + if( speedSteps == SPEED_STEP_14 ) + { + speed = (Cmd & 0b00001111) ; // => 2..15 + } + else + { +#endif + speed = (((Cmd & 0b00001111) << 1 ) | ((Cmd & 0b00010000) >> 4)) - 2 ; // => 2..29 +#ifdef NMRA_DCC_ENABLE_14_SPEED_STEP_MODE + } +#endif + } + dir = (DCC_DIRECTION) ((Cmd & 0b00100000) >> 5) ; + notifyDccSpeed( Addr, AddrType, speed, dir, speedSteps ) ; + } + if( notifyDccSpeedRaw ) + notifyDccSpeedRaw(Addr, AddrType, Cmd ); + +#ifdef NMRA_DCC_ENABLE_14_SPEED_STEP_MODE + if( notifyDccFunc && (speedSteps == SPEED_STEP_14) ) + { + // function light is controlled by this package + uint8_t fn0 = (Cmd & 0b00010000) ; + notifyDccFunc( Addr, AddrType, FN_0, fn0 ) ; + } +#endif + break; + + case 0b10000000: // Function Group 0..4 + if( notifyDccFunc ) + { + // function light is controlled by this package (28 or 128 speed steps) + notifyDccFunc( Addr, AddrType, FN_0_4, Cmd & 0b00011111 ) ; + } + break; + + case 0b10100000: // Function Group 5..8 + if( notifyDccFunc) + { + if (Cmd & 0b00010000 ) + notifyDccFunc( Addr, AddrType, FN_5_8, Cmd & 0b00001111 ) ; + else + notifyDccFunc( Addr, AddrType, FN_9_12, Cmd & 0b00001111 ) ; + } + break; + + case 0b11000000: // Feature Expansion Instruction + switch(Cmd & 0b00011111) + { + case 0b00011110: + if( notifyDccFunc ) + notifyDccFunc( Addr, AddrType, FN_13_20, Data1 ) ; + break; + + case 0b00011111: + if( notifyDccFunc ) + notifyDccFunc( Addr, AddrType, FN_21_28, Data1 ) ; + break; + } + break; + + case 0b11100000: // CV Access + CVAddr = ( ( ( Cmd & 0x03 ) << 8 ) | Data1 ) + 1 ; + + processDirectOpsOperation( Cmd, CVAddr, Data2 ) ; + break; + } +} +#endif + +///////////////////////////////////////////////////////////////////////// +#ifdef NMRA_DCC_PROCESS_SERVICEMODE +void processServiceModeOperation( DCC_MSG * pDccMsg ) +{ + uint16_t CVAddr ; + uint8_t Value ; + if( pDccMsg->Size == 3) // 3 Byte Packets are for Address Only, Register and Paged Mode + { + uint8_t RegisterAddr ; + NODE_DBG("[dcc_processServiceModeOperation] 3-BytePkt\n"); + RegisterAddr = pDccMsg->Data[0] & 0x07 ; + Value = pDccMsg->Data[1] ; + + if( RegisterAddr == 5 ) + { + DccProcState.PageRegister = Value ; + } + + else + { + if( RegisterAddr == 4 ) + CVAddr = CV_29_CONFIG ; + + else if( ( RegisterAddr <= 3 ) && ( DccProcState.PageRegister > 0 ) ) + CVAddr = ( ( DccProcState.PageRegister - 1 ) * 4 ) + RegisterAddr + 1 ; + + else + CVAddr = RegisterAddr + 1 ; + + if( pDccMsg->Data[0] & 0x08 ) // Perform the Write Operation + { + if( validCV( CVAddr, 1 ) ) + { + writeCV( CVAddr, Value ); + } + } + } + } + + else if( pDccMsg->Size == 4) // 4 Byte Packets are for Direct Byte & Bit Mode + { + NODE_DBG("[dcc_processServiceModeOperation] BB-Mode\n"); + CVAddr = ( ( ( pDccMsg->Data[0] & 0x03 ) << 8 ) | pDccMsg->Data[1] ) + 1 ; + Value = pDccMsg->Data[2] ; + + processDirectOpsOperation( pDccMsg->Data[0] & 0b00001100, CVAddr, Value ) ; + } +} +#endif + +void resetServiceModeTimer(uint8_t inServiceMode) +{ + if (notifyServiceMode && inServiceMode != DccProcState.inServiceMode) + { + notifyServiceMode(inServiceMode); + } + // Set the Service Mode + DccProcState.inServiceMode = inServiceMode ; + + DccProcState.LastServiceModeMillis = inServiceMode ? system_get_time() : 0 ; + if (notifyServiceMode && inServiceMode != DccProcState.inServiceMode) + { + notifyServiceMode(inServiceMode); + } +} + +void clearDccProcState(uint8_t inServiceMode) +{ + resetServiceModeTimer( inServiceMode ) ; + + // Set the Page Register to it's default of 1 only on the first Reset + DccProcState.PageRegister = 1 ; + + // Clear the LastMsg buffer and DuplicateCount in preparation for possible CV programming + DccProcState.DuplicateCount = 0 ; + memset( &DccProcState.LastMsg, 0, sizeof( DCC_MSG ) ) ; +} + +void execDccProcessor( DCC_MSG * pDccMsg ) +{ + NODE_DBG("[dcc_execDccProcessor]\n"); + + if( ( pDccMsg->Data[0] == 0 ) && ( pDccMsg->Data[1] == 0 ) ) + { + if( notifyDccReset ) + notifyDccReset( 0 ) ; + +#ifdef NMRA_DCC_PROCESS_SERVICEMODE + // If this is the first Reset then perform some one-shot actions as we maybe about to enter service mode + if( DccProcState.inServiceMode ) + resetServiceModeTimer( 1 ) ; + else + clearDccProcState( 1 ); +#endif + } + + else + { +#ifdef NMRA_DCC_PROCESS_SERVICEMODE + if( DccProcState.inServiceMode && ( pDccMsg->Data[0] >= 112 ) && ( pDccMsg->Data[0] < 128 ) ) + { + resetServiceModeTimer( 1 ) ; + + if( memcmp( pDccMsg, &DccProcState.LastMsg, sizeof( DCC_MSG ) ) ) + { + DccProcState.DuplicateCount = 0 ; + memcpy( &DccProcState.LastMsg, pDccMsg, sizeof( DCC_MSG ) ) ; + } + // Wait until you see 2 identicle packets before acting on a Service Mode Packet + else + { + DccProcState.DuplicateCount++ ; + processServiceModeOperation( pDccMsg ) ; + } + } + + else + { + if( DccProcState.inServiceMode ) + clearDccProcState( 0 ); +#endif + + // Idle Packet + if( ( pDccMsg->Data[0] == 0b11111111 ) && ( pDccMsg->Data[1] == 0 ) ) + { + if( notifyDccIdle ) + notifyDccIdle() ; + } + +#ifdef NMRA_DCC_PROCESS_MULTIFUNCTION + // Multi Function Decoders (7-bit address) + else if( pDccMsg->Data[0] < 128 ) + processMultiFunctionMessage( pDccMsg->Data[0], DCC_ADDR_SHORT, pDccMsg->Data[1], pDccMsg->Data[2], pDccMsg->Data[3] ) ; + // Basic Accessory Decoders (9-bit) & Extended Accessory Decoders (11-bit) + else if( pDccMsg->Data[0] < 192 ) +#else + else if( ( pDccMsg->Data[0] >= 128 ) && ( pDccMsg->Data[0] < 192 ) ) +#endif + { + if( DccProcState.Flags & FLAGS_DCC_ACCESSORY_DECODER ) + { + int16_t BoardAddress ; + int16_t OutputAddress ; + uint8_t TurnoutPairIndex ; + +#ifdef NODE_DEBUG + // SerialPrintPacketHex(F( "eDP: AccCmd: "), pDccMsg); +#endif + + BoardAddress = ( ( (~pDccMsg->Data[1]) & 0b01110000 ) << 2 ) | ( pDccMsg->Data[0] & 0b00111111 ) ; + TurnoutPairIndex = (pDccMsg->Data[1] & 0b00000110) >> 1; + NODE_DBG("[dcc_execDccProcessor] eDP: BAddr:%d, Index:%d\n", BoardAddress, TurnoutPairIndex); + + // First check for Legacy Accessory Decoder Configuration Variable Access Instruction + // as it's got a different format to the others + if((pDccMsg->Size == 5) && ((pDccMsg->Data[1] & 0b10001100) == 0b00001100)) + { + NODE_DBG( "eDP: Legacy Accessory Decoder CV Access Command"); + // Check if this command is for our address or the broadcast address + if((BoardAddress != getMyAddr()) && ( BoardAddress < 511 )) + { + NODE_DBG("[dcc_execDccProcessor] eDP: Board Address Not Matched\n"); + return; + } + + uint16_t cvAddress = ((pDccMsg->Data[1] & 0b00000011) << 8) + pDccMsg->Data[2] + 1; + uint8_t cvValue = pDccMsg->Data[3]; + NODE_DBG("[dcc_execDccProcessor] eDP: CV:%d Value:%d\n", cvAddress, cvValue ); + if(validCV( cvAddress, 1 )) + writeCV(cvAddress, cvValue); + return; + } + + + OutputAddress = (((BoardAddress - 1) << 2 ) | TurnoutPairIndex) + 1 ; //decoder output addresses start with 1, packet address range starts with 0 + // ( according to NMRA 9.2.2 ) + NODE_DBG("[dcc_execDccProcessor] eDP: OAddr:%d\n", OutputAddress); + + if( DccProcState.inAccDecDCCAddrNextReceivedMode) + { + if( DccProcState.Flags & FLAGS_OUTPUT_ADDRESS_MODE ) + { + NODE_DBG("[dcc_execDccProcessor] eDP: Set OAddr:%d\n", OutputAddress); + //uint16_t storedOutputAddress = OutputAddress + 1; // The value stored in CV1 & 9 for Output Addressing Mode is + 1 + writeCV(CV_ACCESSORY_DECODER_ADDRESS_LSB, (uint8_t)(OutputAddress % 256)); + writeCV(CV_ACCESSORY_DECODER_ADDRESS_MSB, (uint8_t)(OutputAddress / 256)); + + if( notifyDccAccOutputAddrSet ) + notifyDccAccOutputAddrSet(OutputAddress); + } + else + { + NODE_DBG("[dcc_execDccProcessor] eDP: Set BAddr:%d\n", BoardAddress); + writeCV(CV_ACCESSORY_DECODER_ADDRESS_LSB, (uint8_t)(BoardAddress % 64)); + writeCV(CV_ACCESSORY_DECODER_ADDRESS_MSB, (uint8_t)(BoardAddress / 64)); + + if( notifyDccAccBoardAddrSet ) + notifyDccAccBoardAddrSet(BoardAddress); + } + + DccProcState.inAccDecDCCAddrNextReceivedMode = 0; // Reset the mode now that we have set the address + } + + // If we're filtering addresses, does the address match our address or is it a broadcast address? If NOT then return + if( DccProcState.Flags & FLAGS_MY_ADDRESS_ONLY ) + { + if( DccProcState.Flags & FLAGS_OUTPUT_ADDRESS_MODE ) { + NODE_DBG("[dcc_execDccProcessor] AddrChk: OAddr:%d, BAddr:%d, myAddr:%d Chk=%d\n", OutputAddress, BoardAddress, getMyAddr(), OutputAddress != getMyAddr() ); + if ( OutputAddress != getMyAddr() && OutputAddress < 2045 ) { + NODE_DBG("[dcc_execDccProcessor] eDP: OAddr:%d, myAddr:%d - no match\n", OutputAddress, getMyAddr() ); + return; + } + } else { + if( ( BoardAddress != getMyAddr() ) && ( BoardAddress < 511 ) ) { + NODE_DBG("[dcc_execDccProcessor] eDP: BAddr:%d, myAddr:%d - no match\n", BoardAddress, getMyAddr() ); + return; + } + } + NODE_DBG("[dcc_execDccProcessor] eDP: Address Matched\n"); + } + + + if((pDccMsg->Size == 4) && ((pDccMsg->Data[1] & 0b10001001) == 1)) // Extended Accessory Decoder Control Packet Format + { + // According to the NMRA Dcc Spec the Signal State should only use the lower 5 Bits, + // however some manufacturers seem to allow/use all 8 bits, so we'll relax that constraint for now + uint8_t state = pDccMsg->Data[2] ; + NODE_DBG("[dcc_execDccProcessor] eDP: OAddr:%d Extended State:%0X\n", OutputAddress, state); + if( notifyDccSigOutputState ) + notifyDccSigOutputState(OutputAddress, state); + } + + else if(pDccMsg->Size == 3) // Basic Accessory Decoder Packet Format + { + uint8_t direction = pDccMsg->Data[1] & 0b00000001; + uint8_t outputPower = (pDccMsg->Data[1] & 0b00001000) >> 3; + + if( DccProcState.Flags & FLAGS_OUTPUT_ADDRESS_MODE ) + { + NODE_DBG("[dcc_execDccProcessor] eDP: OAddr:%d Turnout Dir:%d Output Power:%d\n", OutputAddress, direction, outputPower); + if( notifyDccAccTurnoutOutput ) + notifyDccAccTurnoutOutput( OutputAddress, direction, outputPower ); + } + else + { + NODE_DBG("[dcc_execDccProcessor] eDP: Turnout Pair Index:%d Dir:%d Output Power: %d\n", TurnoutPairIndex, direction, outputPower); + if( notifyDccAccTurnoutBoard ) + notifyDccAccTurnoutBoard( BoardAddress, TurnoutPairIndex, direction, outputPower ); + } + } + else if(pDccMsg->Size == 6) // Accessory Decoder OPS Mode Programming + { + NODE_DBG("[dcc_execDccProcessor] eDP: OPS Mode CV Programming Command\n"); + // Check for unsupported OPS Mode Addressing mode + if(((pDccMsg->Data[1] & 0b10001001) != 1) && ((pDccMsg->Data[1] & 0b10001111) != 0x80)) + { + NODE_DBG("[dcc_execDccProcessor] eDP: Unsupported OPS Mode CV Addressing Mode\n"); + return; + } + + // Check if this command is for our address or the broadcast address + if(DccProcState.Flags & FLAGS_OUTPUT_ADDRESS_MODE) + { + NODE_DBG("[dcc_execDccProcessor] eDP: Check Output Address:%d\n", OutputAddress); + if((OutputAddress != getMyAddr()) && ( OutputAddress < 2045 )) + { + NODE_DBG("[dcc_execDccProcessor] eDP: Output Address Not Matched\n"); + return; + } + } + else + { + NODE_DBG("[dcc_execDccProcessor] eDP: Check Board Address:%d\n", BoardAddress); + if((BoardAddress != getMyAddr()) && ( BoardAddress < 511 )) + { + NODE_DBG("[dcc_execDccProcessor] eDP: Board Address Not Matched\n"); + return; + } + } + + uint16_t cvAddress = ((pDccMsg->Data[2] & 0b00000011) << 8) + pDccMsg->Data[3] + 1; + uint8_t cvValue = pDccMsg->Data[4]; + + OpsInstructionType insType = (OpsInstructionType)((pDccMsg->Data[2] & 0b00001100) >> 2) ; + + NODE_DBG("[dcc_execDccProcessor] eDP: OPS Mode Instruction:%d\n", insType); + switch(insType) + { + case OPS_INS_RESERVED: + case OPS_INS_VERIFY_BYTE: + NODE_DBG("[dcc_execDccProcessor] eDP: Unsupported OPS Mode Instruction:%d\n", insType); + break; // We only support Write Byte or Bit Manipulation + + case OPS_INS_WRITE_BYTE: + NODE_DBG("[dcc_execDccProcessor] eDP: CV:%d Value:%d\n", cvAddress, cvValue); + if(validCV( cvAddress, 1 )) + writeCV(cvAddress, cvValue); + break; + + // 111CDBBB + // Where BBB represents the bit position within the CV, + // D contains the value of the bit to be verified or written, + // and C describes whether the operation is a verify bit or a write bit operation. + // C = "1" WRITE BIT + // C = "0" VERIFY BIT + case OPS_INS_BIT_MANIPULATION: + // Make sure its a Write Bit Manipulation + if((cvValue & 0b00010000) && validCV(cvAddress, 1 )) + { + uint8_t currentValue = readCV(cvAddress); + uint8_t newValueMask = 1 << (cvValue & 0b00000111); + if(cvValue & 0b00001000) + writeCV(cvAddress, currentValue | newValueMask); + else + writeCV(cvAddress, currentValue & ~newValueMask); + } + break; + } + } + } + } + +#ifdef NMRA_DCC_PROCESS_MULTIFUNCTION + // Multi Function Decoders (14-bit address) + else if( pDccMsg->Data[0] < 232 ) + { + uint16_t Address ; + Address = ( ( pDccMsg->Data[0] - 192 ) << 8 ) | pDccMsg->Data[1]; + //TODO should we convert Address to 1 .. 10239 ? + processMultiFunctionMessage( Address, DCC_ADDR_LONG, pDccMsg->Data[2], pDccMsg->Data[3], pDccMsg->Data[4] ) ; + } +#endif +#ifdef NMRA_DCC_PROCESS_SERVICEMODE + } +#endif + } +} + +static void process (os_param_t param, uint8_t prio) +{ + // !!!!!! - this will not happen as we call process task only when data is ready + // if( DccProcState.inServiceMode ) + // { + // if( (system_get_time() - DccProcState.LastServiceModeMillis ) > 20L ) + // { + // clearDccProcState( 0 ) ; + // } + // } + // !!!!!! + + // We need to do this check with interrupts disabled + //SET_TP4; + Msg = DccRx.PacketCopy ; + + #ifdef DCC_DBGVAR + countOf.Tel++; + #endif + + uint8_t xorValue = 0 ; + + for(uint8_t i = 0; i < DccRx.PacketCopy.Size; i++) + xorValue ^= DccRx.PacketCopy.Data[i]; + if(xorValue) { + #ifdef DCC_DBGVAR + NODE_DBG("[dcc_process] Cerr\n"); + NODE_DBG("[dcc_process] Data dump:"); + for(uint8_t i = 0; i < DccRx.PacketCopy.Size; i++) + NODE_DBG(" %x", DccRx.PacketCopy.Data[i]); + NODE_DBG("\n"); + countOf.Err++; + #endif + return;// 0 ; + } else { + NODE_DBG("[dcc_process] Size: %d\tPreambleBits: %d\t%d, %d, %d, %d, %d, %d\n", + Msg.Size, Msg.PreambleBits, Msg.Data[0], Msg.Data[1], Msg.Data[2], Msg.Data[3], Msg.Data[4], Msg.Data[5]); + execDccProcessor( &Msg ); + } + + return;// 1 ; +} + +void dcc_setup(uint8_t pin, uint8_t ManufacturerId, uint8_t VersionId, uint8_t Flags, uint8_t OpsModeAddressBaseCV) +{ + NODE_DBG("[dcc_setup]\n"); + // Clear all the static member variables + memset( &DccRx, 0, sizeof( DccRx) ); + + MODE_TP1; // only for debugging and timing measurement + MODE_TP2; + MODE_TP3; + MODE_TP4; + CLR_TP1; + CLR_TP2; + CLR_TP3; + CLR_TP4; + + bitMax = MAX_ONEBITFULL; + bitMin = MIN_ONEBITFULL; + DccProcState.Flags = Flags ; + DccProcState.OpsModeAddressBaseCV = OpsModeAddressBaseCV ; + DccProcState.myDccAddress = -1; + DccProcState.inAccDecDCCAddrNextReceivedMode = 0; + + ISREdge = GPIO_PIN_INTR_POSEDGE; + + DccProcState.IntPin = pin; + DccProcState.IntBitmask = 1 << pin_num[pin]; + + + platform_gpio_mode(pin, PLATFORM_GPIO_INT, PLATFORM_GPIO_PULLUP); + NODE_DBG("[dcc_setup] platform_gpio_register_intr_hook - pin: %d, mask: %d\n", DccProcState.IntPin, DccProcState.IntBitmask); + platform_gpio_register_intr_hook(DccProcState.IntBitmask, InterruptHandler); + + gpio_pin_intr_state_set(GPIO_ID_PIN(pin_num[pin]), GPIO_PIN_INTR_POSEDGE); + + // Set the Bits that control Multifunction or Accessory behaviour + // and if the Accessory decoder optionally handles Output Addressing + // we need to peal off the top two bits + writeCV( CV_29_CONFIG, ( readCV( CV_29_CONFIG ) & ~FLAGS_CV29_BITS ) | (Flags & FLAGS_CV29_BITS) ) ; //!!!!! + + uint8_t doAutoFactoryDefault = 0; + if((Flags & FLAGS_AUTO_FACTORY_DEFAULT) && (readCV(CV_VERSION_ID) == 255) && (readCV(CV_MANUFACTURER_ID) == 255)) + doAutoFactoryDefault = 1; + + writeCV( CV_VERSION_ID, VersionId ) ; + writeCV( CV_MANUFACTURER_ID, ManufacturerId ) ; + + clearDccProcState( 0 ); + + if(notifyCVResetFactoryDefault && doAutoFactoryDefault) + notifyCVResetFactoryDefault(); +} + +void dcc_close() +{ + NODE_DBG("[dcc_close]\n"); + platform_gpio_mode(DccProcState.IntPin, PLATFORM_GPIO_INPUT, PLATFORM_GPIO_PULLUP); +} + +void dcc_init() +{ + NODE_DBG("[dcc_init]\n"); + DataReady_taskid = task_get_id((task_callback_t) process); +} \ No newline at end of file diff --git a/app/include/driver/NmraDcc.h b/app/include/driver/NmraDcc.h new file mode 100644 index 00000000..b864b259 --- /dev/null +++ b/app/include/driver/NmraDcc.h @@ -0,0 +1,479 @@ +//------------------------------------------------------------------------ +// +// Model Railroading with Arduino - NmraDcc.h +// +// Copyright (c) 2008 - 2018 Alex Shepherd +// +// This source file is subject of the GNU general public license 2, +// that is available at the world-wide-web at +// http://www.gnu.org/licenses/gpl.txt +// +//------------------------------------------------------------------------ +// +// file: NmraDcc.h +// author: Alex Shepherd +// webpage: http://mrrwa.org/ +// history: 2008-03-20 Initial Version +// 2011-06-26 Migrated into Arduino library from OpenDCC codebase +// 2014 Added getAddr to NmraDcc Geoff Bunza +// 2015-11-06 Martin Pischky (martin@pischky.de): +// Experimental Version to support 14 speed steps +// and new signature of notifyDccSpeed and notifyDccFunc +// 2017-11-29 Ken West (kgw4449@gmail.com): +// Added method and callback headers. +// +//------------------------------------------------------------------------ +// +// purpose: Provide a simplified interface to decode NMRA DCC packets +// and build DCC MutliFunction and Stationary Decoders +// +//------------------------------------------------------------------------ + +// NodeMCU Lua port by @voborsky + +// #define NODE_DEBUG +// #define DCC_DEBUG +// #define DCC_DBGVAR + +// Uncomment the following Line to Enable Service Mode CV Programming +#define NMRA_DCC_PROCESS_SERVICEMODE + +// Uncomment the following line to Enable MultiFunction Decoder Operations +#define NMRA_DCC_PROCESS_MULTIFUNCTION + +// #ifndef NMRADCC_IS_IN +// #define NMRADCC_IS_IN + +#define NMRADCC_VERSION 201 // Version 2.0.1 + +#define MAX_DCC_MESSAGE_LEN 6 // including XOR-Byte + +typedef struct +{ + uint8_t Size ; + uint8_t PreambleBits ; + uint8_t Data[MAX_DCC_MESSAGE_LEN] ; +} DCC_MSG ; + +//-------------------------------------------------------------------------- +// This section contains the NMRA Assigned DCC Manufacturer Id Codes that +// are used in projects +// +// This value is to be used for CV8 +//-------------------------------------------------------------------------- + +#define MAN_ID_JMRI 0x12 +#define MAN_ID_DIY 0x0D +#define MAN_ID_SILICON_RAILWAY 0x21 + +//-------------------------------------------------------------------------- +// This section contains the Product/Version Id Codes for projects +// +// This value is to be used for CV7 +// +// NOTE: Each Product/Version Id Code needs to be UNIQUE for that particular +// the DCC Manufacturer Id Code +//-------------------------------------------------------------------------- + +// Product/Version Id Codes allocated under: MAN_ID_JMRI + +// Product/Version Id Codes allocated under: MAN_ID_DIY + +// Standard CV Addresses +#define CV_ACCESSORY_DECODER_ADDRESS_LSB 1 +#define CV_ACCESSORY_DECODER_ADDRESS_MSB 9 + +#define CV_MULTIFUNCTION_PRIMARY_ADDRESS 1 +#define CV_MULTIFUNCTION_EXTENDED_ADDRESS_MSB 17 +#define CV_MULTIFUNCTION_EXTENDED_ADDRESS_LSB 18 + +#define CV_VERSION_ID 7 +#define CV_MANUFACTURER_ID 8 +#define CV_29_CONFIG 29 + +typedef enum { + CV29_LOCO_DIR = 0b00000001, /** bit 0: Locomotive Direction: "0" = normal, "1" = reversed */ + CV29_F0_LOCATION = 0b00000010, /** bit 1: F0 location: "0" = bit 4 in Speed and Direction instructions, "1" = bit 4 in function group one instruction */ + CV29_APS = 0b00000100, /** bit 2: Alternate Power Source (APS) "0" = NMRA Digital only, "1" = Alternate power source set by CV12 */ + CV29_ADV_ACK = 0b00001000, /** bit 3: ACK, Advanced Acknowledge mode enabled if 1, disabled if 0 */ + CV29_SPEED_TABLE_ENABLE = 0b00010000, /** bit 4: STE, Speed Table Enable, "0" = values in CVs 2, 4 and 6, "1" = Custom table selected by CV 25 */ + CV29_EXT_ADDRESSING = 0b00100000, /** bit 5: "0" = one byte addressing, "1" = two byte addressing */ + CV29_OUTPUT_ADDRESS_MODE = 0b01000000, /** bit 6: "0" = Decoder Address Mode "1" = Output Address Mode */ + CV29_ACCESSORY_DECODER = 0b10000000, /** bit 7: "0" = Multi-Function Decoder Mode "1" = Accessory Decoder Mode */ +} CV_29_BITS; + +typedef enum { +#ifdef NMRA_DCC_ENABLE_14_SPEED_STEP_MODE + SPEED_STEP_14 = 15, /**< ESTOP=0, 1 to 15 */ +#endif + SPEED_STEP_28 = 29, /**< ESTOP=0, 1 to 29 */ + SPEED_STEP_128 = 127 /**< ESTOP=0, 1 to 127 */ +} DCC_SPEED_STEPS; + +typedef enum { + DCC_DIR_REV = 0, /** The locomotive to go in the reverse direction */ + DCC_DIR_FWD = 1, /** The locomotive should move in the forward direction */ +} DCC_DIRECTION; + +typedef enum { + DCC_ADDR_SHORT, /** Short address is used. The range is 0 to 127. */ + DCC_ADDR_LONG, /** Long Address is used. The range is 1 to 10239 */ +} DCC_ADDR_TYPE; + +typedef enum +{ + FN_0_4 = 1, + FN_5_8, + FN_9_12, + FN_13_20, + FN_21_28, +#ifdef NMRA_DCC_ENABLE_14_SPEED_STEP_MODE + FN_0 /** function light is controlled by base line package (14 speed steps) */ +#endif +} FN_GROUP; + +#define FN_BIT_00 0x10 +#define FN_BIT_01 0x01 +#define FN_BIT_02 0x02 +#define FN_BIT_03 0x04 +#define FN_BIT_04 0x08 + +#define FN_BIT_05 0x01 +#define FN_BIT_06 0x02 +#define FN_BIT_07 0x04 +#define FN_BIT_08 0x08 + +#define FN_BIT_09 0x01 +#define FN_BIT_10 0x02 +#define FN_BIT_11 0x04 +#define FN_BIT_12 0x08 + +#define FN_BIT_13 0x01 +#define FN_BIT_14 0x02 +#define FN_BIT_15 0x04 +#define FN_BIT_16 0x08 +#define FN_BIT_17 0x10 +#define FN_BIT_18 0x20 +#define FN_BIT_19 0x40 +#define FN_BIT_20 0x80 + +#define FN_BIT_21 0x01 +#define FN_BIT_22 0x02 +#define FN_BIT_23 0x04 +#define FN_BIT_24 0x08 +#define FN_BIT_25 0x10 +#define FN_BIT_26 0x20 +#define FN_BIT_27 0x40 +#define FN_BIT_28 0x80 + +#ifdef DCC_DBGVAR +typedef struct countOf_t { + unsigned long Tel; + unsigned long Err; +}countOf_t ; + +countOf_t countOf; +#endif + +// Flag values to be logically ORed together and passed into the init() method +#define FLAGS_MY_ADDRESS_ONLY 0x01 // Only process DCC Packets with My Address +#define FLAGS_AUTO_FACTORY_DEFAULT 0x02 // Call notifyCVResetFactoryDefault() if CV 7 & 8 == 255 +#define FLAGS_SETCV_CALLED 0x10 // only used internally !! +#define FLAGS_OUTPUT_ADDRESS_MODE 0x40 // CV 29/541 bit 6 +#define FLAGS_DCC_ACCESSORY_DECODER 0x80 // CV 29/541 bit 7 + +// Flag Bits that are cloned from CV29 relating the DCC Accessory Decoder +#define FLAGS_CV29_BITS (FLAGS_OUTPUT_ADDRESS_MODE | FLAGS_DCC_ACCESSORY_DECODER) + +#define DCC_RESET 1 +#define DCC_IDLE 2 +#define DCC_SPEED 3 +#define DCC_SPEED_RAW 4 +#define DCC_FUNC 5 +#define DCC_TURNOUT 6 +#define DCC_ACCESSORY 7 +#define DCC_RAW 8 +#define DCC_SERVICEMODE 9 + +#define CV_VALID 10 +#define CV_READ 11 +#define CV_WRITE 12 +#define CV_RESET 13 + + +void dcc_setup(uint8_t pin, uint8_t ManufacturerId, uint8_t VersionId, uint8_t Flags, uint8_t OpsModeAddressBaseCV ); + + +void dcc_close(); + +void dcc_init(); + + +/************************************************************************************ + Call-back functions +************************************************************************************/ + +/*+ + * notifyDccReset(uint8_t hardReset) Callback for a DCC reset command. + * + * Inputs: + * hardReset - 0 normal reset command. + * 1 hard reset command. + * + * Returns: + * None + */ +extern void notifyDccReset(uint8_t hardReset ) __attribute__ ((weak)); + +/*+ + * notifyDccIdle() Callback for a DCC idle command. + * + * Inputs: + * None + * + * Returns: + * None + */ +extern void notifyDccIdle(void) __attribute__ ((weak)); + + +/*+ + * notifyDccSpeed() Callback for a multifunction decoder speed command. + * The received speed and direction are unpacked to separate values. + * + * Inputs: + * Addr - Active decoder address. + * AddrType - DCC_ADDR_SHORT or DCC_ADDR_LONG. + * Speed - Decoder speed. 0 = Emergency stop + * 1 = Regular stop + * 2 to SpeedSteps = Speed step 1 to max. + * Dir - DCC_DIR_REV or DCC_DIR_FWD + * SpeedSteps - Highest speed, SPEED_STEP_14 = 15 + * SPEED_STEP_28 = 29 + * SPEED_STEP_128 = 127 + * + * Returns: + * None + */ +extern void notifyDccSpeed( uint16_t Addr, DCC_ADDR_TYPE AddrType, uint8_t Speed, DCC_DIRECTION Dir, DCC_SPEED_STEPS SpeedSteps ) __attribute__ ((weak)); + +/*+ + * notifyDccSpeedRaw() Callback for a multifunction decoder speed command. + * The value in Raw is the unpacked speed command. + * + * Inputs: + * Addr - Active decoder address. + * AddrType - DCC_ADDR_SHORT or DCC_ADDR_LONG. + * Raw - Raw decoder speed command. + * + * Returns: + * None + */ +extern void notifyDccSpeedRaw( uint16_t Addr, DCC_ADDR_TYPE AddrType, uint8_t Raw) __attribute__ ((weak)); + +/*+ + * notifyDccFunc() Callback for a multifunction decoder function command. + * + * Inputs: + * Addr - Active decoder address. + * AddrType - DCC_ADDR_SHORT or DCC_ADDR_LONG. + * FuncGrp - Function group. FN_0 - 14 speed step headlight function. + * Mask FN_BIT_00. + * FN_0_4 - Functions 0 to 4. Mask FN_BIT_00 - FN_BIT_04 + * FN_5_8 - Functions 5 to 8. Mask FN_BIT_05 - FN_BIT_08 + * FN_9_12 - Functions 9 to 12. Mask FN_BIT_09 - FN_BIT_12 + * FN_13_20 - Functions 13 to 20. Mask FN_BIT_13 - FN_BIT_20 + * FN_21_28 - Functions 21 to 28. Mask FN_BIT_21 - FN_BIT_28 + * FuncState - Function state. Bitmask where active functions have a 1 at that bit. + * You must & FuncState with the appropriate + * FN_BIT_nn value to isolate a given bit. + * + * Returns: + * None + */ +extern void notifyDccFunc( uint16_t Addr, DCC_ADDR_TYPE AddrType, FN_GROUP FuncGrp, uint8_t FuncState) __attribute__ ((weak)); + +/*+ + * notifyDccAccTurnoutBoard() Board oriented callback for a turnout accessory decoder. + * Most useful when CV29_OUTPUT_ADDRESS_MODE is not set. + * Decoders of this type have 4 paired turnout outputs per board. + * OutputPower is 1 if the power is on, and 0 otherwise. + * + * Inputs: + * BoardAddr - Per board address. Equivalent to CV 1 LSB & CV 9 MSB. + * OutputPair - Output pair number. It has a range of 0 to 3. + * Equivalent to upper 2 bits of the 3 DDD bits in the accessory packet. + * Direction - Turnout direction. It has a value of 0 or 1. + * It is equivalent to bit 0 of the 3 DDD bits in the accessory packet. + * OutputPower - Output On/Off. Equivalent to packet C bit. It has these values: + * 0 - Output pair is off. + * 1 - Output pair is on. + * + * Returns: + * None + */ + +extern void notifyDccAccTurnoutBoard( uint16_t BoardAddr, uint8_t OutputPair, uint8_t Direction, uint8_t OutputPower ) __attribute__ ((weak)); +/*+ + * notifyDccAccTurnoutOutput() Output oriented callback for a turnout accessory decoder. + * Most useful when CV29_OUTPUT_ADDRESS_MODE is not set. + * Decoders of this type have 4 paired turnout outputs per board. + * OutputPower is 1 if the power is on, and 0 otherwise. + * + * Inputs: + * Addr - Per output address. There will be 4 Addr addresses + * per board for a standard accessory decoder with 4 output pairs. + * Direction - Turnout direction. It has a value of 0 or 1. + * Equivalent to bit 0 of the 3 DDD bits in the accessory packet. + * OutputPower - Output On/Off. Equivalent to packet C bit. It has these values: + * 0 - Output is off. + * 1 - Output is on. + * + * Returns: + * None + */ +extern void notifyDccAccTurnoutOutput( uint16_t Addr, uint8_t Direction, uint8_t OutputPower ) __attribute__ ((weak)); + +/*+ + * notifyDccAccBoardAddrSet() Board oriented callback for a turnout accessory decoder. + * This notification is when a new Board Address is set to the + * address of the next DCC Turnout Packet that is received + * + * This is enabled via the setAccDecDCCAddrNextReceived() method above + * + * Inputs: + * BoardAddr - Per board address. Equivalent to CV 1 LSB & CV 9 MSB. + * per board for a standard accessory decoder with 4 output pairs. + * + * Returns: + * None + */ +extern void notifyDccAccBoardAddrSet( uint16_t BoardAddr) __attribute__ ((weak)); + +/*+ + * notifyDccAccOutputAddrSet() Output oriented callback for a turnout accessory decoder. + * This notification is when a new Output Address is set to the + * address of the next DCC Turnout Packet that is received + * + * This is enabled via the setAccDecDCCAddrNextReceived() method above + * + * Inputs: + * Addr - Per output address. There will be 4 Addr addresses + * per board for a standard accessory decoder with 4 output pairs. + * + * Returns: + * None + */ +extern void notifyDccAccOutputAddrSet( uint16_t Addr) __attribute__ ((weak)); + +/*+ + * notifyDccSigOutputState() Callback for a signal aspect accessory decoder. + * Defined in S-9.2.1 as the Extended Accessory Decoder Control Packet. + * + * Inputs: + * Addr - Decoder address. + * State - 6 bit command equivalent to S-9.2.1 00XXXXXX. + * + * Returns: + * None + */ +extern void notifyDccSigOutputState( uint16_t Addr, uint8_t State) __attribute__ ((weak)); + +/*+ + * notifyDccMsg() Raw DCC packet callback. + * Called with raw DCC packet bytes. + * + * Inputs: + * Msg - Pointer to DCC_MSG structure. The values are: + * Msg->Size - Number of Data bytes in the packet. + * Msg->PreambleBits - Number of preamble bits in the packet. + * Msg->Data[] - Array of data bytes in the packet. + * + * Returns: + * None + */ +extern void notifyDccMsg( DCC_MSG * Msg ) __attribute__ ((weak)); + +/*+ + * notifyCVValid() Callback to determine if a given CV is valid. + * This is called when the library needs to determine + * if a CV is valid. Note: If defined, this callback + * MUST determine if a CV is valid and return the + * appropriate value. If this callback is not defined, + * the library will determine validity. + * + * Inputs: + * CV - CV number. + * Writable - 1 for CV writes. 0 for CV reads. + * + * Returns: + * 1 - CV is valid. + * 0 - CV is not valid. + */ +extern uint8_t notifyCVValid( uint16_t CV, uint8_t Writable ) __attribute__ ((weak)); + +/*+ + * notifyCVRead() Callback to read a CV. + * This is called when the library needs to read + * a CV. Note: If defined, this callback + * MUST return the value of the CV. + * If this callback is not defined, + * the library will read the CV from EEPROM. + * + * Inputs: + * CV - CV number. + * + * Returns: + * Value - Value of the CV. + */ +extern uint8_t notifyCVRead( uint16_t CV) __attribute__ ((weak)); + +/*+ + * notifyCVWrite() Callback to write a value to a CV. + * This is called when the library needs to write + * a CV. Note: If defined, this callback + * MUST write the Value to the CV and return the value of the CV. + * If this callback is not defined, + * the library will read the CV from EEPROM. + * + * Inputs: + * CV - CV number. + * Value - Value of the CV. + * + * Returns: + * Value - Value of the CV. + */ +extern uint8_t notifyCVWrite( uint16_t CV, uint8_t Value) __attribute__ ((weak)); + +/*+ + * notifyCVResetFactoryDefault() Called when CVs must be reset. + * This is called when CVs must be reset + * to their factory defaults. This callback + * should write the factory default value of + * relevent CVs using the setCV() method. + * setCV() must not block whens this is called. + * Test with isSetCVReady() prior to calling setCV() + * + * Inputs: + * None + * * + * Returns: + * None + */ +extern void notifyCVResetFactoryDefault(void) __attribute__ ((weak)); + +/*+ + * notifyServiceMode(bool) Called when state of 'inServiceMode' changes + * + * Inputs: + * bool state of inServiceMode + * * + * Returns: + * None + */ +extern void notifyServiceMode(bool) __attribute__ ((weak)); + +// Deprecated, only for backward compatibility with version 1.4.2. +// Don't use in new designs. These functions may be dropped in future versions +// extern void notifyDccAccState( uint16_t Addr, uint16_t BoardAddr, uint8_t OutputAddr, uint8_t State ) __attribute__ ((weak)); +// extern void notifyDccSigState( uint16_t Addr, uint8_t OutputIndex, uint8_t State) __attribute__ ((weak)); diff --git a/app/include/user_modules.h b/app/include/user_modules.h index 79853254..c04a6bdb 100644 --- a/app/include/user_modules.h +++ b/app/include/user_modules.h @@ -21,6 +21,7 @@ //#define LUA_USE_MODULES_COLOR_UTILS //#define LUA_USE_MODULES_CRON //#define LUA_USE_MODULES_CRYPTO +//#define LUA_USE_MODULES_DCC #define LUA_USE_MODULES_DHT //#define LUA_USE_MODULES_ENCODER //#define LUA_USE_MODULES_ENDUSER_SETUP // USE_DNS in dhcpserver.h needs to be enabled for this module to work. diff --git a/app/modules/dcc.c b/app/modules/dcc.c new file mode 100644 index 00000000..c979880f --- /dev/null +++ b/app/modules/dcc.c @@ -0,0 +1,290 @@ +// NodeMCU Lua port by @voborsky +// Module for handling NMRA DCC protocol +// #define NODE_DEBUG + +#include "module.h" +#include "lauxlib.h" +#include "platform.h" +#include "driver/NmraDcc.h" + +#ifdef LUA_USE_MODULES_DCC +#if !defined(GPIO_INTERRUPT_ENABLE) || !defined(GPIO_INTERRUPT_HOOK_ENABLE) +#error Must have GPIO_INTERRUPT and GPIO_INTERRUPT_HOOK if using DCC module +#endif +#endif + +#define TYPE "Type" +#define OPERATION "Operation" + +static inline void register_lua_cb(lua_State* L,int* cb_ref){ + int ref=luaL_ref(L, LUA_REGISTRYINDEX); + if( *cb_ref != LUA_NOREF){ + luaL_unref(L, LUA_REGISTRYINDEX, *cb_ref); + } + *cb_ref = ref; +} + +static inline void unregister_lua_cb(lua_State* L, int* cb_ref){ + if(*cb_ref != LUA_NOREF){ + luaL_unref(L, LUA_REGISTRYINDEX, *cb_ref); + *cb_ref = LUA_NOREF; + } +} + +static int notify_cb = LUA_NOREF; +static int CV_cb = LUA_NOREF; + +// DCC commands + +void cbInit(lua_State* L, uint16_t command) { + if(notify_cb == LUA_NOREF) + return; + lua_rawgeti(L, LUA_REGISTRYINDEX, notify_cb); + lua_pushinteger(L, command); + lua_newtable(L); +} + +void cbAddFieldInteger(lua_State* L, uint16_t Value, char *Field) { + lua_pushinteger(L, Value); + lua_setfield(L, -2, Field); +} + +void notifyDccReset(uint8_t hardReset ) { + lua_State* L = lua_getstate(); + cbInit(L, DCC_RESET); + cbAddFieldInteger(L, hardReset, "hardReset"); + lua_call(L, 2, 0); +} + +void notifyDccIdle(void) { + lua_State* L = lua_getstate(); + cbInit(L, DCC_IDLE); + lua_call(L, 2, 0); +} + +void notifyDccSpeed( uint16_t Addr, DCC_ADDR_TYPE AddrType, uint8_t Speed, DCC_DIRECTION Dir, DCC_SPEED_STEPS SpeedSteps ) { + lua_State* L = lua_getstate(); + cbInit(L, DCC_SPEED); + cbAddFieldInteger(L, Addr, "Addr"); + cbAddFieldInteger(L, AddrType, "AddrType"); + cbAddFieldInteger(L, Speed, "Speed"); + cbAddFieldInteger(L, Dir, "Dir"); + cbAddFieldInteger(L, SpeedSteps, "SpeedSteps"); + lua_call(L, 2, 0); +} + +void notifyDccSpeedRaw( uint16_t Addr, DCC_ADDR_TYPE AddrType, uint8_t Raw) { + lua_State* L = lua_getstate(); + cbInit(L, DCC_SPEED_RAW); + cbAddFieldInteger(L, Addr, "Addr"); + cbAddFieldInteger(L, AddrType, "AddrType"); + cbAddFieldInteger(L, Raw, "Raw"); + lua_call(L, 2, 0); +} + +void notifyDccFunc( uint16_t Addr, DCC_ADDR_TYPE AddrType, FN_GROUP FuncGrp, uint8_t FuncState) { + lua_State* L = lua_getstate(); + cbInit(L, DCC_FUNC); + cbAddFieldInteger(L, Addr, "Addr"); + cbAddFieldInteger(L, AddrType, "AddrType"); + cbAddFieldInteger(L, FuncGrp, "FuncGrp"); + cbAddFieldInteger(L, FuncState, "FuncState"); + lua_call(L, 2, 0); +} + +void notifyDccAccTurnoutBoard( uint16_t BoardAddr, uint8_t OutputPair, uint8_t Direction, uint8_t OutputPower ) { + lua_State* L = lua_getstate(); + cbInit(L, DCC_TURNOUT); + cbAddFieldInteger(L, BoardAddr, "BoardAddr"); + cbAddFieldInteger(L, OutputPair, "OutputPair"); + cbAddFieldInteger(L, Direction, "Direction"); + cbAddFieldInteger(L, OutputPower, "OutputPower"); + lua_call(L, 2, 0); +} + +void notifyDccAccTurnoutOutput( uint16_t Addr, uint8_t Direction, uint8_t OutputPower ) { + lua_State* L = lua_getstate(); + cbInit(L, DCC_TURNOUT); + cbAddFieldInteger(L, Addr, "Addr"); + cbAddFieldInteger(L, Direction, "Direction"); + cbAddFieldInteger(L, OutputPower, "OutputPower"); + lua_call(L, 2, 0); +} + +void notifyDccAccBoardAddrSet( uint16_t BoardAddr) { + lua_State* L = lua_getstate(); + cbInit(L, DCC_ACCESSORY); + cbAddFieldInteger(L, BoardAddr, "BoardAddr"); + lua_call(L, 2, 0); +} + +void notifyDccAccOutputAddrSet( uint16_t Addr) { + lua_State* L = lua_getstate(); + cbInit(L, DCC_ACCESSORY); + cbAddFieldInteger(L, Addr, "Addr"); + lua_call(L, 2, 0); +} + +void notifyDccSigOutputState( uint16_t Addr, uint8_t State) { + lua_State* L = lua_getstate(); + cbInit(L, DCC_ACCESSORY); + cbAddFieldInteger(L, State, "State"); + lua_call(L, 2, 0); +} + +void notifyDccMsg( DCC_MSG * Msg ) { + lua_State* L = lua_getstate(); + cbInit(L, DCC_RAW); + cbAddFieldInteger(L, Msg->Size, "Size"); + cbAddFieldInteger(L, Msg->PreambleBits, "PreambleBits"); + char field[8]; + for(uint8_t i = 0; i< MAX_DCC_MESSAGE_LEN; i++ ) { + ets_sprintf(field, "Data%d", i); + cbAddFieldInteger(L, Msg->Data[i], field); + } + lua_call(L, 2, 0); +} + +void notifyServiceMode(bool InServiceMode){ + lua_State* L = lua_getstate(); + cbInit(L, DCC_SERVICEMODE); + cbAddFieldInteger(L, InServiceMode, "InServiceMode"); + lua_call(L, 2, 0); +} + +// CV handling + +uint8_t notifyCVValid( uint16_t CV, uint8_t Writable ) { + lua_State* L = lua_getstate(); + if(notify_cb == LUA_NOREF) + return 0; + lua_rawgeti(L, LUA_REGISTRYINDEX, CV_cb); + lua_pushinteger(L, CV_VALID); + lua_newtable(L); + cbAddFieldInteger(L, CV, "CV"); + cbAddFieldInteger(L, Writable, "Writable"); + lua_call(L, 2, 1); + uint8 result = lua_tointeger(L, -1); + lua_pop(L, 1); + return result; +} + +uint8_t notifyCVRead( uint16_t CV) { + lua_State* L = lua_getstate(); + if(notify_cb == LUA_NOREF) + return 0; + lua_rawgeti(L, LUA_REGISTRYINDEX, CV_cb); + lua_pushinteger(L, CV_READ); + lua_newtable(L); + cbAddFieldInteger(L, CV, "CV"); + lua_call(L, 2, 1); + uint8 result = lua_tointeger(L, -1); + lua_pop(L, 1); + return result; +} + +uint8_t notifyCVWrite( uint16_t CV, uint8_t Value) { + lua_State* L = lua_getstate(); + if(notify_cb == LUA_NOREF) + return 0; + lua_rawgeti(L, LUA_REGISTRYINDEX, CV_cb); + lua_pushinteger(L, CV_WRITE); + lua_newtable(L); + cbAddFieldInteger(L, CV, "CV"); + cbAddFieldInteger(L, Value, "Value"); + lua_call(L, 2, 0); + return Value; +} + +void notifyCVResetFactoryDefault(void) { + lua_State* L = lua_getstate(); + if(notify_cb == LUA_NOREF) + return; + lua_rawgeti(L, LUA_REGISTRYINDEX, CV_cb); + lua_pushinteger(L, CV_RESET); + lua_call(L, 1, 0); +} + +static int dcc_lua_setup(lua_State* L) { + NODE_DBG("[dcc_lua_setup]\n"); + if (!lua_isnumber(L, 6) && !lua_isnumber(L, 7)) { + return luaL_error(L, "wrong arg range"); + } + uint8_t pin = luaL_checkinteger(L, 1); + luaL_argcheck(L, platform_gpio_exists(pin) && pin>0, 1, "Invalid interrupt pin"); + + if (lua_type(L, 2) == LUA_TFUNCTION || lua_type(L, 3) == LUA_TLIGHTFUNCTION) + { + lua_pushvalue(L, 2); + register_lua_cb(L, ¬ify_cb); + } + else + { + unregister_lua_cb(L, ¬ify_cb); + } + + uint8_t ManufacturerId = luaL_checkinteger(L, 3); + uint8_t VersionId = luaL_checkinteger(L, 4); + uint8_t Flags = luaL_checkinteger(L, 5); + uint8_t OpsModeAddressBaseCV = luaL_checkinteger(L, 6); + + if (lua_type(L, 7) == LUA_TFUNCTION || lua_type(L, 3) == LUA_TLIGHTFUNCTION) + { + lua_pushvalue(L, 7); + register_lua_cb(L, &CV_cb); + } + else + { + unregister_lua_cb(L, &CV_cb); + } + + NODE_DBG("[dcc_lua_setup] Enabling interrupt on PIN %d\n", pin); + dcc_setup(pin, ManufacturerId, VersionId, Flags, OpsModeAddressBaseCV ); + + return 0; +} + +static int dcc_lua_close(lua_State* L) { + dcc_close(); + unregister_lua_cb(L, ¬ify_cb); + return 0; +} + +int luaopen_dcc( lua_State *L ) { + NODE_DBG("[dcc_luaopen]\n"); + dcc_init(); + //DccRx.lua_cb_ref = LUA_NOREF; + return 0; +} + +// Module function map +LROT_BEGIN( dcc ) + LROT_FUNCENTRY( setup, dcc_lua_setup ) + LROT_FUNCENTRY( close, dcc_lua_close ) + + LROT_NUMENTRY( DCC_RESET, DCC_RESET ) + LROT_NUMENTRY( DCC_IDLE, DCC_IDLE ) + LROT_NUMENTRY( DCC_SPEED, DCC_SPEED ) + LROT_NUMENTRY( DCC_SPEED_RAW, DCC_SPEED_RAW ) + LROT_NUMENTRY( DCC_FUNC, DCC_FUNC ) + LROT_NUMENTRY( DCC_TURNOUT, DCC_TURNOUT ) + LROT_NUMENTRY( DCC_ACCESSORY, DCC_ACCESSORY ) + LROT_NUMENTRY( DCC_RAW, DCC_RAW ) + LROT_NUMENTRY( DCC_SERVICEMODE, DCC_SERVICEMODE ) + + LROT_NUMENTRY( CV_VALID, CV_VALID ) + LROT_NUMENTRY( CV_READ, CV_READ ) + LROT_NUMENTRY( CV_WRITE, CV_WRITE ) + LROT_NUMENTRY( CV_RESET, CV_RESET ) + + LROT_NUMENTRY( MAN_ID_JMRI, MAN_ID_JMRI) + LROT_NUMENTRY( MAN_ID_DIY, MAN_ID_DIY) + LROT_NUMENTRY( MAN_ID_SILICON_RAILWAY, MAN_ID_SILICON_RAILWAY) + + LROT_NUMENTRY( FLAGS_MY_ADDRESS_ONLY, FLAGS_MY_ADDRESS_ONLY ) + LROT_NUMENTRY( FLAGS_AUTO_FACTORY_DEFAULT, FLAGS_AUTO_FACTORY_DEFAULT ) + LROT_NUMENTRY( FLAGS_OUTPUT_ADDRESS_MODE, FLAGS_OUTPUT_ADDRESS_MODE ) + LROT_NUMENTRY( FLAGS_DCC_ACCESSORY_DECODER, FLAGS_DCC_ACCESSORY_DECODER ) +LROT_END( dcc, NULL, 0 ) + +NODEMCU_MODULE(DCC, "dcc", dcc, luaopen_dcc); diff --git a/docs/modules/dcc.md b/docs/modules/dcc.md new file mode 100644 index 00000000..2a55fda2 --- /dev/null +++ b/docs/modules/dcc.md @@ -0,0 +1,118 @@ +# DCC module +| Since | Origin / Contributor | Maintainer | Source | +| :----- | :-------------------- | :---------- | :------ | +| 2019-12-28 | [vsky279](https://github.com/vsky279) | [vsky279](https://github.com/vsky279) | [dcc.c](../../app/modules/dcc.c)| + +The dcc module implements decoder of the [National Model Railroad Association](https://www.nmra.org/) (NMRA) Digital Command Control (DCC) decoder - see [DCC wiki](https://dccwiki.com/Introduction_to_DCC) for details. + +The hardware needed to decode the DCC signal can be built based on different DCC decoders implementation for Arduino, for inspiration see [https://mrrwa.org/dcc-decoder-interface/](https://mrrwa.org/dcc-decoder-interface/). Basically the signal from the DCC bus is connected via an optocoupler to any GPIO pin. The DCC bus can be also used to power the ESP. + +The module is based on the project NmraDcc [https://github.com/mrrwa/NmraDcc](https://github.com/mrrwa/NmraDcc) by Alex Shepherd. The module is based on the version from May 2005, commit [6d12e6cd3f5f520020d49946652a94c1e3473f6b](https://github.com/mrrwa/NmraDcc/tree/6d12e6cd3f5f520020d49946652a94c1e3473f6b). + +## dcc.setup() + +Initializes the dcc module and links callback functions. + +#### Syntax +`dcc.setup(DCC_command, ManufacturerId, VersionId, Flags, OpsModeAddressBaseCV, CV_callback)` + +#### Parameters +- `DCC_command(cmd, params)` calllback function that is called when a DCC command is decoded. `cmd` parameters is one of the following values. `params` contains a collection of parameters specific to given command. + - `dcc.DCC_RESET` no additional parameters, `params` is `nil`. + - `dcc.DCC_IDLE` no additional parameters, `params` is `nil`. + - `dcc.DCC_SPEED` parameters collection members are `Addr`, `AddrType`, `Speed`,`Dir`, `SpeedSteps`. + - `dcc.DCC_SPEED_RAW` parameters collection members are `Addr`, `AddrType`, `Raw`. + - `dcc.DCC_FUNC` parameters collection members are `Addr`, `AddrType`, `FuncGrp`,`FuncState`. + - `dcc.DCC_TURNOUT` parameters collection members are `BoardAddr`, `OutputPair`, `Direction`,`OutputPower` or `Addr`, `Direction`,`OutputPower`. + - `dcc.DCC_ACCESSORY` parameters collection has one member `BoardAddr` or `Addr` or `State`. + - `dcc.DCC_RAW` parameters collection member are `Size`, `PreambleBits`, `Data1` to `Data6`. + - `dcc.DCC_SERVICEMODE` parameters collection has one member `InServiceMode`. +- `ManufacturerId` Manufacturer ID returned in CV 8. Commonly `dcc.MAN_ID_DIY`. +- `VersionId` Version ID returned in CV 7. +- `Flags` one of or combination (OR operator) of + - `dcc.FLAGS_MY_ADDRESS_ONLY`Only process packets with My Address. + - `dcc.FLAGS_DCC_ACCESSORY_DECODER` Decoder is an accessory decode. + - `dcc.FLAGS_OUTPUT_ADDRESS_MODE` This flag applies to accessory decoders only. Accessory decoders normally have 4 paired outputs and a single address refers to all 4 outputs. Setting this flag causes each address to refer to a single output. + - `dcc.FLAGS_AUTO_FACTORY_DEFAULT` Call DCC command callback with `dcc.CV_RESET` command if CV 7 & 8 == 255. +- `OpsModeAddressBaseCV` Ops Mode base address. Set it to 0? +- `CV_callback(operation, param)` callback function that is called when any manipulation with CV ([Configuarion Variable](https://dccwiki.com/Configuration_Variable)) is requested. + - `dcc.CV_VALID`to determine if a given CV is valid. This callback must determine if a CV is valid and return the appropriate value. `param` collection has members `CV` and `Value`. + - `dcc.CV_READ` to read a CV. This callback must return the value of the CV. `param` collection has one member `CV` determing the CV number to be read. + - `dcc.CV_WRITE` to write a value to a CV. This callback must write the Value to the CV and return the value of the CV. `param` collection has members `CV` and `Value`. + - `dcc.CV_RESET` Called when CVs must be reset to their factory defaults. + +#### Returns +`nil` + +#### Example +`bit` module is used in the example though it is not needed for the dcc module functionality. +```lua +local PIN = 2 -- GPIO4 + +local addr = 0x12a + +CV = {[29]=0, + [1]=bit.band(addr, 0x3f), --CV_ACCESSORY_DECODER_ADDRESS_LSB (6 bits) + [9]=bit.band(bit.rshift(addr,6), 0x7) --CV_ACCESSORY_DECODER_ADDRESS_MSB (3 bits) + } + +local function DCC_command(cmd, params) + if cmd == dcc.DCC_IDLE then + return + elseif cmd == dcc.DCC_TURNOUT then + print("Turnout command") + elseif cmd == dcc.DCC_SPEED then + print("Speed command") + elseif cmd == dcc.DCC_FUNC then + print("Function command") + else + print("Other command", cmd) + end + + for i,j in pairs(params) do + print(i, j) + end + print(("="):rep(80)) +end + +local function CV_callback(operation, param) + local oper = "" + local result + if operation == dcc.CV_WRITE then + oper = "Write" + CV[param.CV]=param.Value + elseif operation == dcc.CV_READ then + oper = "Read" + result = CV[param.CV] + elseif operation == dcc.CV_VALID then + oper = "Valid" + result = 1 + elseif operation == CV_RESET then + oper = "Reset" + CV = {} + end + print(("[CV_callback] %s CV %d%s"):format(oper, param.CV or `nil`, param.Value and "\tValue: "..param.Value or "\tValue: nil")) + return result +end + +dcc.setup(PIN, + DCC_command, + dcc.MAN_ID_DIY, 1, + --bit.bor(dcc.FLAGS_AUTO_FACTORY_DEFAULT, dcc.FLAGS_DCC_ACCESSORY_DECODER, dcc.FLAGS_MY_ADDRESS_ONLY), + bit.bor(dcc.FLAGS_AUTO_FACTORY_DEFAULT), + 0, -- ??? + CV_callback) +``` + +## dcc.close() + +Stops the dcc module. + +#### Syntax +`dcc.close()` + +#### Parameters +`nil` + +#### Returns +`nil` diff --git a/lua_examples/dcc/dcc.lua b/lua_examples/dcc/dcc.lua new file mode 100644 index 00000000..922b89d3 --- /dev/null +++ b/lua_examples/dcc/dcc.lua @@ -0,0 +1,86 @@ +-- Simple example for responding to NMRA DCC commands +-- author @voborsky +local PIN = 2 -- GPIO4 + +local addr = 0x12a + +CV = {[29]=0, + [1]=bit.band(addr, 0x3f), --CV_ACCESSORY_DECODER_ADDRESS_LSB (6 bits) + [9]=bit.band(bit.rshift(addr,6), 0x7) --CV_ACCESSORY_DECODER_ADDRESS_MSB (3 bits) + } + +local function deepcopy(orig) + local orig_type = type(orig) + local copy + if orig_type == 'table' then + copy = {} + for orig_key, orig_value in next, orig, nil do + copy[deepcopy(orig_key)] = deepcopy(orig_value) + end + setmetatable(copy, deepcopy(getmetatable(orig))) + else -- number, string, boolean, etc + copy = orig + end + return copy +end + +local cmd_last +local params_last + +local function is_new(cmd, params) + if cmd ~= cmd_last then return true end + for i,j in pairs(params) do + if params_last[i] ~= j then return true end + end + return false +end + +local function DCC_command(cmd, params) + if not is_new(cmd, params) then return end + if cmd == dcc.DCC_IDLE then + return + elseif cmd == dcc.DCC_TURNOUT then + print("Turnout command") + elseif cmd == dcc.DCC_SPEED then + print("Speed command") + elseif cmd == dcc.DCC_FUNC then + print("Function command") + else + print("Other command", cmd) + end + + for i,j in pairs(params) do + print(i, j) + end + print(("="):rep(80)) + cmd_last = cmd + params_last = deepcopy(params) +end + +local function CV_callback(operation, param) + local oper = "" + local result + if operation == dcc.CV_WRITE then + oper = "Write" + CV[param.CV]=param.Value + elseif operation == dcc.CV_READ then + oper = "Read" + result = CV[param.CV] + elseif operation == dcc.CV_VALID then + oper = "Valid" + result = 1 + elseif operation == CV_RESET then + oper = "Reset" + CV = {} + end + print(("[CV_callback] %s CV %d%s"):format(oper, param.CV, param.Value and "\tValue: "..param.Value or "\tValue: nil")) + return result +end + +dcc.setup(PIN, + DCC_command, + dcc.MAN_ID_DIY, 1, + --bit.bor(dcc.FLAGS_AUTO_FACTORY_DEFAULT, dcc.FLAGS_DCC_ACCESSORY_DECODER, dcc.FLAGS_MY_ADDRESS_ONLY), -- Accessories (turnouts) decoder + bit.bor(dcc.FLAGS_AUTO_FACTORY_DEFAULT), -- Cab (train) decoder + 0, -- ??? + CV_callback) diff --git a/mkdocs.yml b/mkdocs.yml index 11be8c88..16c16859 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -67,6 +67,7 @@ pages: - 'color-utils': 'modules/color-utils.md' - 'cron': 'modules/cron.md' - 'crypto': 'modules/crypto.md' + - 'dcc': 'modules/dcc.md' - 'dht': 'modules/dht.md' - 'encoder': 'modules/encoder.md' - 'enduser setup / captive portal / WiFi manager': 'modules/enduser-setup.md'