diff --git a/EXRAILMacros.h b/EXRAILMacros.h
index 7fcfc73..b2ee920 100644
--- a/EXRAILMacros.h
+++ b/EXRAILMacros.h
@@ -63,6 +63,10 @@
// playing sounds with IO_I2CDFPlayer
#define PLAYSOUND ANOUT
+// SEG7 is a helper to create ANOUT from a 7-segment requets
+#define SEG7(vpin,value,format) \
+ ANOUT(vpin,(value & 0xFFFF),TM1638::DF_##format,((uint32_t)value)>>16)
+
// helper macro to strip leading zeros off time inputs
// (10#mins)%100)
#define STRIP_ZERO(value) 10##value%100
diff --git a/IODevice.h b/IODevice.h
index 05df4df..09fd08d 100644
--- a/IODevice.h
+++ b/IODevice.h
@@ -570,6 +570,6 @@ protected:
#include "IO_EncoderThrottle.h"
#include "IO_TCA8418.h"
#include "IO_NeoPixel.h"
-
+#include "IO_TM1638.h"
#endif // iodevice_h
diff --git a/IO_TM1638.cpp b/IO_TM1638.cpp
new file mode 100644
index 0000000..45614f6
--- /dev/null
+++ b/IO_TM1638.cpp
@@ -0,0 +1,217 @@
+/*
+ * © 2024, Chris Harlow. 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 .
+ */
+
+/* Credit to https://github.com/dvarrel/TM1638 for the basic formulae.*/
+
+
+#include
+#include "IODevice.h"
+#include "DIAG.h"
+
+
+const uint8_t HIGHFLASH _digits[16]={
+ 0b00111111,0b00000110,0b01011011,0b01001111,
+ 0b01100110,0b01101101,0b01111101,0b00000111,
+ 0b01111111,0b01101111,0b01110111,0b01111100,
+ 0b00111001,0b01011110,0b01111001,0b01110001
+ };
+
+ // Constructor
+ TM1638::TM1638(VPIN firstVpin, byte clk_pin,byte dio_pin,byte stb_pin){
+ _firstVpin = firstVpin;
+ _nPins = 8;
+ _clk_pin = clk_pin;
+ _stb_pin = stb_pin;
+ _dio_pin = dio_pin;
+ pinMode(clk_pin,OUTPUT);
+ pinMode(stb_pin,OUTPUT);
+ pinMode(dio_pin,OUTPUT);
+ _pulse = PULSE1_16;
+
+ _buttons=0;
+ _leds=0;
+ _lastLoop=micros();
+ addDevice(this);
+ }
+
+
+ void TM1638::create(VPIN firstVpin, byte clk_pin,byte dio_pin,byte stb_pin) {
+ if (checkNoOverlap(firstVpin,8))
+ new TM1638(firstVpin, clk_pin,dio_pin,stb_pin);
+ }
+
+ void TM1638::_begin() {
+ displayClear();
+ test();
+ _display();
+ }
+
+
+ void TM1638::_loop(unsigned long currentMicros) {
+ if (currentMicros - _lastLoop > (1000000UL/LoopHz)) {
+ _buttons=getButtons();// Read the buttons
+ _lastLoop=currentMicros;
+ }
+ }
+
+ void TM1638::_display() {
+ DIAG(F("TM1638 Configured on Vpins:%u-%u"), _firstVpin, _firstVpin+_nPins-1);
+ }
+
+// digital read gets button state
+int TM1638::_read(VPIN vpin) {
+ byte pin=vpin - _firstVpin;
+ bool result=bitRead(_buttons,pin);
+ // DIAG(F("TM1638 read (%d) buttons %x = %d"),pin,_buttons,result);
+ return result;
+}
+
+// digital write sets led state
+void TM1638::_write(VPIN vpin, int value) {
+ // TODO.. skip if no state change
+ writeLed(vpin - _firstVpin + 1,value!=0);
+ }
+
+// Analog write sets digit displays
+
+void TM1638::_writeAnalogue(VPIN vpin, int lowBytes, uint8_t mode, uint16_t highBytes) {
+ // mode is in DataFormat defined above.
+ byte formatLength=mode & 0x0F; // last 4 bits
+ byte formatType=mode & 0xF0; //
+ int8_t leftDigit=vpin-_firstVpin; // 0..7 from left
+ int8_t rightDigit=leftDigit+formatLength-1; // 0..7 from left
+
+ // loading is done right to left startDigit first
+ int8_t startDigit=7-rightDigit; // reverse as 7 on left
+ int8_t lastDigit=7-leftDigit; // reverse as 7 on left
+ uint32_t value=highBytes;
+ value<<=16;
+ value |= (uint16_t)lowBytes;
+
+ //DIAG(F("TM1638 fl=%d ft=%x sd=%d ld=%d v=%l vx=%X"),
+ // formatLength,formatType,startDigit,lastDigit,value,value);
+ while(startDigit<=lastDigit) {
+ switch (formatType) {
+ case _DF_DECIMAL:// decimal (leading zeros)
+ displayDig(startDigit,GETHIGHFLASH(_digits,(value%10)));
+ value=value/10;
+ break;
+ case _DF_HEX:// HEX (leading zeros)
+ displayDig(startDigit,GETHIGHFLASH(_digits,(value & 0x0F)));
+ value>>=4;
+ break;
+ case _DF_RAW:// Raw 7-segment pattern
+ displayDig(startDigit,value & 0xFF);
+ value>>=8;
+ break;
+ default:
+ DIAG(F("TM1368 invalid mode 0x%x"),mode);
+ return;
+ }
+ startDigit++;
+ }
+}
+
+uint8_t TM1638::getButtons(){
+ ArduinoPins::fastWriteDigital(_stb_pin, LOW);
+ writeData(INSTRUCTION_READ_KEY);
+ pinMode(_dio_pin, INPUT);
+ ArduinoPins::fastWriteDigital(_clk_pin, LOW);
+ uint8_t buttons=0;
+ for (uint8_t eachByte=0; eachByte<4;eachByte++) {
+ uint8_t value = 0;
+ for (uint8_t eachBit = 0; eachBit < 8; eachBit++) {
+ ArduinoPins::fastWriteDigital(_clk_pin, HIGH);
+ value |= ArduinoPins::fastReadDigital(_dio_pin) << eachBit;
+ ArduinoPins::fastWriteDigital(_clk_pin, LOW);
+ }
+ buttons |= value << eachByte;
+ delayMicroseconds(1);
+ }
+ pinMode(_dio_pin, OUTPUT);
+ ArduinoPins::fastWriteDigital(_stb_pin, HIGH);
+ return buttons;
+}
+
+
+void TM1638::displayDig(uint8_t digitId, uint8_t pgfedcba){
+ if (digitId>7) return;
+ setDataInstruction(DISPLAY_TURN_ON | _pulse);
+ setDataInstruction(INSTRUCTION_WRITE_DATA| INSTRUCTION_ADDRESS_FIXED);
+ writeDataAt(FIRST_DISPLAY_ADDRESS+14-(digitId*2), pgfedcba);
+}
+
+void TM1638::displayClear(){
+ setDataInstruction(DISPLAY_TURN_ON | _pulse);
+ setDataInstruction(INSTRUCTION_WRITE_DATA | INSTRUCTION_ADDRESS_FIXED);
+ for (uint8_t i=0;i<15;i+=2){
+ writeDataAt(FIRST_DISPLAY_ADDRESS+i,0x00);
+ }
+}
+
+void TM1638::writeLed(uint8_t num,bool state){
+ if ((num<1) | (num>8)) return;
+ setDataInstruction(DISPLAY_TURN_ON | _pulse);
+ setDataInstruction(INSTRUCTION_WRITE_DATA | INSTRUCTION_ADDRESS_FIXED);
+ writeDataAt(FIRST_DISPLAY_ADDRESS + (num*2-1), state);
+}
+
+
+void TM1638::writeData(uint8_t data){
+ for (uint8_t i = 0; i < 8; i++) {
+ ArduinoPins::fastWriteDigital(_dio_pin, data & 1);
+ data >>= 1;
+ ArduinoPins::fastWriteDigital(_clk_pin, HIGH);
+ ArduinoPins::fastWriteDigital(_clk_pin, LOW);
+ }
+}
+
+void TM1638::writeDataAt(uint8_t displayAddress, uint8_t data){
+ ArduinoPins::fastWriteDigital(_stb_pin, LOW);
+ writeData(displayAddress);
+ writeData(data);
+ ArduinoPins::fastWriteDigital(_stb_pin, HIGH);
+ delayMicroseconds(1);
+}
+
+void TM1638::setDataInstruction(uint8_t dataInstruction){
+ ArduinoPins::fastWriteDigital(_stb_pin, LOW);
+ writeData(dataInstruction);
+ ArduinoPins::fastWriteDigital(_stb_pin, HIGH);
+ delayMicroseconds(1);
+}
+
+void TM1638::test(){
+ DIAG(F("TM1638 test"));
+ uint8_t val=0;
+ for(uint8_t i=0;i<5;i++){
+ setDataInstruction(DISPLAY_TURN_ON | _pulse);
+ setDataInstruction(INSTRUCTION_WRITE_DATA| INSTRUCTION_ADDRESS_AUTO);
+ ArduinoPins::fastWriteDigital(_stb_pin, LOW);
+ writeData(FIRST_DISPLAY_ADDRESS);
+ for(uint8_t i=0;i<16;i++)
+ writeData(val);
+ ArduinoPins::fastWriteDigital(_stb_pin, HIGH);
+ delay(1000);
+ val = ~val;
+ }
+
+}
+
+
diff --git a/IO_TM1638.h b/IO_TM1638.h
new file mode 100644
index 0000000..9907e6a
--- /dev/null
+++ b/IO_TM1638.h
@@ -0,0 +1,134 @@
+ /*
+ * © 2024, Chris Harlow. 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 .
+ */
+
+#ifndef IO_TM1638_h
+#define IO_TM1638_h
+#include
+#include "IODevice.h"
+#include "DIAG.h"
+
+class TM1638 : public IODevice {
+private:
+
+ uint8_t _buttons;
+ uint8_t _leds;
+ unsigned long _lastLoop;
+ static const int LoopHz=20;
+
+ static const byte
+ INSTRUCTION_WRITE_DATA=0x40,
+ INSTRUCTION_READ_KEY=0x42,
+ INSTRUCTION_ADDRESS_AUTO=0x40,
+ INSTRUCTION_ADDRESS_FIXED=0x44,
+ INSTRUCTION_NORMAL_MODE=0x40,
+ INSTRUCTION_TEST_MODE=0x48,
+
+ FIRST_DISPLAY_ADDRESS=0xC0,
+
+ DISPLAY_TURN_OFF=0x80,
+ DISPLAY_TURN_ON=0x88;
+
+
+ uint8_t _clk_pin;
+ uint8_t _stb_pin;
+ uint8_t _dio_pin;
+ uint8_t _pulse;
+ bool _isOn;
+
+
+ // Constructor
+ TM1638(VPIN firstVpin, byte clk_pin,byte dio_pin,byte stb_pin);
+
+public:
+ enum DigitFormat : byte {
+ // last 4 bits are length.
+ // DF_1.. DF_8 decimal
+ DF_1=0x01,DF_2=0x02,DF_3=0x03,DF_4=0x04,
+ DF_5=0x05,DF_6=0x06,DF_7=0x07,DF_8=0x08,
+ // DF_1X.. DF_8X HEX
+ DF_1X=0x11,DF_2X=0x12,DF_3X=0x13,DF_4X=0x14,
+ DF_5X=0x15,DF_6X=0x16,DF_7X=0x17,DF_8X=0x18,
+ // DF_1R .. DF_4R raw 7 segmnent data
+ // only 4 because HAL analogWrite only passes 4 bytes
+ DF_1R=0x21,DF_2R=0x22,DF_3R=0x23,DF_4R=0x24,
+
+ // bits of data conversion type (ored with length)
+ _DF_DECIMAL=0x00,// right adjusted decimal unsigned leading zeros
+ _DF_HEX=0x10, // right adjusted hex leading zeros
+ _DF_RAW=0x20 // bytes are raw 7-segment pattern (max length 4)
+ };
+
+ static void create(VPIN firstVpin, byte clk_pin,byte dio_pin,byte stb_pin);
+
+ // Functions overridden in IODevice
+ void _begin();
+ void _loop(unsigned long currentMicros) override ;
+ void _writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) override;
+ void _display() override ;
+ int _read(VPIN pin) override;
+ void _write(VPIN pin,int value) override;
+
+ // Device driving functions
+ private:
+ enum pulse_t {
+ PULSE1_16,
+ PULSE2_16,
+ PULSE4_16,
+ PULSE10_16,
+ PULSE11_16,
+ PULSE12_16,
+ PULSE13_16,
+ PULSE14_16
+ };
+
+ /**
+ * @fn getButtons
+ * @return state of 8 buttons
+ */
+ uint8_t getButtons();
+
+ /**
+ * @fn writeLed
+ * @brief put led ON or OFF
+ * @param num num of led(1-8)
+ * @param state (true or false)
+ */
+ void writeLed(uint8_t num, bool state);
+
+
+ /**
+ * @fn displayDig
+ * @brief set 7 segment display + dot
+ * @param digitId num of digit(0-7)
+ * @param val value 8 bits
+ */
+ void displayDig(uint8_t digitId, uint8_t pgfedcba);
+
+ /**
+ * @fn displayClear
+ * @brief switch off all leds and segment display
+ */
+ void displayClear();
+ void test();
+ void writeData(uint8_t data);
+ void writeDataAt(uint8_t displayAddress, uint8_t data);
+ void setDisplayMode(uint8_t displayMode);
+ void setDataInstruction(uint8_t dataInstruction);
+};
+#endif
diff --git a/KeywordHasher.h b/KeywordHasher.h
index 4b1e0fb..fe85eb4 100644
--- a/KeywordHasher.h
+++ b/KeywordHasher.h
@@ -54,4 +54,43 @@ static_assert("MAIN"_hk == 11339,"Keyword hasher error");
static_assert("SLOW"_hk == -17209,"Keyword hasher error");
static_assert("SPEED28"_hk == -17064,"Keyword hasher error");
static_assert("SPEED128"_hk == 25816,"Keyword hasher error");
+
+// Compile time converter from "abcd"_s7 to the 7 segment nearest equivalent
+
+constexpr uint8_t seg7Digits[]={
+ 0b00111111,0b00000110,0b01011011,0b01001111, // 0..3
+ 0b01100110,0b01101101,0b01111101,0b00000111, // 4..7
+ 0b01111111,0b01101111 // 8..9
+ };
+
+constexpr uint8_t seg7Letters[]={
+ 0b01110111,0b01111100,0b00111001,0b01011110, // ABCD
+ 0b01111001,0b01110001,0b00111101,0b01110110, // EFGH
+ 0b00000100,0b00011110,0b01110010,0b00111000, //IJKL
+ 0b01010101,0b01010100,0b01011100,0b01110011, // MNOP
+ 0b10111111,0b01010000,0b01101101,0b01111000, // QRST
+ 0b00111110,0b00011100,0b01101010,0b01001001, //UVWX
+ 0b01100110,0b01011011 //YZ
+ };
+constexpr uint8_t seg7Space=0b00000000;
+constexpr uint8_t seg7Minus=0b01000000;
+constexpr uint8_t seg7Equals=0b01001000;
+
+
+constexpr uint32_t CompiletimeSeg7(const char * sv, uint32_t running, size_t rlen) {
+ return (*sv==0 || rlen==0) ? running << (8*rlen) : CompiletimeSeg7(sv+1,
+ (*sv >= '0' && *sv <= '9') ? (running<<8) | seg7Digits[*sv-'0'] :
+ (*sv >= 'A' && *sv <= 'Z') ? (running<<8) | seg7Letters[*sv-'A'] :
+ (*sv >= 'a' && *sv <= 'z') ? (running<<8) | seg7Letters[*sv-'a'] :
+ (*sv == '-') ? (running<<8) | seg7Minus :
+ (*sv == '=') ? (running<<8) | seg7Equals :
+ (running<<8) | seg7Space,
+ rlen-1
+ ); //
+}
+
+constexpr uint32_t operator""_s7(const char * keyword, size_t len)
+{
+ return CompiletimeSeg7(keyword,0*len,4);
+}
#endif
\ No newline at end of file
diff --git a/Release_Notes/TM1638.md b/Release_Notes/TM1638.md
new file mode 100644
index 0000000..adec7ae
--- /dev/null
+++ b/Release_Notes/TM1638.md
@@ -0,0 +1,84 @@
+## TM1638 ##
+
+The TM1638 board provides a very cheap way of implementing 8 buttons, 8 leds and an 8 digit 7segment display in a package requiring just 5 Dupont wires (vcc, gnd + 3 GPIO pins) from the command station without soldering.
+
+
+This is ideal for prototyping and testing, simulating sensors and signals, displaying states etc. For a built layout, this could provide a control for things that are not particularly suited to throttle 'route' buttons, perhaps lineside automations or fiddle yard lane selection.
+
+By adding a simple HAL statement to myAutomation.h it creates 8 buttons/sensors and 8 leds.
+
+`HAL(TM1638,500,29,31,33)`
+Creates VPINs 500-507 And desscribes the GPIO pins used to connect the clk,dio,stb pins on the TM1638 board.
+
+Setting each of the VPINs will control the associated LED (using for example SET, RESET or BLINK in Exrail or ` from a command).
+
+Unlike most pins, you can also read the same pin number and get the button state, using Exrail IF/AT/ONBUTTON etc.
+
+For example:
+`
+HAL(TM1638,500,29,31,33)
+`
+All the folowing examples assume you are using VPIN 500 as the first, leftmost, led/button on the TM1638 board.
+
+
+`ONBUTTON(500)
+ SET(500) // light the first led
+ BLINK(501,500,500) // blink the second led
+ SETLOCO(3) FWD(50) // set a loco going
+ AT(501) STOP // press second button to stop
+ RESET(500) RESET(501) // turn leds off
+ DONE
+`
+
+Buttons behave like any other sensor, so using `` will cause the command station to issue `` and `` messages when the first button is pressed or released.
+
+Exrail `JMRI_SENSOR(500,8)` will create `