Browse Source

NmraDcc port to NodeMCU Lua

Merging @pjsg's ack functionalities and other fixes
Lukáš Voborský 3 years ago
parent
commit
f017338216
4 changed files with 454 additions and 58 deletions
  1. 223 10
      app/driver/NmraDcc.c
  2. 58 4
      app/include/driver/NmraDcc.h
  3. 161 36
      app/modules/dcc.c
  4. 12 8
      docs/modules/dcc.md

+ 223 - 10
app/driver/NmraDcc.c

@@ -45,8 +45,59 @@
 //
 //------------------------------------------------------------------------
 
+// NodeMCU Lua port by @voborsky
+
+// #define NODE_DEBUG
+#define NODEMCUDCC
+
+#ifdef NODEMCUDCC
+  #include <stdint.h>
+  #include <stdlib.h>
+  #include <stdio.h>
+  #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')
+   
+   #define byte uint8_t
+   #define word int16_t
+
+   #define abs(a) ((a) > 0 ? (a) : (0-a))
+   
+   #define RISING GPIO_PIN_INTR_POSEDGE
+   #define FALLING GPIO_PIN_INTR_NEGEDGE
+   #define CHANGE GPIO_PIN_INTR_ANYEDGE
+   
+    static uint32_t last_time_overflow_millis;
+    static uint32_t last_system_time;
+
+    uint32_t millis() {
+      uint32_t now = system_get_time();
+
+      if (now < last_system_time) {
+        // we have an overflow situation 
+        // assume only one overflow
+        last_time_overflow_millis += (1 << 29) / 125;   // (1 << 32) / 1000
+      }
+
+      last_system_time = now;
+      return last_time_overflow_millis + now / 1000;
+    }
+#else
 #include "NmraDcc.h"
 #include "EEPROM.h"
+#endif
 
 // Uncomment to print DEBUG messages
 // #define DEBUG_PRINT		
@@ -113,6 +164,9 @@
 
 // Debug-Ports
 //#define debug     // Testpulse for logic analyser
+#ifdef NODE_DEBUG
+    #define debug
+#endif
 #ifdef debug 
     #if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
         #define MODE_TP1 DDRF |= (1<<2) //pinA2
@@ -210,6 +264,24 @@
         
         
     //#elif defined(__AVR_ATmega128__) ||defined(__AVR_ATmega1281__)||defined(__AVR_ATmega2561__)
+    #elif defined(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 
@@ -241,8 +313,12 @@
     
 #endif
 #ifdef DEBUG_PRINT
+  #ifdef NODEMCUDCC
+    #define DB_PRINT NODE_DBG
+  #else
     #define DB_PRINT( x, ... ) { char dbgbuf[80]; sprintf_P( dbgbuf, (const char*) F( x ) , ##__VA_ARGS__ ) ; Serial.println( dbgbuf ); }
     #define DB_PRINT_( x, ... ) { char dbgbuf[80]; sprintf_P( dbgbuf, (const char*) F( x ) , ##__VA_ARGS__ ) ; Serial.print( dbgbuf ); }
+  #endif
 #else
     #define DB_PRINT( x, ... ) ;
     #define DB_PRINT_( x, ... ) ;
@@ -257,6 +333,10 @@ static ExtIntTriggerMode ISREdge;
 #elif defined ( ESP32 )
 static byte  ISREdge;   // Holder of the Next Edge we're looking for: RISING or FALLING
 static byte  ISRWatch;  // Interrupt Handler Edge Filter 
+#elif defined ( NODEMCUDCC )
+static uint8_t  ISREdge;   // Holder of the Next Edge we're looking for: RISING or FALLING
+static int16_t  bitMax, bitMin;
+DCC_MSG Msg ;
 #else
 static byte  ISREdge;   // Holder of the Next Edge we're looking for: RISING or FALLING
 static byte  ISRWatch;  // Interrupt Handler Edge Filter 
@@ -307,10 +387,15 @@ typedef struct
   uint8_t   PageRegister ;  // Used for Paged Operations in Service Mode Programming
   uint8_t   DuplicateCount ;
   DCC_MSG   LastMsg ;
+#ifdef NODEMCUDCC
+  uint8_t   IntPin;
+  uint8_t   IntBitmask;
+#else  
   uint8_t	ExtIntNum; 
   uint8_t	ExtIntPinNum;
   volatile uint8_t   *ExtIntPort;     // use port and bitmask to read input at AVR in ISR
   uint8_t   ExtIntMask;     // digitalRead is too slow on AVR
+#endif
   int16_t   myDccAddress;	// Cached value of DCC Address from CVs
   uint8_t   inAccDecDCCAddrNextReceivedMode;
   uint8_t	cv29Value; 
@@ -330,12 +415,27 @@ portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;
 void IRAM_ATTR ExternalInterruptHandler(void)
 #elif defined(ESP8266)
 void ICACHE_RAM_ATTR ExternalInterruptHandler(void)
