diff --git a/IO_RocoDriver.h b/IO_RocoDriver.h
new file mode 100644
index 00000000..08ab73bd
--- /dev/null
+++ b/IO_RocoDriver.h
@@ -0,0 +1,157 @@
+/*
+ * © 2024, Chris Harlow. All rights reserved.
+ *
+ * This file is part of EX-CommandStation
+ *
+ * This is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * It is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with CommandStation. If not, see .
+*/
+
+/*
+* The IO_RocoDriver device driver uses a rotary encoder connected to vpins
+* to drive a loco.
+* Loco id is selected by writeAnalog.
+*/
+
+#ifndef IO_ROCODRIVER_H
+#define IO_ROCODRIVER_H
+
+#include "DCC.h"
+#include "IODevice.h"
+#include "DIAG.h"
+
+const byte _DIR_CW = 0x10; // Clockwise step
+const byte _DIR_CCW = 0x20; // Counter-clockwise step
+
+const byte transition_table[5][4]= {
+ {0,1,3,0}, // 0: 00
+ {1,1,1,2 | _DIR_CW}, // 1: 00->01
+ {2,2,0,2}, // 2: 00->01->11
+ {3,3,3,4 | _DIR_CCW}, // 3: 00->10
+ {4,0,4,4} // 4: 00->10->11
+};
+
+const byte _STATE_MASK = 0x07;
+const byte _DIR_MASK = 0x30;
+
+
+class RocoDriver : public IODevice {
+public:
+
+ static void create(VPIN firstVpin, int dtPin, int clkPin, int clickPin, byte notch=10) {
+ if (checkNoOverlap(firstVpin)) new RocoDriver(firstVpin, dtPin,clkPin,clickPin,notch);
+ }
+
+private:
+ int _dtPin,_clkPin,_clickPin, _locoid, _notch,_prevpinstate;
+ enum {xrSTOP,xrFWD,xrREV} _stopState;
+ byte _rocoState;
+
+
+ // Constructor
+ RocoDriver(VPIN firstVpin, int dtPin, int clkPin, int clickPin, byte notch){
+ _firstVpin = firstVpin;
+ _nPins = 1;
+ _I2CAddress = 0;
+ _dtPin=dtPin;
+ _clkPin=clkPin;
+ _clickPin=clickPin;
+ _notch=notch;
+ _locoid=0;
+ _stopState=xrSTOP;
+ _rocoState=0;
+ _prevpinstate=4; // not 01..11
+ IODevice::configureInput(dtPin,true);
+ IODevice::configureInput(clkPin,true);
+ IODevice::configureInput(clickPin,true);
+ addDevice(this);
+ _display();
+ }
+
+
+
+ void _loop(unsigned long currentMicros) override {
+ if (_locoid==0) return; // not in use
+
+ // Clicking down on the roco, stops the loco and sets the direction as unknown.
+ if (IODevice::read(_clickPin)) {
+ if (_stopState==xrSTOP) return; // debounced multiple stops
+ DCC::setThrottle(_locoid,1,DCC::getThrottleDirection(_locoid));
+ _stopState=xrSTOP;
+ DIAG(F("DRIVE %d STOP"),_locoid);
+ return;
+ }
+
+ // read roco pins and detect state change
+ byte pinstate = (IODevice::read(_dtPin) << 1) | IODevice::read(_clkPin);
+ if (pinstate==_prevpinstate) return;
+ _prevpinstate=pinstate;
+
+ _rocoState = transition_table[_rocoState & _STATE_MASK][pinstate];
+ if ((_rocoState & _DIR_MASK) == 0) return; // no value change
+
+ int change=(_rocoState & _DIR_CW)?+1:-1;
+ // handle roco change -1 or +1 (clockwise)
+
+ if (_stopState==xrSTOP) {
+ // first move after button press sets the direction. (clockwise=fwd)
+ _stopState=change>0?xrFWD:xrREV;
+ }
+
+ // when going fwd, clockwise increases speed.
+ // but when reversing, anticlockwise increases speed.
+ // This is similar to a center-zero pot control but with
+ // the added safety that you cant panic-spin into the other
+ // direction.
+ if (_stopState==xrREV) change=-change;
+ // manage limits
+ int oldspeed=DCC::getThrottleSpeed(_locoid);
+ if (oldspeed==1)oldspeed=0; // break out of estop
+ int newspeed=change>0 ? (min((oldspeed+_notch),126)) : (max(0,(oldspeed-_notch)));
+ if (newspeed==1) newspeed=0; // normal decelereated stop.
+ if (oldspeed!=newspeed) {
+ DIAG(F("DRIVE %d notch %S %d %S"),_locoid,
+ change>0?F("UP"):F("DOWN"),_notch,
+ _stopState==xrFWD?F("FWD"):F("REV"));
+ DCC::setThrottle(_locoid,newspeed,_stopState==xrFWD);
+ }
+}
+
+ // Selocoid as analog value to start drive
+ // use
+ void _writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) override {
+ (void) param2;
+ _locoid=value;
+ if (param1>0) _notch=param1;
+ _rocoState=0;
+
+ // If loco is moving, we inherit direction from it.
+ _stopState=xrSTOP;
+ if (_locoid>0) {
+ auto speedbyte=DCC::getThrottleSpeedByte(_locoid);
+ if ((speedbyte & 0x7f) >1) {
+ // loco is moving
+ _stopState= (speedbyte & 0x80)?xrFWD:xrREV;
+ }
+ }
+ _display();
+ }
+
+
+ void _display() override {
+ DIAG(F("DRIVE vpin %d loco %d notch %d"),_firstVpin,_locoid,_notch);
+ }
+
+ };
+
+#endif
diff --git a/version.h b/version.h
index b428c75b..084751dc 100644
--- a/version.h
+++ b/version.h
@@ -3,7 +3,8 @@
#include "StringFormatter.h"
-#define VERSION "5.2.68"
+#define VERSION "5.2.69"
+// 5.2.69 - IO_RocoDriver. Direct drive train with rotary encoder hw.
// 5.2.68 - Revert function map to signed (from 5.2.66) to avoid
// incompatibilities with ED etc for F31 frequency flag.
// 5.2.67 - EXRAIL AFTER optional debounce time variable (default 500mS)