X27 stepper motor that can be controlled from the Arduino directly

  • Plugin Developer

    Here is some code to manage an X27 stepper motor. These are commonly found behind a gauge in car dashboards.

    Because this type of stepper motor uses very little power, an Arduino can operate the stepper motor directly (video), by pulsing the coils.

    The idea is that it can act as a dashboard for certain values in the smart home.

     * This device can toggle two relays, and thus two electric locks. These relays can be controlled via the Candle Controller, but also via SMS (if you want).
     * The SMS function is password protected: you have to send a password to switch the relay. Access can also be limited to only certain mobile phone numbers, and only certain moments (by toggling the Mobile control switch).
     * If you send an SMS without the password, it will be shown as the Incoming SMS. This can be used to automate other things in your home. For example, you could create a rule that when the system receices an SMS with "purifier on" in it, the air purifier will turn on.
     * This way you can control smart devices in your home even if it/Candle is not connected to the internet.
     * If data transmission is disabled, you can change the state of the lock via SMS or the pushbutton without the new state being sent to the controller.
     * SETTINGS */ 
    #define CLOCKWISE                                   // Rotate clockwise. As values increase, the indicator needle will move clockwise. To make the needle move counter clockwise as values increase, uncheck this.
    #define RF_NANO                                     // RF-Nano. Check this box if you are using the RF-Nano Arduino, which has a built in radio. The Candle project uses the RF-Nano.
    // Enable debug prints to serial monitor
    #define DEBUG
    //#define MY_DEBUG
    #define LOOP_DURATION 1000                          // The main loop runs every x milliseconds. This main loop starts the modem, and from then on periodically requests the password.
    #define SECONDS_BETWEEN_HEARTBEAT_TRANSMISSION 120  // The smart lock might not send any data for a very long time if it isn't used. Sending a heartbeat tells the controller: I'm still there.
    #ifdef RF_NANO
    // If you are using an RF-Nano, you have to switch CE and CS pins.
    #define MY_RF24_CS_PIN 9                            // Used by the MySensors library.
    #define MY_RF24_CE_PIN 10                           // Used by the MySensors library.
    // Enable and select radio type attached
    #define MY_RADIO_RF24
    #define MY_RF24_PA_LEVEL RF24_PA_MAX  
    //#define MY_RADIO_RFM69
    // Mysensors advanced settings
    #define MY_TRANSPORT_WAIT_READY_MS 10000            // Try connecting for 10 seconds. Otherwise just continue.
    //#define MY_RF24_CHANNEL 100                       // In EU the default channel 76 overlaps with wifi, so you could try using channel 100. But you will have to set this up on every device, and also on the controller.
    #define MY_RF24_DATARATE RF24_1MBPS                 // Slower datarate makes the network more stable
    //#define MY_RF24_DATARATE RF24_250KBPS             // Slower datarate increases wireless range
    //#define MY_NODE_ID 10                             // Giving a node a manual ID can in rare cases fix connection issues.
    //#define MY_PARENT_NODE_ID 0                       // Fixating the ID of the gatewaynode can in rare cases fix connection issues.
    //#define MY_PARENT_NODE_IS_STATIC                  // Used together with setting the parent node ID. Daking the controller ID static can in rare cases fix connection issues.
    #define MY_SPLASH_SCREEN_DISABLED                   // Saves a little memory.
    //#define MY_DISABLE_RAM_ROUTING_TABLE_FEATURE      // Saves a little memory.
    // Mysensors security
    //#define DEBUG_SIGNING
    #define MY_ENCRYPTION_SIMPLE_PASSWD "changeme"     // Be aware, the length of the password has an effect on memory use.
    //#define MY_SECURITY_SIMPLE_PASSWD "changeme"      // Be aware, the length of the password has an effect on memory use.
    //#define MY_SIGNING_SOFT_RANDOMSEED_PIN A7         // Setting a pin to pickup random electromagnetic noise helps make encryption more secure.
    // Enable repeater functionality for this node
    #include <MySensors.h>
    #define ROTATION_CHILD_ID  3                        // Child ID for the actual rotation input
    #define LOWER_BOUNDARY_ID  4                        // Child ID for the lower boundary value. Starts as 0%
    #define UPPER_BOUNDARY_ID  5                        // Child ID for the upper boundary value. Starts as 100%
    #define RADIO_DELAY 100                             // Milliseconds delay betweeen radio signals. This gives the radio some breathing room.
    MyMessage percentage_message(ROTATION_CHILD_ID, V_PERCENTAGE); // A generic boolean state message.
    boolean send_all_values = 1;
    int percentage = 0;
    int previous_percentage = 0;
    int lower_boundary = 0;
    int upper_boundary = 100;
    // STEPPER
    #include <Stepper.h>
    #define  STEPS  720                                 // steps per revolution (limited to 315°)
    #define  STEPPER_1  3
    #define  STEPPER_2  4
    #define  STEPPER_3  5
    #define  STEPPER_4  6
    // create an instance of the stepper class:
    Stepper stepper(STEPS, STEPPER_1, STEPPER_2, STEPPER_3, STEPPER_4);
    int pos = 0;                                        //Position in steps(0-630)= (0°-315°)
    int val = 0;
    void before()
      pinMode(STEPPER_1, OUTPUT);
      pinMode(STEPPER_2, OUTPUT);
      pinMode(STEPPER_3, OUTPUT);
      pinMode(STEPPER_4, OUTPUT);
    void presentation()
    	// Send the sketch version information to the gateway and Controller
    	sendSketchInfo(F("Gauge"), F("1.0")); wait(RADIO_DELAY);
      present(ROTATION_CHILD_ID, S_DIMMER, F("Rotation") ); wait(RADIO_DELAY);
      present(LOWER_BOUNDARY_ID, S_DIMMER, F("Lower boundary") ); wait(RADIO_DELAY);
      present(UPPER_BOUNDARY_ID, S_DIMMER, F("Upper boundary") ); wait(RADIO_DELAY);
      send_all_values = true;                           // Whenever a new presentation is requested, we should also send the current values of the children.
    void setup()
      Serial.println(F("Hello world, I am gauge."));
      stepper.setSpeed(30);    // set the motor speed to 30 RPM (360 PPS aprox.).
      stepper.step(630);       // Reset Position(630 steps counter-clockwise). 
      if( loadState(LOWER_BOUNDARY_ID) <= 100 ){
        lower_boundary = loadState(LOWER_BOUNDARY_ID);
        Serial.print(F("Loaded lower boundary from eeprom: ")); Serial.println(lower_boundary);
      if( loadState(UPPER_BOUNDARY_ID) <= 100 ){
        upper_boundary = loadState(UPPER_BOUNDARY_ID);
        Serial.print(F("Loaded upper boundary from eeprom: ")); Serial.println(upper_boundary);
      //percentage = loadState(ROTATION_CHILD_ID);
      //if( percentage > 100 ){ percentage = 100; }
      //if( percentage < 0 ){ percentage = 0; } // not really possible
      //Serial.println(F("Sending initial percentage"));
      //send(percentage_message.setSensor(ROTATION_CHILD_ID).set(percentage),1); wait(RADIO_DELAY); // Send initial state
    void send_values()
    #ifdef DEBUG
      Serial.println(F("Sending values and states"));
      send(percentage_message.setSensor(ROTATION_CHILD_ID).set(percentage)); wait(RADIO_DELAY); // Send initial state
      send(percentage_message.setSensor(LOWER_BOUNDARY_ID).set(lower_boundary)); wait(RADIO_DELAY); // Send initial state
      send(percentage_message.setSensor(UPPER_BOUNDARY_ID).set(upper_boundary)); wait(RADIO_DELAY); // Send initial state
      send_all_values = 0;
    void loop()
      if( send_all_values ){
    #ifdef DEBUG
        Serial.println(F("RESENDING VALUES"));
      if( percentage != previous_percentage ){
        previous_percentage = percentage;
        Serial.print(F("Percentage changed to: ")); Serial.println(percentage);
        send(percentage_message.setSensor(ROTATION_CHILD_ID).set(percentage)); // Send on state
        //saveState(ROTATION_CHILD_ID, percentage);
    #ifdef CLOCKWISE
        val = map(percentage,lower_boundary,upper_boundary,0,630);    // map pot range in the stepper range.
        val = map(percentage,lower_boundary,upper_boundary,630,0);    // map pot range in the stepper range.
        Serial.print(F("New val: "));
        Serial.println( val );
      // Move the stepper needle
      if( abs(val - pos) > 2 ){         // if difference is greater than 2 steps.
          if( (val - pos) > 0 ){
              stepper.step(-1);      // move one step to the left.
          if((val - pos) < 0){
              stepper.step(1);       // move one step to the right.
      static unsigned long last_loop_time = 0;          // Holds the last time the main loop ran.
      static byte loop_counter = 0;                     // Count how many loops have passed (reset to 0 after at most 254 loops).
      if( millis() - last_loop_time > LOOP_DURATION ){  // Runs every second
        last_loop_time = millis();
        wdt_reset();                                    // Reset the watchdog timer. If this doesn't happen, the device must have crashed, and it will be automatically rebooted by the watchdog.
        if( loop_counter >= SECONDS_BETWEEN_HEARTBEAT_TRANSMISSION ){ // If a couple of minutes have passed, tell the controller we're still here
          loop_counter = 0;
    #ifdef DEBUG
          Serial.println(F("Sending heartbeat"));
    void receive(const MyMessage &message)
      Serial.print(F(">> Incoming message of type ")); Serial.print(message.type);
      Serial.print(F(" for child:")); Serial.println(message.sensor);
      if( message.isAck() ){
    #ifdef DEBUG
        Serial.println(F("- Echo"));
      else if( message.sensor==ROTATION_CHILD_ID ){
        percentage = message.getInt();
        //Serial.print(", type:");
        //Serial.print(", New value: ");
        //Serial.println( percentage );
      else if( message.sensor==LOWER_BOUNDARY_ID ){                // We only expect one type of message from controller. But we better check anyway.
        lower_boundary = message.getInt();
        saveState(LOWER_BOUNDARY_ID, lower_boundary);
      else if( message.sensor==UPPER_BOUNDARY_ID ){                // We only expect one type of message from controller. But we better check anyway.
        upper_boundary = message.getInt();
        saveState(UPPER_BOUNDARY_ID, upper_boundary);
     * The MySensors Arduino library handles the wireless radio link and protocol
     * between your home built sensors/actuators and HA controller of choice.
     * The sensors forms a self healing radio network with optional repeaters. Each
     * repeater and gateway builds a routing tables in EEPROM which keeps track of the
     * network topology allowing messages to be routed to nodes.
     * Created by Henrik Ekblad <henrik.ekblad@mysensors.org>
     * Copyright (C) 2013-2015 Sensnology AB
     * Full contributor list: https://github.com/mysensors/Arduino/graphs/contributors
     * Documentation: http://www.mysensors.org
     * Support Forum: http://forum.mysensors.org
     * This program is free software; you can redistribute it and/or
     * modify it under the terms of the GNU General Public License
     * version 2 as published by the Free Software Foundation.

    During my testing I noticed that at a certain time the Arduino Nano's USB chip stopped working, so I suspect there is too much kickback from the stepper anyway. Turns out the USB cable broke. It still works fine.

    In theory there's a library out there that works better than using the standard stepper library. But I wasn't able to find it. There is a library for the X25 that might work, but I haven't tried it. But I haven't gotten it to work properly.

    As an alternative, you could use normal small servo's that the Arduino can also operate without requiring a motor control chip. But that's a lot more noisy. It does have a lot more power though.

    I put the code here in case someone wants to play around with it. For those interested, there is also an open source board to make these gauges more robust by adding protective diodes.

Log in to reply

Suggested Topics