+#elif defined(NODEMCUDCC)
+task_handle_t   DataReady_taskid;
+static uint32_t ICACHE_RAM_ATTR InterruptHandler (uint32_t ret_gpio_status)
 #else
 void ExternalInterruptHandler(void)
 #endif
 {
     SET_TP3;
 
+#ifdef NODEMCUDCC
+  // 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);
+  ret_gpio_status &= ~(DccProcState.IntBitmask);
+#endif
+  
 #ifdef ESP32
 //   switch (ISRWatch)
 //   {
@@ -363,7 +463,11 @@ void ExternalInterruptHandler(void)
     uint8_t DccBitVal;
     static int8_t  bit1, bit2 ;
     static unsigned int  lastMicros = 0;
+    #ifdef NODEMCUDCC
+    static byte halfBit, preambleBitCount;
+    #else
     static byte halfBit, DCC_IrqRunning, preambleBitCount;
+    #endif
     unsigned int  actMicros, bitMicros;
     #ifdef ALLOW_NESTED_IRQ
     if ( DCC_IrqRunning ) {
@@ -377,19 +481,29 @@ void ExternalInterruptHandler(void)
         return; //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> abort IRQ
     }
     #endif
+#ifdef NODEMCUDCC
+    actMicros = system_get_time();
+#else
     actMicros = micros();
+#endif
     bitMicros = actMicros-lastMicros;
 
         CLR_TP3; SET_TP3;
 #ifdef __AVR_MEGA__
     if ( bitMicros < bitMin || ( DccRx.State != WAIT_START_BIT && (*DccProcState.ExtIntPort & DccProcState.ExtIntMask) != (ISRLevel) ) ) {
+#elif defined(NODEMCUDCC)
+    if ( bitMicros < bitMin ) {
 #else
     if ( bitMicros < bitMin || ( DccRx.State != WAIT_START_BIT && digitalRead( DccProcState.ExtIntPinNum ) != (ISRLevel) ) ) {
 #endif
         // too short - my be false interrupt due to glitch or false protocol  or level does not match RISING / FALLING edge -> ignore this IRQ
         CLR_TP3;
         SET_TP4; /*delayMicroseconds(1); */ CLR_TP4;
+#ifdef NODEMCUDCC
+        return ret_gpio_status; //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> abort IRQ
+#else
         return; //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> abort IRQ
+#endif
     }
         CLR_TP3;  SET_TP3;
 
@@ -409,6 +523,8 @@ void ExternalInterruptHandler(void)
         #endif
         #ifdef ESP32
         ISRWatch = ISREdge;
+        #elif defined(NODEMCUDCC)
+        gpio_pin_intr_state_set(GPIO_ID_PIN(pin_num[DccProcState.IntPin]), ISREdge );
         #else
         attachInterrupt( DccProcState.ExtIntNum, ExternalInterruptHandler, ISREdge );
         #endif
@@ -492,6 +608,8 @@ void ExternalInterruptHandler(void)
 				#endif
                 #ifdef ESP32
 				ISRWatch = ISREdge;
+                #elif defined(NODEMCUDCC)
+                gpio_pin_intr_state_set(GPIO_ID_PIN(pin_num[DccProcState.IntPin]), ISREdge);
                 #else
                 attachInterrupt( DccProcState.ExtIntNum, ExternalInterruptHandler, ISREdge );
                 // enable level checking ( with direct port reading @ AVR )
@@ -542,13 +660,17 @@ void ExternalInterruptHandler(void)
         #endif
         #ifdef ESP32
         ISRWatch = ISREdge;
+        #elif defined(NODEMCUDCC)
+        gpio_pin_intr_state_set(GPIO_ID_PIN(pin_num[DccProcState.IntPin]), ISREdge);
         #else
         attachInterrupt( DccProcState.ExtIntNum, ExternalInterruptHandler, ISREdge );
         #endif
+        #ifndef NODEMCUDCC
         // enable level-checking
         ISRChkMask = DccProcState.ExtIntMask;
         ISRLevel = (ISREdge==RISING)? DccProcState.ExtIntMask : 0 ;
         //CLR_TP4;
+        #endif
         break;
       case 4: // previous (first) halfbit was 0
         // if this halfbit is 0 too, we got the startbit
@@ -585,14 +707,17 @@ void ExternalInterruptHandler(void)
 		#endif
         #ifdef ESP32
         ISRWatch = ISREdge;
+        #elif defined(NODEMCUDCC)
+        gpio_pin_intr_state_set(GPIO_ID_PIN(pin_num[DccProcState.IntPin]), ISREdge);
         #else
 		attachInterrupt( DccProcState.ExtIntNum, ExternalInterruptHandler, ISREdge );
         #endif
+        #ifndef NODEMCUDCC
         // enable level-checking
         ISRChkMask = DccProcState.ExtIntMask;
         ISRLevel = (ISREdge==RISING)? DccProcState.ExtIntMask : 0 ;
-
 		//CLR_TP4;
+        #endif
         break;
             
     }        
@@ -642,6 +767,8 @@ void ExternalInterruptHandler(void)
         DccRx.DataReady = 1 ;
         #ifdef ESP32
         portEXIT_CRITICAL_ISR(&mux);
+        #elif defined(NODEMCUDCC)
+        task_post_high(DataReady_taskid, (os_param_t) 0);
         #endif
         // SET_TP2; CLR_TP2;
         preambleBitCount = 0 ;
@@ -701,6 +828,8 @@ void ExternalInterruptHandler(void)
             #endif
             #ifdef ESP32
             ISRWatch = CHANGE;
+            #elif defined(NODEMCUDCC)
+            gpio_pin_intr_state_set(GPIO_ID_PIN(pin_num[DccProcState.IntPin]), CHANGE);
             #else
             attachInterrupt( DccProcState.ExtIntNum, ExternalInterruptHandler, CHANGE);
             #endif
@@ -726,6 +855,9 @@ void ExternalInterruptHandler(void)
   #endif
   //CLR_TP1;
   CLR_TP3;
+#ifdef NODEMCUDCC
+  return ret_gpio_status;
+#endif
 }
 
 void ackCV(void)
@@ -747,6 +879,7 @@ void ackAdvancedCV(void)
 }
 
 
