Forum Rule: Always post complete source code & details to reproduce any issue!
Results 1 to 24 of 24

Thread: Encoders (once more)

  1. #1
    Senior Member+ Theremingenieur's Avatar
    Join Date
    Feb 2014
    Location
    Colmar, France
    Posts
    2,592

    Encoders (once more)

    This time, it's simply about these small and cheap rotary encoder knobs which one uses often as input controls, replacing analog potentiometers or whatever. Basically, these are quadrature encoders like others, but when using these as user control inputs, for example for a midi device, you don't need a huge numeric range to count millions of turns at 0.1 angle resolution.

    The problem with these mechanical switches is that these bounce. That means that instead of simply closing a contact once, it will bounce back and forth tenths of times during a few milliseconds before remaining stable which might lead to wrong results. That's why one needs to do either hardware debouncing with external components (resistor-capacitor filters, schmitt triggers), or software debouncing, filtering the wrong impulses out and/or waisting time with delay().

    Most encoder drivers or libraries I've seen attach interrupts to the encoder pins which means that even if the code will filter the wrong pulses out, a lot of unnecessary interrupts could (and would) still be fired by the bouncing contacts, eating up precious cpu cycles.

    That's why I decided to write my own simple and compact rotKnob class (templated, thanks to @tni who taught us that). The approach to filter bouncing is as follows: Knowing that the rotary knob will pull down and let up pins A and B alternating, there is no reason to handle for example multiple consecutive interrupts (due to contact bouncing) on pinA after the first one. That's why the first pinA interrupt will disable further pinA interrupts with a quick port config register write until an interrupt at pinB will occur, which will disable pinB interrupts and re-enable pinA interrupts and vice versa. That means that independent of contact bouncing, for turning the rotary knob by one detent, only exactly 4 interrupts A-B-A-B or B-A-B-A will be fired and handled, not more. Since the class is templated by pin pairs, one might use as many of these rotary knobs as one wants as long as there are enough free digital pins, since the memory and CPU impact is greatly reduced. Just declare these as rotKnob<pinA1,pinB1> enc1; ... rotKnob<pinAx, pinBx> encx; before setup() in your sketch as in the following example and have fun:
    Code:
    /* Compact and quick encoder class for these horribly bouncing
     * cheap rotary knobs with 4 transitions per click.
     * Default range is from -128 to +127 starting at 0.
     *
     * Might be initialized with begin(startVal); to start at
     * a predefined number.
     * Might be initialized with begin(startVal, lowBound, hiBound);
     * to reduce the range, see examples below.
     * Works with all TEENSY 3.x processors.
     *
     * Free to use for everybody - by (ThF) 2017 */
    
    template<uint8_t pinA, uint8_t pinB>
    class rotKnob {
    public:
    	void begin(int8_t startVal, int8_t lowBound, int8_t hiBound) {
    		d._sValue = startVal;
    		d._lBound = lowBound;
    		d._hBound = hiBound;
    		d._aConf = digital_pin_to_info_PGM[(pinA)].config;
    		d._bConf = digital_pin_to_info_PGM[(pinB)].config;
    		pinMode(pinA, INPUT_PULLUP);
    		pinMode(pinB, INPUT_PULLUP);
    		delay(1);
    		d._aVal = digitalReadFast(pinA);
    		d._bVal = digitalReadFast(pinB);
    		attachInterrupt(pinA, intPinA, CHANGE);
    		attachInterrupt(pinB, intPinB, CHANGE);
    	}
    	void begin(int8_t startVal) {
    		begin(startVal, -128, 127);
    	}
    	void begin() {
    		begin(0, -128, 127);
    	}
    	void end() {
    		detachInterrupt(pinA);
    		detachInterrupt(pinB);
    	}
    	int8_t read() {
    		d._avail = false;
    		return d._sValue;
    	}
    	bool available() {
    		return d._avail;
    	}
    private:
    	struct objData {
    		volatile uint32_t* _aConf, *_bConf;
    		volatile int8_t _lBound, _hBound, _sValue;
    		volatile bool _aVal, _bVal, _avail;
    	};
    	static objData d;
    	static void intPinA() {
    		*d._aConf &= ~0x000F0000; // disable pin A interrupts
                    // decoding logic
    		if (!d._aVal) {
    			if ((d._sValue < d._hBound) && !d._bVal) {
    				d._sValue++;
    				d._avail = true;
    			}
    			if ((d._sValue > d._lBound) && d._bVal) {
    				d._sValue--;
    				d._avail = true;
    			}
    		}
    		d._bVal = digitalReadFast(pinB); // read pinB which is stable after pinA transition
    		*d._bConf |= 0x000B0000; // (re-) enable pinB interrupts
    	}
    	static void intPinB() {
    		*d._bConf &= ~0x000F0000; // disable pinB interrupts
    		d._aVal = digitalReadFast(pinA); // read pinA which is stable after pinB transition
    		*d._aConf |= 0x000B0000; // (re-) enable pinA interrupts
    	}
    };
    template<uint8_t pinA, uint8_t pinB>
    typename rotKnob<pinA, pinB>::objData rotKnob<pinA, pinB>::d;
    
    /* Describe your hardware connections, pins and knobs */
    rotKnob<18, 19> enc1;
    rotKnob<7, 8> enc2;
    
    void setup() {
    	Serial.begin(57600);
    	while (!Serial)
    		;
    	delay(5);
    	pinMode(LED_BUILTIN, OUTPUT);
    	/* Configure your encoders */
    	enc1.begin();          // will start at 0 and go from -128 to 127
    	enc2.begin(20, 0, 40); // will start at 20 and go from 0 to 40
    }
    
    void loop() {
    	if (enc1.available()) {
    		Serial.print("Encoder 1: ");
    		Serial.println(enc1.read());
    	}
    	if (enc2.available()) {
    		Serial.print("Encoder 2: ");
    		Serial.println(enc2.read());
    	}
    }
    Last edited by Theremingenieur; 06-05-2017 at 10:27 AM.

  2. #2
    Senior Member+ KurtE's Avatar
    Join Date
    Jan 2014
    Posts
    8,183
    Quote Originally Posted by Theremingenieur View Post
    That's why I decided to write my own simple and compact rotKnob class (templated, thanks to @tni who taught us that).
    ...
    Since the class is templated by pin pairs, one might use as many of these rotary knobs as one wants as long as there are enough free digital pins, since the memory and CPU impact is greatly reduced. Just declare these as rotKnob<pinA1,pinB1> enc1; ... rotKnob<pinAx, pinBx> encx;
    Thanks!

    Still trying to get my head around all of the newer stuff of C+ (different thread) and working my way through 4th edition of the C++ book... As I mention still getting head around things like templates. So wondering pros/cons of using template here versus simply having a simple class whose constructor takes two pin numbers.

    My gut tells me your version with templates may be faster and smaller for one instance, as the compiler can probably directly compile in the pin numbers directly into the code without having to load from member variable... But what does it do when I have multiple instances? Replicate all of the code for each instance? So maybe bigger? Again this is my gut wondering this and it has been wrong before! So trying to learn new tricks!

    Thanks again

  3. #3
    Senior Member+ Theremingenieur's Avatar
    Join Date
    Feb 2014
    Location
    Colmar, France
    Posts
    2,592
    Yes, thanks to the template parameters, there will be one replicated instance for each pin pair.

    There is one major problem when using interrupt routines as object member functions, since these have to be static, so that the interrupt vector can correctly be set. When I had multiple (non templated) encoder instances with the pin pair in the constructor, all would share the same static ISR routines which is not wanted. I need to know in the code which pin has triggered the interrupt. Thus, I would have to waist much time reading and comparing all affected (and bouncing!) pins in that shared ISR to see what happens which is not very efficient.

    Now from your questions, @KurtE, I decided to do compile tests with the sketch instatiating (and using) varying number of encoders. With Teensy 3.6, 180MHz, Faster without LTO settings, I got the following:
    Code:
    1 Encoder  : 14808 bytes flash, 5588 bytes global variables
    2 Encoders : 15112 bytes flash, 5604 bytes global variables
    3 Encoders : 15424 bytes flash, 5620 bytes global variables
    4 Encoders : 15768 bytes flash, 5636 bytes global variables
    5 Encoders : 16080 bytes flash, 5652 bytes global variables
    That allows to deduce that every new instance eats just a little more than 300 bytes flash and 16 bytes global variables.
    Last edited by Theremingenieur; 06-05-2017 at 02:50 PM.

  4. #4
    Senior Member
    Join Date
    Jul 2014
    Posts
    2,990
    Quote Originally Posted by KurtE View Post
    But what does it do when I have multiple instances? Replicate all of the code for each instance? So maybe bigger? Again this is my gut wondering this and it has been wrong before! So trying to learn new tricks!
    reading also about templates, I realize (but I may be wrong) that templates are simply instructions to the compiler on how to create functions/classes etc.
    So my conclusion is use of templates increases code size.
    Also, it seams that templates make it harder to debug run-time errors (e.g. code runs fine with 'short' or 'int' but fails with 'float' or 'complex', because nobody implemented the specific functionality)

  5. #5
    Senior Member+ KurtE's Avatar
    Join Date
    Jan 2014
    Posts
    8,183
    Thanks again,

    Yes - I totally understand the idea of having the ISR be part of the actual object and not have to do kludges to make it work... Could use some of that with the SPI code.
    Nice stuff for that!

    Still need to learn more, like if hypothetically I have three SPI classes built from template, I probably then can't be able to pass for example SPI or SPI1 or SPI2 as reference to some class that hopefully can work with any of them...

    Wonder if I was doing this in the full template power, if it would make sense to pass in, a type for the values, currently you use int8_t... and potentially maybe the increment value... That way maybe one could use float values like range of 0.0 to 1.0 with increment of 0.1...

    But if one implements the additional functionality I mentioned, probably need to add additional code. Example: if the type is unsigned and the increment can cause it to go logically negative, it may never find it and wrap around!

    More random wondering... May have to experiement. Things like: would it make sense to move the parameters in begin to template parameters? Would it then generate smaller code? Or will the compiler do this anyway? Likewise suppose I add a direction setting on the template, On some I may rotate right to increment and left decrement, but others the opposite... If I then add code if (_reverse) x -= _incr; else x += _incr; Will the compiler remove all of the unnecessary code?

    But feel free to ignore all of this, as I am just thinking out laud as a learning exercise!
    Last edited by KurtE; 06-05-2017 at 04:41 PM.

  6. #6
    Senior Member
    Join Date
    Jan 2013
    Posts
    843
    Quote Originally Posted by KurtE View Post
    Still need to learn more, like if hypothetically I have three SPI classes built from template, I probably then can't be able to pass for example SPI or SPI1 or SPI2 as reference to some class that hopefully can work with any of them...
    They are distinct / incompatible classes. There is no inheritance relationship, unless you introduce a common parent.

    Wonder if I was doing this in the full template power, if it would make sense to pass in, a type for the values, currently you use int8_t... and potentially maybe the increment value... That way maybe one could use float values like range of 0.0 to 1.0 with increment of 0.1...
    My code does that:
    https://forum.pjrc.com/threads/43862...ncoders-as-pot
    https://github.com/tni/teensy-sample...pped_encoder.h

  7. #7
    Senior Member
    Join Date
    Apr 2014
    Location
    Germany
    Posts
    1,309
    Quote Originally Posted by tni View Post
    They are distinct / incompatible classes. There is no inheritance relationship, unless you introduce a common parent.
    Wouldn't that destroy the original attempt to optimize the code by passing pin numbers etc as template parameters? Is there something like a best practice for defining a class which uses say two passed in rotKnob<int,int> objects?

  8. #8
    Senior Member
    Join Date
    Feb 2015
    Posts
    162
    ooh im gonna have to try this one out. I am getting bouncing problems with an Alps encoder and the Encoder library.

  9. #9
    Senior Member+ Theremingenieur's Avatar
    Join Date
    Feb 2014
    Location
    Colmar, France
    Posts
    2,592
    @tenkai: Feel free to write feedback here. Although I found my code performing well and being compact, there is always room for some improvements, I think.

  10. #10
    Senior Member
    Join Date
    Dec 2015
    Location
    LA
    Posts
    191
    While this code looks good for the described purpose I'm not sure it works like the typical quadrature decoder should. It looks like it only changes values at one sensor position and not the other. Even worse after moving over a sensor it won't change when you move back until the next pass. Looks like a divide by two quad encoder. As long as not used for servo position feedback its ok.

    Am I missing something?

  11. #11
    Senior Member+ Theremingenieur's Avatar
    Join Date
    Feb 2014
    Location
    Colmar, France
    Posts
    2,592
    Quote Originally Posted by bicycleguy View Post
    While this code looks good for the described purpose I'm not sure it works like the typical quadrature decoder should. It looks like it only changes values at one sensor position and not the other. Even worse after moving over a sensor it won't change when you move back until the next pass. Looks like a divide by two quad encoder. As long as not used for servo position feedback its ok.

    Am I missing something?
    As I wrote in my first posting, the code is for these rotary knob switches which do 4 transitions per detent. It's not a full quadrature decoder.

  12. #12
    Senior Member
    Join Date
    Dec 2016
    Posts
    116
    Hi

    I tested the template yesterday and it give sometimes (well quite often) 2 times the same value. I think that the usage of avail inside the normal flow and inside the interrupts is maybe the reason. Inside the loop it's clearly used non atomic, aka the svalue can be changed after checking avail.
    Running on t3.2 on 24MHz with one rotary encoder and one pushbutton (bounce library), encoder library works flawless btw.
    --> adding a "lastreadvalue" in the template and using one member function to check if it's changed and return the svalue could be a solution. All changes of svalue are then inside the interrupt. I expect that assigning an int to another local to be atomic, although svalue is a int8_t and then I'm not so sure anymore.
    (I would make svalue an int and even move the bounds checking outside the interrupt, but this last change does change the behaviour a bit -in some cases-) The shorter the interrupt the better.

    A more basic question: Would the contacts bounce when a dented encoder is not touched? ---> It will be seldom that more than one (or two) knobs are turned.

  13. #13
    Senior Member+ Theremingenieur's Avatar
    Join Date
    Feb 2014
    Location
    Colmar, France
    Posts
    2,592
    I never got or get 2 times the same value with dented encoders giving 4 transitions per detention, but only when moving slightly forth and back on the same dent which the end user is normally not intended to do... In case you see a different behavior, I’d be interested to know which encoder hardware you use and how you wired it.
    It might also be a problem with the Teensy T3.2 running a such an unusual slow speed (default is 96MHz) - as I wrote, I tested on a T3.6 @180MHz. I will investigate as soon as I find some free time.

  14. #14
    Senior Member
    Join Date
    Dec 2016
    Posts
    116
    Quote Originally Posted by Theremingenieur View Post
    I never got or get 2 times the same value with dented encoders giving 4 transitions per detention, but only when moving slightly forth and back on the same dent which the end user is normally not intended to do... In case you see a different behavior, Id be interested to know which encoder hardware you use and how you wired it.
    It might also be a problem with the Teensy T3.2 running a such an unusual slow speed (default is 96MHz) - as I wrote, I tested on a T3.6 @180MHz. I will investigate as soon as I find some free time.
    It's the " but only when moving slightly forth and back on the same dent which the end user is normally not intended to do...". But I differ in the user intend, in practice I quite often do this while fine tuning a value (with a 30mm knob and yes it should have no effect on the value with dented encoders).

  15. #15
    Senior Member
    Join Date
    Sep 2014
    Posts
    100
    I find this templated class very useful with these kind of encoders, it works really well! I have one question though, as my C++ knowledge is next to nothing. Is there a way to create an array of instances of this class or a pointer, which could be used in a similar manner?
    I want to read 8 rotary encoders and the code would be much more compact and efficient if all encoders were included in an array. Below is an example of how I would image to use it, something that works with the Encoder class (but that's not a template):
    Code:
    rotKnob *rotKnobs[8];
    uint8_t pinsA[8] = { 5, 7, 20, 15, 22, 25, 33, 24 };
    uint8_t pinsB[8] = { 6, 14, 17, 16, 21, 32, 27, 30 };
    
    void setup() {
      for (int i = 0; i < 8; i++) {
        rotKnobs[i] = new rotKnob(pinsA[i], pinsB[i]);
        rotKnobs[i]->begin(0, -999, 999);
      }
    }
    Or something like this so I can reference the encoders by way of indexing. I've read stuff about parent classes etc. but I don't really understand what needs to be done.
    Thanks for the code anyway!

  16. #16
    Senior Member PaulS's Avatar
    Join Date
    Apr 2015
    Location
    Netherlands
    Posts
    397

    Encoders encore

    @ Theremingenieur: thanks, this is by far the best way to read those cheap 75 cent encoders! Running a T3.2 @ 96MHz never gave a wrong readout.
    It took me a while to figure out what you did in that class, but eventually I understood what "digital_pin_to_info_PGM[]" does.
    I stripped down your code to better understand. Here it is just in case someone else initially gets "overwhelmed" by the class.
    Code:
    // https://forum.pjrc.com/threads/44592-Encoders-(once-more)
    
    #define pinA 2
    #define pinB 3
    
    volatile uint8_t valA = 1;          // default pinA value at detent
    volatile uint8_t valB = 1;          // default pinB value at detent
    volatile bool RotationDetected = false;
    volatile int8_t Count = 0;
    volatile uint32_t *confA = digital_pin_to_info_PGM[(pinA)].config;
    volatile uint32_t *confB = digital_pin_to_info_PGM[(pinB)].config;
    
    void setup() {
      Serial.begin(57600);
      
      pinMode(pinA, INPUT_PULLUP);
      pinMode(pinB, INPUT_PULLUP);
    
      attachInterrupt(digitalPinToInterrupt(pinA), ISRpinA, CHANGE);
      attachInterrupt(digitalPinToInterrupt(pinB), ISRpinB, CHANGE);
    }
    
    void loop() {
      if (RotationDetected) {
        Serial.println(Count);
        RotationDetected = false;A
      }
    }
    
    void ISRpinA() {
      *confA &= ~0x000F0000;            // disable pin A interrupts
      if (!valA && !valB) {             // decoding logic
        Count++;
        RotationDetected = true;
      }
      if (!valA && valB) {
        Count--;
        RotationDetected = true;
      }
      valB = digitalReadFast(pinB);     // read pinB which is stable after pinA transition
      *confB |= 0x000B0000;             // (re-)enable pinB interrupts
    }
    
    void ISRpinB() {
      *confB &= ~0x000F0000;            // disable pinB interrupts
      valA = digitalReadFast(pinA);     // read pinA which is stable after pinB transition
      *confA |= 0x000B0000;             // (re-)enable pinA interrupts
    }
    Out of curiousity, I also took such an encoder apart:

    Click image for larger version. 

Name:	Tinytronics EC11 rotary encoder 1.jpg 
Views:	27 
Size:	72.5 KB 
ID:	18306

    Click image for larger version. 

Name:	Tinytronics EC11 rotary encoder 2.jpg 
Views:	18 
Size:	93.3 KB 
ID:	18307

    Regards,
    Paul

  17. #17
    Junior Member
    Join Date
    Sep 2020
    Posts
    4
    I registered just to say this fantastic library has now unfortunately broken for the Teensy 4!

    error: 'const struct digital_pin_bitband_and_config_table_struct' has no member named 'config'

    but checking the digital.c file hardware/teensy/avr/cores/teensy4 it seems the second entry in that struct has changed from config to mux. Changing .config to .mux on these lines allows it to compile:

    d._aConf = digital_pin_to_info_PGM[(pinA)].config;
    d._bConf = digital_pin_to_info_PGM[(pinB)].config;

    However, my encoders are still bouncy and jumping about. I didn't have this problem on the teensy 3 but it could be my hardware.

    Reading this fantastic post which really dives into the details:

    http://l0ner.github.io/2020-08-24_te...ard-way-blink/

    And I think this is highly relevant too!

    https://forum.pjrc.com/threads/58478...ncoder-Library
    https://github.com/mjs513/Teensy-4.x...ncoder-Library
    Last edited by Luke2642; 09-28-2020 at 12:15 PM. Reason: adding link

  18. #18
    Senior Member
    Join Date
    Apr 2014
    Location
    Germany
    Posts
    1,309
    I can't help you with adopting Theremingenieurs library but I can point you to my EnocderTool which works with T3.x and T4.x. It should also be bounce free and has a couple of other nice features like position-change callbacks, limited/periodic count ranges, support of multiplexed encoders polled and interrupt based readout etc.. Here the link to some rudimentary documentation: https://github.com/luni64/EncoderTool/wiki. The reference chapter is pretty complete and there are a couple of examples showing the usage.
    Last edited by luni; 09-28-2020 at 02:47 PM.

  19. #19
    Junior Member
    Join Date
    Sep 2020
    Posts
    4
    Thanks so much!

    Signal_Analysis worked just fine, but the other examples including EncoderButton and Hello_Encoder gives an error on first run, teensy 4.1 arduino ide 1.8.13:

    In file included from /home/luke/Arduino/libraries/EncoderTool/src/EncoderBase.h:2:0,
    from /home/luke/Arduino/libraries/EncoderTool/src/Multiplexed/EncPlexBase.h:4,
    from /home/luke/Arduino/libraries/EncoderTool/src/Multiplexed/EncPlex74165.h:5,
    from /home/luke/Arduino/libraries/EncoderTool/src/EncoderTool.h:3,
    from /home/luke/Arduino/libraries/EncoderTool/examples/01_HelloEncoder/01_HelloEncoder.ino:1:
    /home/luke/Arduino/libraries/EncoderTool/src/EncoderButton.h:19:14: error: 'bool EncoderTool::EncoderButton::readCurrentState()' marked 'override', but does not override
    bool readCurrentState() override { return curState; }
    ^
    Error compiling for board Teensy 4.1.

    Editing EncoderButton.h to remove the "override" fixed it!

    I try not to add namespaces, but it's convenient to extantiate four without it, e.g. EncoderTool::PolledEncoder knob1, knob2, knob3, knob4;

    I couldn't make any sense of the third parameter pinBtn, but setting it =pinA works fine.

    Great except from the documentation:

    "At a first glance it looks like it is necessary to catch all of the bounce pulses to get the correct result. Some libraries use pin change interrupts to catch the bounce pulses. Actually, this is not necessary at all. Here a simple argument: Missing a bounce transition is obviously the same as erasing the transition in the graph above. If you do that and sum up the new +/- 1 steps you will still end up with a total count of +1 after the bouncing."

    Thanks again!
    Last edited by Luke2642; 09-28-2020 at 04:43 PM.

  20. #20
    Senior Member
    Join Date
    Apr 2014
    Location
    Germany
    Posts
    1,309
    Looks like you are using an old version of Bounce which doesn't have readCurrentState(). Removing the override does make the compiler happy but the push button feature won't work. However, as long as you don't use push buttons your change won't do any harm.

    I was 100% sure having mentioned that one needs a recent version of Bounce2 (the version which comes with Teensyduino seems to be outdated). Somehow this never made it into the documentation. Strange, getting old :-)
    I'll add that tomorrow.

    The code base is quite new, let me know if everything works as expected and if additional features would be useful.

  21. #21
    Junior Member
    Join Date
    Sep 2020
    Posts
    4
    Fantastic, got bounce2, reverted my deletion. The buttons now work nicely too, and I do need them!

    Looks like a great library and well documented, thanks again! I particularly like valueChanged being handled behind the scenes needing to remember it.

    I was a little skeptical having to put the tick() in the loop but that little bit I quoted before makes me think it's actually better to use idle cycles rather than an interrupt, so bravo!

  22. #22
    Senior Member
    Join Date
    Apr 2014
    Location
    Germany
    Posts
    1,309
    Perfect.
    ValueChanged and ButtonChanged are useful indeed. But, attaching a change callback to the encoder or button can be even more useful :-).

    I was a little skeptical having to put the tick() in the loop
    The library can do both, polled mode with manually calling tick() and interrupt mode. For mechanical encoders polled mode is most often absolutely sufficient (and it has the push button debouncing feature which is not possible in interrupt mode).

  23. #23
    Junior Member
    Join Date
    Sep 2020
    Posts
    4
    I realise I'm now off the thread completely. It's working great, but I can't seem to attach a callback, although the example is a multiplex not a regular polled encoder?

    Screen_Test_2:176: error: no matching function for call to 'EncoderTool::PolledEncoder::attachCallback(void (&)())'
    knob1.attachCallback(displayPreset);

    I tried putting my void displayPreset() before void setup() although it didn't make a difference.

  24. #24
    Senior Member
    Join Date
    Apr 2014
    Location
    Germany
    Posts
    1,309
    No no, this is how it would work:

    Code:
    void myCallback(int position, int delta)
    {
       Serial.printf("pos:%d delta:%d\n", position, delta);
    }
    
    //....
    void setup()
    { 
      encoder1.attachCallback(myCallback);  
    //....
    }
    Whenever the encoder gets a new value the function myCallback would be called with the current position and the delta to the old position.


    ... But it might be better not to hijack Theremingenieurs thread and open an dedicated one if you have more questions about the EncoderTool...
    Last edited by luni; 09-28-2020 at 09:19 PM.

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •