diff --git a/IO_VCNL4040.h b/IO_VCNL4040.h
new file mode 100644
index 0000000..47a11c5
--- /dev/null
+++ b/IO_VCNL4040.h
@@ -0,0 +1,344 @@
+/*
+ * © 2023, Fred Decker. All rights reserved.
+ *
+ * This file is part of DCC++EX API
+ *
+ * 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 VCNL4040 is a combination IR proximity sensor (PS) and ambient light sensor
+ * (ALS) with a distance sensing range limit of 20cm (about 7.5 inches) and an
+ * ambient light sensing range of 0.0125 to 6553 lux. For object (train) detection
+ * purposes, we are only supporting the proximity features of the sensor.
+ *
+ * Can set:
+ *
+ * led duty cycle: 40, 80, 160, 320ms (???)
+ * led drive current: 200 (Affects led brightness, therefore distance and sensitivity)
+ * persistence: (higher values reduce false triggers)
+ *
+ * The basic PS function settings, such as duty ratio, integration time,
+interrupt, and PS enable / disable, and persistence, are handled by the register: PS_CONF1. Duty ratio controls the PS response
+time. Integration time represents the duration of the energy being received. The interrupt is triggered when the PS detection
+levels over the high threshold level setting (register: PS_THDH) or lower than low threshold (register: PS_THDL). If the interrupt
+function is enabled, the host can react to the interrupt pin, instead of polling the PS data registers. The INT flag (register:
+INT_Flag) indicates the type of interrupt that has been triggered, depending on the interrupt settings in the configuration
+registers. PS persistence (PS_PERS) sets up the PS INT trigger conditions, defining the amount of consecutive hits required
+before an interrupt event occurs. The intelligent cancellation level can be set on register: PS_CANC to reduce the cross talk
+phenomenon.
+VCNL4040 also supports an easy to use proximity detection logic mode, that triggers when the PS high threshold is exceeded
+and automatically resets the interrupt pin when the proximity reading falls beneath the PS low threshold. This functionality can
+be set in the register: PS_MS. A smart persistence is provided to be able to prevent false PS interrupt trigger events.
+Descriptions of each of these parameters are shown in table 1.
+
+REGISTER: PS_CONF1 COMMAND CODE: 0x03_L (0x03 DATA BYTE LOW)
+Command Bit Description
+PS_Duty 7 : 6 (0 : 0) = 1/40, (0 : 1) = 1/80, (1 : 0) = 1/160, (1 : 1) = 1/320
+ PS IRED on / off duty ratio setting
+PS_PERS 5 : 4 (0 : 0) = 1, (0 : 1) = 2, (1 : 0) = 3, (1 : 1) = 4
+ PS interrupt persistence setting
+PS_ IT 3 : 1 (0 : 0 : 0) = 1T, (0 : 0 : 1) = 1.5T, (0 : 1 : 0) = 2T, (0 : 1 : 1) = 2.5T
+ (1 : 0 : 0) = 3T, (1 : 0 : 1) = 3.5T, (1 : 1 : 0) = 4T, (1 : 1 : 1) = 8T
+ PS integration time setting
+PS_SD 0 0 = PS power on, 1 = PS shut down, default = 1
+
+REGISTER: PS_CONF2 COMMAND CODE: 0x03_H (0x03 DATA BYTE HIGH)
+PS_HD 3 0 = PS output is 12 bits; 1 = PS output is 16 bits
+Reserved 2 Default = 0
+PS_INT 1 : 0 (0 : 0) = interrupt disable, (0 : 1) = trigger when close,
+ (1 : 0)= trigger when away, (1 : 1)= trigger when close or away
+
+fred gave up here. too many options to cover
+ *
+ *
+ fnd stopped here
+ * the reflection of the pulse. The time between the pulse and the receipt of reflections
+ * is measured and used to determine the distance to the reflecting object.
+ *
+ * For economy of memory and processing time, this driver includes only part of the code
+ * that ST provide in their API. Also, the API code isn't very clear and it is not easy
+ * to identify what operations are useful and what are not.
+ * The operation shown here doesn't include any calibration, so is probably not as accurate
+ * as using the full driver, but it's probably accurate enough for the purpose.
+ *
+ * The device driver allocates up to 3 vpins to the device. A digital read on the first pin
+ * will return a value that indicates whether the object is within the threshold range (1)
+ * or not (0). An analogue read on the first pin returns the last measured distance (in mm),
+ * the second pin returns the signal strength, and the third pin returns detected
+ * ambient light level. By default the device takes around 60ms to complete a ranging
+ * operation, so we do a 100ms cycle (10 samples per second).
+ *
+ * The VL53L0X is initially set to respond to I2C address 0x29. If you only have one module,
+ * you can use this address. However, the address can be modified by software. If
+ * you select another address, that address will be written to the device and used until the device is reset.
+ *
+ * If you have more than one module, then you will need to specify a digital VPIN (Arduino
+ * digital output or I/O extender pin) which you connect to the module's XSHUT pin. Now,
+ * when the device driver starts, the XSHUT pin is set LOW to turn the module off. Once
+ * all VL53L0X modules are turned off, the driver works through each module in turn by
+ * setting XSHUT to HIGH to turn the module on,, then writing the module's desired I2C address.
+ * In this way, many VL53L0X modules can be connected to the one I2C bus, each one
+ * using a distinct I2C address.
+ *
+ * WARNING: If the device's XSHUT pin is not connected, then it is very prone to noise,
+ * and the device may even reset when handled. If you're not using XSHUT, then it's
+ * best to tie it to +5V.
+ *
+ * The driver is configured as follows:
+ *
+ * Single VL53L0X module:
+ * VL53L0X::create(firstVpin, nPins, i2cAddress, lowThreshold, highThreshold);
+ * Where firstVpin is the first vpin reserved for reading the device,
+ * nPins is 1, 2 or 3,
+ * i2cAddress is the address of the device (normally 0x29),
+ * lowThreshold is the distance at which the digital vpin state is set to 1 (in mm),
+ * and highThreshold is the distance at which the digital vpin state is set to 0 (in mm).
+ *
+ * Multiple VL53L0X modules:
+ * VL53L0X::create(firstVpin, nPins, i2cAddress, lowThreshold, highThreshold, xshutPin);
+ * ...
+ * Where firstVpin is the first vpin reserved for reading the device,
+ * nPins is 1, 2 or 3,
+ * i2cAddress is the address of the device (any valid address except 0x29),
+ * lowThreshold is the distance at which the digital vpin state is set to 1 (in mm),
+ * highThreshold is the distance at which the digital vpin state is set to 0 (in mm),
+ * and xshutPin is the VPIN number corresponding to a digital output that is connected to the
+ * XSHUT terminal on the module.
+ *
+ * Example:
+ * In mySetup function within mySetup.cpp:
+ * VL53L0X::create(4000, 3, 0x29, 200, 250);
+ * Sensor::create(4000, 4000, 0); // Create a sensor
+ *
+ * When an object comes within 200mm of the sensor, a message
+ *
+ * will be sent over the serial USB, and when the object moves more than 250mm from the sensor,
+ * a message
+ *
+ * will be sent.
+ *
+ */
+
+#ifndef IO_VCNL4040_h
+#define IO_VCNL4040_h
+
+#include "IODevice.h"
+
+class VCNL4040 : public IODevice {
+private:
+ uint8_t _i2cAddress;
+ uint16_t _ambient;
+ uint16_t _distance;
+ uint16_t _signal;
+ uint16_t _onThreshold;
+ uint16_t _offThreshold;
+ VPIN _xshutPin;
+ bool _value;
+ uint8_t _nextState = 0;
+ I2CRB _rb;
+ uint8_t _inBuffer[12];
+ uint8_t _outBuffer[2];
+ // State machine states.
+ enum : uint8_t {
+ STATE_INIT = 0,
+ STATE_CONFIGUREADDRESS = 1,
+ STATE_SKIP = 2,
+ STATE_CONFIGUREDEVICE = 3,
+ STATE_INITIATESCAN = 4,
+ STATE_CHECKSTATUS = 5,
+ STATE_GETRESULTS = 6,
+ STATE_DECODERESULTS = 7,
+ };
+
+ // Register addresses
+ enum : uint8_t {
+ VL53L0X_REG_SYSRANGE_START=0x00,
+ VL53L0X_REG_RESULT_INTERRUPT_STATUS=0x13,
+ VL53L0X_REG_RESULT_RANGE_STATUS=0x14,
+ VL53L0X_CONFIG_PAD_SCL_SDA__EXTSUP_HV=0x89,
+ VL53L0X_REG_I2C_SLAVE_DEVICE_ADDRESS=0x8A,
+ };
+ const uint8_t VL53L0X_I2C_DEFAULT_ADDRESS=0x29;
+
+public:
+ VL53L0X(VPIN firstVpin, int nPins, uint8_t i2cAddress, uint16_t onThreshold, uint16_t offThreshold, VPIN xshutPin = VPIN_NONE) {
+ _firstVpin = firstVpin;
+ _nPins = min(nPins, 3);
+ _i2cAddress = i2cAddress;
+ _onThreshold = onThreshold;
+ _offThreshold = offThreshold;
+ _xshutPin = xshutPin;
+ _value = 0;
+ addDevice(this);
+ }
+ static void create(VPIN firstVpin, int nPins, uint8_t i2cAddress, uint16_t onThreshold, uint16_t offThreshold, VPIN xshutPin = VPIN_NONE) {
+ new VL53L0X(firstVpin, nPins, i2cAddress, onThreshold, offThreshold, xshutPin);
+ }
+
+protected:
+ void _begin() override {
+ if (_xshutPin == VPIN_NONE) {
+ // Check if device is already responding on the nominated address.
+ if (I2CManager.exists(_i2cAddress)) {
+ // Yes, it's already on this address, so skip the address initialisation.
+ _nextState = STATE_CONFIGUREDEVICE;
+ } else {
+ _nextState = STATE_INIT;
+ }
+ }
+ }
+
+ void _loop(unsigned long currentMicros) override {
+ uint8_t status;
+ switch (_nextState) {
+ case STATE_INIT:
+ // On first entry to loop, reset this module by pulling XSHUT low. All modules
+ // will be reset in turn.
+ if (_xshutPin != VPIN_NONE) IODevice::write(_xshutPin, 0);
+ _nextState = STATE_CONFIGUREADDRESS;
+ break;
+ case STATE_CONFIGUREADDRESS:
+ // On second entry, set XSHUT pin high to allow the module to restart.
+ // On the module, there is a diode in series with the XSHUT pin to
+ // protect the low-voltage pin against +5V.
+ if (_xshutPin != VPIN_NONE) IODevice::write(_xshutPin, 1);
+ // Allow the module time to restart
+ delay(10);
+ // Then write the desired I2C address to the device, while this is the only
+ // module responding to the default address.
+ I2CManager.write(VL53L0X_I2C_DEFAULT_ADDRESS, 2, VL53L0X_REG_I2C_SLAVE_DEVICE_ADDRESS, _i2cAddress);
+ _nextState = STATE_SKIP;
+ break;
+ case STATE_SKIP:
+ // Do nothing on the third entry.
+ _nextState = STATE_CONFIGUREDEVICE;
+ break;
+ case STATE_CONFIGUREDEVICE:
+ // On next entry, check if device address has been set.
+ if (I2CManager.exists(_i2cAddress)) {
+ #ifdef DIAG_IO
+ _display();
+ #endif
+ // Set 2.8V mode
+ write_reg(VL53L0X_CONFIG_PAD_SCL_SDA__EXTSUP_HV,
+ read_reg(VL53L0X_CONFIG_PAD_SCL_SDA__EXTSUP_HV) | 0x01);
+ } else {
+ DIAG(F("VL53L0X I2C:x%x device not responding"), _i2cAddress);
+ _deviceState = DEVSTATE_FAILED;
+ }
+ _nextState = STATE_INITIATESCAN;
+ break;
+ case STATE_INITIATESCAN:
+ // Not scanning, so initiate a scan
+ _outBuffer[0] = VL53L0X_REG_SYSRANGE_START;
+ _outBuffer[1] = 0x01;
+ I2CManager.write(_i2cAddress, _outBuffer, 2, &_rb);
+ _nextState = STATE_CHECKSTATUS;
+ break;
+ case STATE_CHECKSTATUS:
+ status = _rb.status;
+ if (status == I2C_STATUS_PENDING) return; // try next time
+ if (status != I2C_STATUS_OK) {
+ DIAG(F("VL53L0X I2C:x%x Error:%d %S"), _i2cAddress, status, I2CManager.getErrorMessage(status));
+ _deviceState = DEVSTATE_FAILED;
+ _value = false;
+ } else
+ _nextState = 2;
+ delayUntil(currentMicros + 95000); // wait for 95 ms before checking.
+ _nextState = STATE_GETRESULTS;
+ break;
+ case STATE_GETRESULTS:
+ // Ranging completed. Request results
+ _outBuffer[0] = VL53L0X_REG_RESULT_RANGE_STATUS;
+ I2CManager.read(_i2cAddress, _inBuffer, 12, _outBuffer, 1, &_rb);
+ _nextState = 3;
+ delayUntil(currentMicros + 5000); // Allow 5ms to get data
+ _nextState = STATE_DECODERESULTS;
+ break;
+ case STATE_DECODERESULTS:
+ // If I2C write still busy, return.
+ status = _rb.status;
+ if (status == I2C_STATUS_PENDING) return; // try again next time
+ if (status == I2C_STATUS_OK) {
+ if (!(_inBuffer[0] & 1)) return; // device still busy
+ uint8_t deviceRangeStatus = ((_inBuffer[0] & 0x78) >> 3);
+ if (deviceRangeStatus == 0x0b) {
+ // Range status OK, so use data
+ _ambient = makeuint16(_inBuffer[7], _inBuffer[6]);
+ _signal = makeuint16(_inBuffer[9], _inBuffer[8]);
+ _distance = makeuint16(_inBuffer[11], _inBuffer[10]);
+ if (_distance <= _onThreshold)
+ _value = true;
+ else if (_distance > _offThreshold)
+ _value = false;
+ }
+ }
+ // Completed. Restart scan on next loop entry.
+ _nextState = STATE_INITIATESCAN;
+ break;
+ default:
+ break;
+ }
+ }
+
+ // For analogue read, first pin returns distance, second pin is signal strength, and third is ambient level.
+ int _readAnalogue(VPIN vpin) override {
+ int pin = vpin - _firstVpin;
+ switch (pin) {
+ case 0:
+ return _distance;
+ case 1:
+ return _signal;
+ case 2:
+ return _ambient;
+ default:
+ return -1;
+ }
+ }
+
+ // For digital read, return zero for all but first pin.
+ int _read(VPIN vpin) override {
+ if (vpin == _firstVpin)
+ return _value;
+ else
+ return 0;
+ }
+
+ void _display() override {
+ DIAG(F("VL53L0X I2C:x%x Configured on Vpins:%d-%d On:%dmm Off:%dmm %S"),
+ _i2cAddress, _firstVpin, _firstVpin+_nPins-1, _onThreshold, _offThreshold,
+ (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F(""));
+ }
+
+
+private:
+ inline uint16_t makeuint16(byte lsb, byte msb) {
+ return (((uint16_t)msb) << 8) | lsb;
+ }
+ uint8_t write_reg(uint8_t reg, uint8_t data) {
+ // write byte to register
+ uint8_t outBuffer[2];
+ outBuffer[0] = reg;
+ outBuffer[1] = data;
+ return I2CManager.write(_i2cAddress, outBuffer, 2);
+ }
+ uint8_t read_reg(uint8_t reg) {
+ // read byte from register and return value
+ I2CManager.read(_i2cAddress, _inBuffer, 1, ®, 1);
+ return _inBuffer[0];
+ }
+};
+
+#endif // IO_VL53L0X_h