+#ifndef NODEMCUDCC
 uint8_t readEEPROM( unsigned int CV )
 {
     return EEPROM.read(CV) ;
@@ -773,6 +906,7 @@ bool readyEEPROM()
 	return true;
 #endif
 }
+#endif
 
 uint8_t validCV( uint16_t CV, uint8_t Writable )
 {
@@ -782,6 +916,9 @@ uint8_t validCV( uint16_t CV, uint8_t Writable )
   if( notifyCVValid )
     return notifyCVValid( CV, Writable ) ;
 
+#ifdef NODEMCUDCC
+  return 0;
+#else
   uint8_t Valid = 1 ;
 
   if( CV > MAXCV )
@@ -791,17 +928,28 @@ uint8_t validCV( uint16_t CV, uint8_t Writable )
     Valid = 0 ;
 
   return Valid ;
+#endif
 }
 
+#ifdef NODEMCUDCC
+uint16_t readCV( unsigned int CV )
+#else
 uint8_t readCV( unsigned int CV )
+#endif
 {
+#ifndef NODEMCUDCC
   uint8_t Value ;
+#endif
 
   if( notifyCVRead )
     return notifyCVRead( CV ) ;
 
+#ifndef NODEMCUDCC
   Value = readEEPROM(CV);
   return Value ;
+#else
+  return 0;
+#endif
 }
 
 uint8_t writeCV( unsigned int CV, uint8_t Value)
@@ -825,6 +973,9 @@ uint8_t writeCV( unsigned int CV, uint8_t Value)
   if( notifyCVWrite )
     return notifyCVWrite( CV, Value ) ;
 
+#ifdef NODEMCUDCC
+  return 0;
+#else
   if( readEEPROM( CV ) != Value )
   {
     writeEEPROM( CV, Value ) ;
@@ -837,6 +988,7 @@ uint8_t writeCV( unsigned int CV, uint8_t Value)
   }
 
   return readEEPROM( CV ) ;
+#endif
 }
 
 uint16_t getMyAddr(void)
@@ -896,9 +1048,18 @@ void processDirectCVOperation( uint8_t Cmd, uint16_t CVAddr, uint8_t Value, void
     uint8_t BitValue = Value & 0x08 ;
     uint8_t BitWrite = Value & 0x10 ;
 
+#ifdef NODEMCUDCC
+    uint16_t tempValue = readCV( CVAddr ) ;  // Read the Current CV Value
+#else
     uint8_t tempValue = readCV( CVAddr ) ;  // Read the Current CV Value
+#endif
 
+#ifdef NODEMCUDCC
+    if (tempValue <= 255) {
+        DB_PRINT("CV: %d Current Value: %02X  Bit-Wise Mode: %s  Mask: %02X  Value: %02X", CVAddr, tempValue, BitWrite ? "Write":"Read", BitMask, BitValue);
+#else
     DB_PRINT("CV: %d Current Value: %02X  Bit-Wise Mode: %s  Mask: %02X  Value: %02X", CVAddr, tempValue, BitWrite ? "Write":"Read", BitMask, BitValue);
+#endif
 
     // Perform the Bit Write Operation
     if( BitWrite )
@@ -915,7 +1076,6 @@ void processDirectCVOperation( uint8_t Cmd, uint16_t CVAddr, uint8_t Value, void
           ackFunction() ;
       }
     }
-
     // Perform the Bit Verify Operation
     else
     {
@@ -933,6 +1093,9 @@ void processDirectCVOperation( uint8_t Cmd, uint16_t CVAddr, uint8_t Value, void
         }
       }
     }
+#ifdef NODEMCUDCC
+    }
+#endif
   }
 }
 
@@ -947,9 +1110,13 @@ void processMultiFunctionMessage( uint16_t Addr, DCC_ADDR_TYPE AddrType, uint8_t
 
   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 ) )
@@ -966,6 +1133,7 @@ void processMultiFunctionMessage( uint16_t Addr, DCC_ADDR_TYPE AddrType, uint8_t
   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
@@ -1224,6 +1392,7 @@ void SerialPrintPacketHex(const __FlashStringHelper *strLabel, DCC_MSG * pDccMsg
 ///////////////////////////////////////////////////////////////////////////////
 void execDccProcessor( DCC_MSG * pDccMsg )
 {
+  NODE_DBG("[dcc_execDccProcessor]\n"); 
   if( ( pDccMsg->Data[0] == 0 ) && ( pDccMsg->Data[1] == 0 ) )
   {
     if( notifyDccReset )
@@ -1295,7 +1464,7 @@ void execDccProcessor( DCC_MSG * pDccMsg )
 
           BoardAddress = ( ( (~pDccMsg->Data[1]) & 0b01110000 ) << 2 ) | ( pDccMsg->Data[0] & 0b00111111 ) ;
           TurnoutPairIndex = (pDccMsg->Data[1] & 0b00000110) >> 1;
-          DB_PRINT("eDP: BAddr:%d, Index:%d", BoardAddress, TurnoutPairIndex);
+          DB_PRINT("[dcc_execDccProcessor] eDP: BAddr:%d, Index:%d", BoardAddress, TurnoutPairIndex);
           
           // First check for Legacy Accessory Decoder Configuration Variable Access Instruction
           // as it's got a different format to the others 
@@ -1305,13 +1474,13 @@ void execDccProcessor( DCC_MSG * pDccMsg )
             // Check if this command is for our address or the broadcast address
             if((BoardAddress != getMyAddr()) && ( BoardAddress < 511 )) 
             {
-              DB_PRINT("eDP: Board Address Not Matched");
+              DB_PRINT("[dcc_execDccProcessor] eDP: Board Address Not Matched");
               return;
             }
 
             uint16_t cvAddress = ((pDccMsg->Data[1] & 0b00000011) << 8) + pDccMsg->Data[2] + 1;
 		  	uint8_t  cvValue   = pDccMsg->Data[3];
-            DB_PRINT("eDP: CV:%d Value:%d", cvAddress, cvValue );
+            DB_PRINT("[dcc_execDccProcessor] eDP: CV:%d Value:%d", cvAddress, cvValue );
           	if(validCV( cvAddress, 1 ))
               writeCV(cvAddress, cvValue);
           	return;
@@ -1320,7 +1489,7 @@ void execDccProcessor( DCC_MSG * pDccMsg )
 
           OutputAddress = (((BoardAddress - 1) << 2 ) | TurnoutPairIndex) + 1 ; //decoder output addresses start with 1, packet address range starts with 0
                                                                                 // ( according to NMRA 9.2.2 )
-          DB_PRINT("eDP: OAddr:%d", OutputAddress);
+          DB_PRINT("[dcc_execDccProcessor] eDP: OAddr:%d", OutputAddress);
           
           if( DccProcState.inAccDecDCCAddrNextReceivedMode)
           {
@@ -1351,18 +1520,18 @@ void execDccProcessor( DCC_MSG * pDccMsg )
           if( DccProcState.Flags & FLAGS_MY_ADDRESS_ONLY )
           {
             if( DccProcState.Flags & FLAGS_OUTPUT_ADDRESS_MODE ) {
-              DB_PRINT(" AddrChk: OAddr:%d, BAddr:%d, myAddr:%d Chk=%d", OutputAddress, BoardAddress, getMyAddr(), OutputAddress != getMyAddr() );
+              DB_PRINT("[dcc_execDccProcessor] AddrChk: OAddr:%d, BAddr:%d, myAddr:%d Chk=%d", OutputAddress, BoardAddress, getMyAddr(), OutputAddress != getMyAddr() );
               if ( OutputAddress != getMyAddr()  &&  OutputAddress < 2045 ) {
-                DB_PRINT(" eDP: OAddr:%d, myAddr:%d - no match", OutputAddress, getMyAddr() );
+                DB_PRINT("[dcc_execDccProcessor] eDP: OAddr:%d, myAddr:%d - no match", OutputAddress, getMyAddr() );
                 return;
               }  
             } else {
               if( ( BoardAddress != getMyAddr() ) && ( BoardAddress < 511 ) ) {
-                DB_PRINT(" eDP: BAddr:%d, myAddr:%d - no match", BoardAddress, getMyAddr() );
+                DB_PRINT("[dcc_execDccProcessor] eDP: BAddr:%d, myAddr:%d - no match", BoardAddress, getMyAddr() );
                 return;
               }
             }
-	        DB_PRINT("eDP: Address Matched");
+	        DB_PRINT("[dcc_execDccProcessor] eDP: Address Matched");
           }
           
 
@@ -1491,6 +1660,7 @@ void execDccProcessor( DCC_MSG * pDccMsg )
 }
 
 ////////////////////////////////////////////////////////////////////////
+#ifndef NODEMCUDCC
 NmraDcc::NmraDcc()
 {
 }
@@ -1538,9 +1708,14 @@ void NmraDcc::initAccessoryDecoder( uint8_t ManufacturerId, uint8_t VersionId, u
 {
 	init(ManufacturerId, VersionId, Flags | FLAGS_DCC_ACCESSORY_DECODER, OpsModeAddressBaseCV);
 }
+#endif //#ifndef NODEMCUDCC
 
 ////////////////////////////////////////////////////////////////////////
+#ifdef NODEMCUDCC
+void dcc_setup(uint8_t pin, uint8_t ManufacturerId, uint8_t VersionId, uint8_t Flags, uint8_t OpsModeAddressBaseCV)
+#else
 void NmraDcc::init( uint8_t ManufacturerId, uint8_t VersionId, uint8_t Flags, uint8_t OpsModeAddressBaseCV )
+#endif
 {
   #if defined(ESP8266)
     EEPROM.begin(MAXCV);
@@ -1564,13 +1739,24 @@ void NmraDcc::init( uint8_t ManufacturerId, uint8_t VersionId, uint8_t Flags, ui
   DccProcState.inAccDecDCCAddrNextReceivedMode = 0;
 
   ISREdge = RISING;
+#ifdef NODEMCUDCC
+  DccProcState.IntPin = pin;
+  DccProcState.IntBitmask = 1 << pin_num[pin];
+#else
   // level checking to detect false IRQ's fired by glitches
   ISRLevel = DccProcState.ExtIntMask;
   ISRChkMask = DccProcState.ExtIntMask;
+#endif
 
   #ifdef ESP32
   ISRWatch = ISREdge;
   attachInterrupt( DccProcState.ExtIntNum, ExternalInterruptHandler, CHANGE);
+  #elif defined(NODEMCUDCC)
+  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]), RISING);
   #else
   attachInterrupt( DccProcState.ExtIntNum, ExternalInterruptHandler, RISING);
   #endif
@@ -1593,6 +1779,7 @@ void NmraDcc::init( uint8_t ManufacturerId, uint8_t VersionId, uint8_t Flags, ui
   	notifyCVResetFactoryDefault();
 }
 
+#ifndef NODEMCUDCC
 ////////////////////////////////////////////////////////////////////////
 uint8_t NmraDcc::getCV( uint16_t CV )
 {
@@ -1662,9 +1849,14 @@ void NmraDcc::setAccDecDCCAddrNextReceived(uint8_t enable)
 {
   DccProcState.inAccDecDCCAddrNextReceivedMode = enable;
 }
+#endif //#ifndef NODEMCUDCC
 
 ////////////////////////////////////////////////////////////////////////
+#ifdef NODEMCUDCC
+static uint8_t process (os_param_t param, uint8_t prio)
+#else
 uint8_t NmraDcc::process()
+#endif
 {
   if( DccProcState.inServiceMode )
   {
@@ -1679,6 +1871,8 @@ uint8_t NmraDcc::process()
     // We need to do this check with interrupts disabled
 #ifdef ESP32
     portENTER_CRITICAL(&mux);
+#elif defined(NODEMCUDCC)
+    ETS_GPIO_INTR_DISABLE();
 #else
     noInterrupts();
 #endif
@@ -1687,6 +1881,8 @@ uint8_t NmraDcc::process()
 
 #ifdef ESP32
     portEXIT_CRITICAL(&mux);
+#elif defined(NODEMCUDCC)
+    ETS_GPIO_INTR_ENABLE();
 #else
     interrupts();
 #endif
@@ -1699,9 +1895,26 @@ uint8_t NmraDcc::process()
     
 	if( notifyDccMsg ) 	notifyDccMsg( &Msg );
 		
+        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 ;
   }
 
   return 0 ;  
 };
+
+#ifdef NODEMCUDCC
+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);
+}
+#endif
+

+ 58 - 4
app/include/driver/NmraDcc.h

@@ -39,6 +39,13 @@
 //
 //------------------------------------------------------------------------
 
+// NodeMCU Lua port by @voborsky
+
+#define NODEMCUDCC
+// #define NODE_DEBUG
+// #define DCC_DEBUG
+// #define DCC_DBGVAR
+
 // Uncomment the following Line to Enable Service Mode CV Programming
 #define NMRA_DCC_PROCESS_SERVICEMODE
 
@@ -50,6 +57,7 @@
 
 #if defined(ARDUINO) && ARDUINO >= 100
 #include "Arduino.h"
+#elif defined(NODEMCUDCC)
 #else
 #include "WProgram.h"
 #endif
@@ -204,9 +212,14 @@ typedef struct countOf_t {
     unsigned long Err;
 }countOf_t ;
 
+#ifdef NODEMCUDCC
+countOf_t countOf;
+#else
 extern struct countOf_t countOf;
 #endif
+#endif
 
+#ifndef NODEMCUDCC
 class NmraDcc
 {
   private:
@@ -214,6 +227,7 @@ class NmraDcc
     
   public:
     NmraDcc();
+#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
@@ -225,7 +239,7 @@ class NmraDcc
 // Flag Bits that are cloned from CV29 relating the DCC Accessory Decoder 
 #define FLAGS_CV29_BITS		(FLAGS_OUTPUT_ADDRESS_MODE | FLAGS_DCC_ACCESSORY_DECODER)
 
-
+#ifndef NODEMCUDCC
   /*+
    *  pin() is called from setup() and sets up the pin used to receive DCC packets.
    *
@@ -403,6 +417,32 @@ void pin( uint8_t ExtIntPinNum, uint8_t EnablePullup);
 
 };
 
+#else
+    #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
+    #define CV_ACK_COMPLETE    14
+
+
+    void dcc_setup(uint8_t pin, uint8_t ManufacturerId, uint8_t VersionId, uint8_t Flags, uint8_t OpsModeAddressBaseCV);
+
+
+    void dcc_close();
+
+    void dcc_init();
+#endif //#ifndef NODEMCUDCC
+
 /************************************************************************************
     Call-back functions
 ************************************************************************************/
@@ -608,7 +648,11 @@ extern void    notifyDccMsg( DCC_MSG * Msg ) __attribute__ ((weak));
  *    1         - CV is valid.
  *    0         - CV is not valid.
  */
+#ifdef NODEMCUDCC
+extern uint16_t notifyCVValid( uint16_t CV, uint8_t Writable ) __attribute__ ((weak));
+#else
 extern uint8_t notifyCVValid( uint16_t CV, uint8_t Writable ) __attribute__ ((weak));
+#endif
 
 /*+
  *  notifyCVRead()  Callback to read a CV.
@@ -622,10 +666,13 @@ extern uint8_t notifyCVValid( uint16_t CV, uint8_t Writable ) __attribute__ ((we
  *    CV        - CV number.
  *
  *  Returns:
- *    Value     - Value of the CV.
+ *    Value     - Value of the CV. Or a value > 255 to indicate error.
  */
+#ifdef NODEMCUDCC
+extern uint16_t notifyCVRead( uint16_t CV) __attribute__ ((weak));
+#else
 extern uint8_t notifyCVRead( uint16_t CV) __attribute__ ((weak));
-
+#endif
 /*+
  *  notifyCVWrite() Callback to write a value to a CV.
  *                  This is called when the library needs to write
@@ -639,10 +686,15 @@ extern uint8_t notifyCVRead( uint16_t CV) __attribute__ ((weak));
  *    Value     - Value of the CV.
  *
  *  Returns:
- *    Value     - Value of the CV.
+ *    Value     - Value of the CV. Or a value > 255 to signal error. 
  */
+#ifdef NODEMCUDCC
+extern uint16_t notifyCVWrite( uint16_t CV, uint8_t Value) __attribute__ ((weak));
+#else
 extern uint8_t notifyCVWrite( uint16_t CV, uint8_t Value) __attribute__ ((weak));
+#endif
 
+#ifndef NODEMCUDCC
 /*+
  *  notifyIsSetCVReady()  Callback to to determine if CVs can be written.
  *                        This is called when the library needs to determine
@@ -681,6 +733,7 @@ extern uint8_t notifyIsSetCVReady(void) __attribute__ ((weak));
  */
 extern void    notifyCVChange( uint16_t CV, uint8_t Value) __attribute__ ((weak));
 extern void    notifyDccCVChange( uint16_t CV, uint8_t Value) __attribute__ ((weak));
+#endif
 
 /*+
  *  notifyCVResetFactoryDefault() Called when CVs must be reset.
@@ -743,3 +796,4 @@ extern void notifyDccSigState( uint16_t Addr, uint8_t OutputIndex, uint8_t State
 #endif
 
 #endif
+

+ 161 - 36
app/modules/dcc.c

@@ -1,4 +1,4 @@
-// NodeMCU Lua port by @voborsky
+// NodeMCU Lua port by @voborsky, @pjsg
 // Module for handling NMRA DCC protocol
 // #define NODE_DEBUG
 
@@ -6,6 +6,7 @@
 #include "lauxlib.h"
 #include "platform.h"
 #include "driver/NmraDcc.h"
+#include "hw_timer.h"
 
 #ifdef LUA_USE_MODULES_DCC
 #if !defined(GPIO_INTERRUPT_ENABLE) || !defined(GPIO_INTERRUPT_HOOK_ENABLE)
@@ -16,6 +17,8 @@
 #define TYPE "Type"
 #define OPERATION "Operation"
 
+#define TIMER_OWNER (('D' << 8) + 'C')
+
 static inline void register_lua_cb(lua_State* L,int* cb_ref){
   int ref=luaL_ref(L, LUA_REGISTRYINDEX);
   if( *cb_ref != LUA_NOREF){
@@ -33,6 +36,15 @@ static inline void unregister_lua_cb(lua_State* L, int* cb_ref){
 
 static int notify_cb = LUA_NOREF;
 static int CV_cb = LUA_NOREF;
+static int CV_ref = LUA_NOREF;
+static int8_t ackPin;
+static char ackInProgress;
+static platform_task_handle_t tasknumber;
+
+typedef struct {
+  uint16_t cv;
+  uint8_t value;
+} CVData;
 
 // DCC commands
 
@@ -154,8 +166,11 @@ void notifyServiceMode(bool InServiceMode){
 
 // CV handling
 
-uint8_t notifyCVValid( uint16_t CV, uint8_t Writable ) { 
+uint16_t notifyCVValid( uint16_t CV, uint8_t Writable ) { 
   lua_State* L = lua_getstate();
+  if (CV_ref != LUA_NOREF) {
+    return 1;
+  }
   if(notify_cb == LUA_NOREF)
     return 0;
   lua_rawgeti(L, LUA_REGISTRYINDEX, CV_cb);
@@ -165,13 +180,45 @@ uint8_t notifyCVValid( uint16_t CV, uint8_t Writable ) {
   cbAddFieldInteger(L, Writable, "Writable");
   if (luaL_pcallx(L, 2, 1) != LUA_OK)
     return 0;
-  uint8 result = lua_tointeger(L, -1);
+  uint8 result = lua_tointeger(L, -1) || lua_toboolean(L, -1);
   lua_pop(L, 1);
   return result;
 }
 
-uint8_t notifyCVRead( uint16_t CV) { 
+static int doDirectCVRead(lua_State *L) {
+  CVData *data = (CVData*) lua_touserdata(L, -1);
+  lua_rawgeti(L, LUA_REGISTRYINDEX, CV_ref);
+  lua_pushinteger(L, data->cv);
+  lua_gettable(L, -2);
+  data->value = (uint8_t) luaL_checkinteger(L, -1);
+  return 0;
+}
+
+static int doDirectCVWrite(lua_State *L) {
+  CVData *data = (CVData*) lua_touserdata(L, -1);
+  lua_rawgeti(L, LUA_REGISTRYINDEX, CV_ref);
+  lua_pushinteger(L, data->cv);
+  lua_pushinteger(L, data->value);
+  lua_settable(L, -3);
+  return 0;
+}
+
+uint16_t notifyCVRead( uint16_t CV) { 
   lua_State* L = lua_getstate();
+  if (CV_ref != LUA_NOREF) {
+    CVData data;
+    data.cv = CV;
+
+    lua_pushcfunction(L, doDirectCVRead);
+    lua_pushlightuserdata(L, &data);
+    if (lua_pcall(L, 1, 0, 0)) {
+      // An error.
+      lua_pop(L, 1);
+      return 256;
+    }
+    
+    return data.value;
+  }
   if(notify_cb == LUA_NOREF)
     return 0;
   lua_rawgeti(L, LUA_REGISTRYINDEX, CV_cb);
@@ -185,8 +232,23 @@ uint8_t notifyCVRead( uint16_t CV) {
   return result;
 }
 
-uint8_t notifyCVWrite( uint16_t CV, uint8_t Value) { 
+uint16_t notifyCVWrite( uint16_t CV, uint8_t Value) { 
   lua_State* L = lua_getstate();
+  if (CV_ref != LUA_NOREF) {
+    CVData data;
+    data.cv = CV;
+    data.value = Value;
+
+    lua_pushcfunction(L, doDirectCVWrite);
+    lua_pushlightuserdata(L, &data);
+    if (lua_pcall(L, 1, 0, 0)) {
+      // An error.
+      lua_pop(L, 1);
+      return 256;
+    }
+    
+    return data.value;
+  }
   if(notify_cb == LUA_NOREF)
     return 0;
   lua_rawgeti(L, LUA_REGISTRYINDEX, CV_cb);
@@ -194,53 +256,106 @@ uint8_t notifyCVWrite( uint16_t CV, uint8_t Value) {
   lua_newtable(L);
   cbAddFieldInteger(L, CV, "CV");
   cbAddFieldInteger(L, Value, "Value");
-  luaL_pcallx(L, 2, 0);
+  luaL_pcallx(L, 2, 1);
+  // Return is an optional value (if integer). If nil, then it is old style
+  if (!lua_isnil(L, -1)) {
+    Value = lua_tointeger(L, -1);
+  }
+  lua_pop(L, 1);
   return Value;
 }
 
-void notifyCVResetFactoryDefault(void) { 
+static void notifyCVNoArgs(int callback_type) {
   lua_State* L = lua_getstate();
-  if(notify_cb == LUA_NOREF)
+  if (notify_cb == LUA_NOREF) {
     return;
+  }
   lua_rawgeti(L, LUA_REGISTRYINDEX, CV_cb);
-  lua_pushinteger(L, CV_RESET);
+  lua_pushinteger(L, callback_type);
   luaL_pcallx(L, 1, 0);
 }
 
+void notifyCVResetFactoryDefault(void) { 
+  notifyCVNoArgs(CV_RESET);
+}
+
+void notifyCVAck(void) {
+  // Invoked when we should generate an ack pulse (if possible)
+  if (ackPin >= 0 && !ackInProgress) {
+    // Duration is 6ms +/- 1ms
+    platform_hw_timer_arm_us(TIMER_OWNER, 6 * 1000);
+    platform_gpio_write(ackPin, 1);
+    ackInProgress = TRUE;
+  }
+}
+
+static void ICACHE_RAM_ATTR cvAckComplete(os_param_t param) {
+  // Invoked when we should end the ack pulse
+  ackInProgress = FALSE;
+  platform_gpio_write(ackPin, 0);
+  if (CV_ref == LUA_NOREF) {
+    platform_post_high(tasknumber, CV_ACK_COMPLETE);
+  }
+}
+
 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");
+  int narg = 1;
+  uint8_t pin = luaL_checkinteger(L, narg);
+  luaL_argcheck(L, platform_gpio_exists(pin) && pin>0, narg, "Invalid interrupt pin");
+  narg++;
+
+  int8_t ackpin = -1;
+
+  if (lua_type(L, narg) == LUA_TNUMBER) {
+    ackpin = luaL_checkinteger(L, narg);
+    luaL_argcheck(L, platform_gpio_exists(ackpin), narg, "Invalid ack pin");
+    narg++;
+  } 
   
-  if (lua_type(L, 2) == LUA_TFUNCTION)
-  {
-    lua_pushvalue(L, 2);
+  if (lua_isfunction(L, narg)) {
+    lua_pushvalue(L, narg++);
     register_lua_cb(L, &notify_cb);
-  }
-  else
-  {
+  } else {
     unregister_lua_cb(L, &notify_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_pushvalue(L, 7);
-    register_lua_cb(L, &CV_cb);
+
+  uint8_t ManufacturerId = luaL_checkinteger(L, narg++);
+  uint8_t VersionId = luaL_checkinteger(L, narg++);
+  uint8_t Flags = luaL_checkinteger(L, narg++);
+  uint8_t OpsModeAddressBaseCV = luaL_checkinteger(L, narg++);
+
+  if (lua_istable(L, narg)) {
+    // This is the raw CV table
+    lua_pushvalue(L, narg++);
+    register_lua_cb(L, &CV_ref);
+  } else {
+    unregister_lua_cb(L, &CV_ref);
   }
-  else
-  {
+
+  if (lua_isfunction(L, narg)) {
+    lua_pushvalue(L, narg++);
+    register_lua_cb(L, &CV_cb);
+  } else {
     unregister_lua_cb(L, &CV_cb);
   }
 
+  if (ackpin >= 0) {
+    // Now start things up
+    if (!platform_hw_timer_init(TIMER_OWNER, FRC1_SOURCE, FALSE)) {
+      // Failed to init the timer
+      luaL_error(L, "Unable to initialize timer");
+    }
+
+    platform_hw_timer_set_func(TIMER_OWNER, cvAckComplete, 0);
+
+    platform_gpio_write(ackpin, 0);
+    platform_gpio_mode(ackpin, PLATFORM_GPIO_OUTPUT, PLATFORM_GPIO_FLOAT);
+  }
+
   NODE_DBG("[dcc_lua_setup] Enabling interrupt on PIN %d\n", pin);
+  ackPin = ackpin;
+  ackInProgress = FALSE;
   dcc_setup(pin, ManufacturerId, VersionId, Flags, OpsModeAddressBaseCV );
   
   return 0;
@@ -249,13 +364,22 @@ static int dcc_lua_setup(lua_State* L) {
 static int dcc_lua_close(lua_State* L) {
   dcc_close();
   unregister_lua_cb(L, &notify_cb);
+  unregister_lua_cb(L, &CV_cb);
+  unregister_lua_cb(L, &CV_ref);
   return 0;
 }
 
-int luaopen_dcc( lua_State *L ) {
-  NODE_DBG("[dcc_luaopen]\n");
+static void dcc_task(os_param_t param, uint8_t prio)
+{
+  (void) prio;
+
+  notifyCVNoArgs(param);
+}
+
+int dcc_lua_init( lua_State *L ) {
+  NODE_DBG("[dcc_lua_init]\n");
   dcc_init();
-  //DccRx.lua_cb_ref = LUA_NOREF;
+  tasknumber = platform_task_get_id(dcc_task);
   return 0;
 }
 
@@ -278,6 +402,7 @@ LROT_BEGIN(dcc, NULL, 0)
   LROT_NUMENTRY( CV_READ, CV_READ )
   LROT_NUMENTRY( CV_WRITE, CV_WRITE )
   LROT_NUMENTRY( CV_RESET, CV_RESET )
+  LROT_NUMENTRY( CV_ACK_COMPLETE, CV_ACK_COMPLETE )
 
   LROT_NUMENTRY( MAN_ID_JMRI, MAN_ID_JMRI)
   LROT_NUMENTRY( MAN_ID_DIY, MAN_ID_DIY)
@@ -289,4 +414,4 @@ LROT_BEGIN(dcc, NULL, 0)
   LROT_NUMENTRY( FLAGS_DCC_ACCESSORY_DECODER, FLAGS_DCC_ACCESSORY_DECODER )
 LROT_END(dcc, NULL, 0)
 
-NODEMCU_MODULE(DCC, "dcc", dcc, luaopen_dcc);
+NODEMCU_MODULE(DCC, "dcc", dcc, dcc_lua_init);

+ 12 - 8
docs/modules/dcc.md

@@ -7,23 +7,25 @@ The dcc module implements decoder of the [National Model Railroad Association](h
 
 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).
+The module is based on the project NmraDcc [https://github.com/mrrwa/NmraDcc](https://github.com/mrrwa/NmraDcc) by Alex Shepherd (@kiwi64ajs). The module is based on latest commit from Oct 2020, commit [7e3b3e346991d74926e6c4f6fb46e27156b08578](https://github.com/mrrwa/NmraDcc/tree/7e3b3e346991d74926e6c4f6fb46e27156b08578).
 
 ## dcc.setup()
 
 Initializes the dcc module and links callback functions.
 
 #### Syntax
-`dcc.setup(DCC_command, ManufacturerId, VersionId, Flags, OpsModeAddressBaseCV, CV_callback)`
+`dcc.setup(Pin, [AckPin, ] DCC_command, ManufacturerId, VersionId, Flags, OpsModeAddressBaseCV [, CV_table] [, CV_callback])`
 
 #### Parameters
+- `Pin` the GPIO pin number connected to the DCC detector (must be interrupt capable pin).
+- `AckPin` the (optional) GPIO pin number connected to the ACK mechanism. Will be set HIGH to signal an ACK.
 - `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` 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_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`.
@@ -35,11 +37,13 @@ Initializes the dcc module and links callback functions.
     - `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_table`  The CV values will be directly accessed from this table. metamethods will be invoked if needed. Any errors thrown will cause the CV to be considered invalid. Using this option will prevent `CV_VALID`, `CV_READ`, `CV_WRITE` and `CV_ACK_COMPLETE` from happening.
 - `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_VALID` to determine if a given CV is valid and (possibly) writable. This callback must determine if a CV is readable or writable and return the appropriate value(0/1/true/false). The `param` collection has members `CV` and `Writable`.
+    -  `dcc.CV_READ` to read a CV. This callback must return the value of the CV. The `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. The `param` collection has members `CV` and `Value`. Ideally, the final value should be returned -- this may differ from the requested value.
     -  `dcc.CV_RESET` Called when CVs must be reset to their factory defaults. 
+    -  `dcc.CV_ACK_COMPLETE` Called when an ACK pulse has finished being sent. Only invoked if `AckPin` is specified.
 
 #### Returns
 `nil`