Vasudevan Vijaya
Published © GPL3+

Remote Internet Controlled 3 Speed Transmission

The Transmission model built using Lego Mindstorm has 3 speed modes, controlled through Internet using Microsoft IoT hub and Raspberry Pi.

IntermediateFull instructions provided8 hours799
Remote Internet Controlled 3 Speed Transmission

Things used in this project

Hardware components

Raspberry Pi 3 Model B
Raspberry Pi 3 Model B
Raspberry Pi running Windows IoT
×1
LEGO MindStorms Lego NXT Mindstorm
Basic Model Project from http://www.nxtprograms.com/transmission/index.html
×1
Dual H-Bridge motor drivers L293D
Texas Instruments Dual H-Bridge motor drivers L293D
×1
1N4007 – High Voltage, High Current Rated Diode
1N4007 – High Voltage, High Current Rated Diode
×2
Resistor 2.21k ohm
Resistor 2.21k ohm
×2
Resistor 100k ohm
Resistor 100k ohm
×1
Breadboard (generic)
Breadboard (generic)
×1

Software apps and online services

Visual Studio 2015
Microsoft Visual Studio 2015
Used for programming the IoT and also for Remote clientWindows Application
LEGO MindStorms Lego NXT Mindstorm
A small portion of the code to control Clutch and Gear shift motors

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)
Needed to attach single stranded wires to snipped Mindstorm connectos

Story

Read more

Schematics

Schematics

Schematics showing the interface between NXT and Raspberry Pi

Breadboard connection

Breadboard setup to connect to NXT

Code

Main Code File for IoT

C#
A synopsis of main code file is provided for understanding the working of the IoT Device connectivity and Transmission control. Note Device Key is omitted. Replace this with your key. Also Device ID and IoT URI must reflect your values.
using System;
using System.Text;
using Microsoft.IoT.Lightning.Providers;
using Windows.Devices;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.Devices.Gpio;
using Windows.Devices.Pwm;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.Azure.Devices.Client;
using Newtonsoft.Json;

// The Blank Page item template is documented at http://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x409

namespace RemoteGearShift
{
    /// <summary>
    /// An empty page that can be used on its own or navigated to within a Frame.
    /// </summary>
    public sealed partial class MainPage : Page
    {
        private GpioPin MotorOutput0; //Digi00 Motor output - permanently low Digi01 is connected to PwmPin
        private GpioPin Switch; //Output to turn on the Gear Motor
        private GpioPin Reverse; //Output to reverse the gear motor 
        private GpioPin Index; //Input to ensure the gear is in First Position
        private PwmPin pwmpin0; //Pulse width modulation for Drive Motor

        private bool EngineOn = false; //Used to avoid changing gears if the engine is not running
        private bool AutoOn = false; //Used for cruise control
        private int Gear = 1; //Keep track of Gear. Always in first gear. Gear rotates as 1 -2 -3 - 2 - 1
        private bool GearMovement = true; //If Gear movement is true then 1 - 2 - 3. If GearMovement is False then 3 - 2 - 1
        private bool AcceleratorChanging = false; //If slider is moved, wait till process is complete. Used only by Cruise control
        private int RevSpeed; //Revspeed is controlled by sl_Accelerator. Need this for decoding from internet.
        private int PrevSpeed;

        private const int MOTOROUTPUT = 12;//For test only. can be later used for speed measurement using IR control
        private const int MOTORWAIT = 1; //Time for motor to rev up in Seconds
        private const int PWMPIN = 13; //For test only. can be later used for speed measurement using IR control
        private const int SWITCH = 16;
        private const int REVERSE = 21;
        private const int INDEX = 20;
        private const int FIRST = 40; //First Gear speed used by Cruise control
        private const int SECOND = 60; //Second Gear. Any value above 70 will third gear.
        private const int THIRD = 80;
        private const float INITIALPOWER = 40;
        //Variables for Cloud IoT
        public string IotUri = "VasuW10IoT.azure-devices.net";
        public static DeviceClient dc;
        public string DeviceKey = "xxxxxxx";
        public string DeviceId = "W10IoT";
        private bool IsAzureConnected = true;
        private bool InternetControl = false;


        private const int BUMPTIME = 500; //Time to create a switch Bump in milliseconds
        public  MainPage()
        {
            this.InitializeComponent();
            this.Loaded += MainPage_Loaded;            
        }

        private async void MainPage_Loaded(object sender, RoutedEventArgs e)
        {
            if (LightningProvider.IsLightningEnabled)
            {
                LowLevelDevicesController.DefaultProvider = LightningProvider.GetAggregateProvider();
            }
            var pwmControllers = await PwmController.GetControllersAsync(LightningPwmProvider.GetPwmProvider());
            var pwmController = pwmControllers[1];
            pwmController.SetDesiredFrequency(100); //Min: 24hz to Max: 1000 hz

            pwmpin0 = pwmController.OpenPin(PWMPIN);
            pwmpin0.SetActiveDutyCyclePercentage(INITIALPOWER/100);
            pwmpin0.Stop();

            var gpio = await GpioController.GetDefaultAsync();
            if (gpio == null)
                return;

            //Set Accelerator Value
            sl_Accelerator.Value = INITIALPOWER;

            //Assign the constants to gpio pins
            MotorOutput0 = gpio.OpenPin(MOTOROUTPUT);
            Switch = gpio.OpenPin(SWITCH);
            Reverse = gpio.OpenPin(REVERSE);
            Index = gpio.OpenPin(INDEX);

            //set Input or Output of the pins
            MotorOutput0.SetDriveMode(GpioPinDriveMode.Output);
            Switch.SetDriveMode(GpioPinDriveMode.Output);
            Reverse.SetDriveMode(GpioPinDriveMode.Output);
            Index.SetDriveMode(GpioPinDriveMode.Input);

            //Set initial values for output ports
            MotorOutput0.Write(GpioPinValue.Low);
            Switch.Write(GpioPinValue.Low);
            Reverse.Write(GpioPinValue.Low);
            
            //Initialise IoT Cloud values
            dc = DeviceClient.Create(IotUri, new DeviceAuthenticationWithRegistrySymmetricKey(DeviceId, DeviceKey));
            await SendDevicetoCloudMessageAsync();

            RevSpeed = (int)INITIALPOWER;
            PrevSpeed = RevSpeed;

            btnStart.Click += BtnStart_Click;
            btnStop.Click += BtnStop_Click;
            btn_Close.Click += Btn_Close_Click;
            btn_GS.Click += Btn_GS_Click;
            rb_Auto.Click += Rb_Auto_Click;
            rb_Manual.Click += Rb_Manual_Click;
            rbControl.Click += RbControl_Click;
            Index.ValueChanged += Index_ValueChanged;
            
        }

        private void Index_ValueChanged(GpioPin sender, GpioPinValueChangedEventArgs args)
        {
            GpioPinValue gp = Index.Read();
            if (gp == GpioPinValue.Low) //If the gear is already in the First position nothing to do.        }
            {
                GearMovement = true;
                Gear = 1;
                tbGear.Text = string.Format("Forward -> Gear: {0}", Gear);
            }
            else
            {
                GearMovement = true;
                Gear = 2;
                tbGear.Text = string.Format("Forward -> Gear: {0}", Gear);

            }
        }

        private async void RbControl_Click(object sender, RoutedEventArgs e)
        {
            StopEngine();
            InternetControl = (rbControl.IsChecked == true) ? true : false;

            if (InternetControl)
            {
                sl_Accelerator.IsEnabled = false;
                await ReceiveCloudToDeviceMessageAsync();
            }
            else
            {
                sl_Accelerator.IsEnabled = true;
            }
        }

        private void Rb_Manual_Click(object sender, RoutedEventArgs e)
        {
            if (InternetControl)
                return;

            if (!AutoOn) //Already Manual. Nothing to do
                return;

            GearModeSelect(true);

            return;
        }

        private void GearModeSelect(bool bManual)
        {
            if (bManual)
            {
                rb_Manual.IsChecked = true;
                rb_Auto.IsChecked = false;
                AutoOn = false;
            }
            else
            {
                rb_Manual.IsChecked = false;
                rb_Auto.IsChecked = true;
                AutoOn = true;
            }
            StopEngine();
            StartEngine();
        }

        private void Rb_Auto_Click(object sender, RoutedEventArgs e)
        {
            if (InternetControl)
                return;

            if (AutoOn) //Already on Auto. Nothing to do. 
                return;

            GearModeSelect(false);

            return;
        }

        private void Btn_GS_Click(object sender, RoutedEventArgs e)
        {
            if (AutoOn) //Manual shift not possible on cruise control
                return;
            if (!InternetControl)
                GearShift();
        }

        private void GearShift()
        {
            if (!EngineOn)
                return;
            BumpGearSwitch(!GearMovement);


        }

        private void Btn_Close_Click(object sender, RoutedEventArgs e)
        {
            App.Current.Exit();
        }

        private void BtnStop_Click(object sender, RoutedEventArgs e)
        {
            //if (!InternetControl)
            StopEngine(); //Need for emergency control

        }

        private void StopEngine()
        {
            if (!EngineOn) //Engine is already stopped. Nothing to do
                return;

            EngineOn = false;
            pwmpin0.Stop();

        }

        private void BtnStart_Click(object sender, RoutedEventArgs e)
        {
            if (!InternetControl)
                StartEngine();
        }

        private void StartEngine()
        {
            if (EngineOn)
                return;
            EngineOn = true;
            pwmpin0.SetActiveDutyCyclePercentage(INITIALPOWER / 100);
            sl_Accelerator.Value = INITIALPOWER;
            pwmpin0.Start();
            RetunGearShiftPosition();
        }

        public async Task SendDevicetoCloudMessageAsync()
        {
            try
            {
                var telemetryDataPoint = new
                {
                    deviceId = DeviceId,
                    message = "Ready"
                };
                var messageString = JsonConvert.SerializeObject(telemetryDataPoint);
                var message = new Message(Encoding.ASCII.GetBytes(messageString));
                await dc.SendEventAsync(message);
                tbEventMsg.Text += string.Format("\n{0} > Sending message: {1}", DateTime.Now, messageString);
                Debug.WriteLine("\n{0} > Sending message: {1}", DateTime.Now, messageString);
                IsAzureConnected = true;
                rbControl.IsEnabled = true;
            }
            catch (Exception ex)
            {
                tbEventMsg.Text += ex.Message;
                Debug.WriteLine(ex.Message);
                IsAzureConnected = false;
                rbControl.IsEnabled = false;
            }

        }

        public async Task ReceiveCloudToDeviceMessageAsync()
        {
            if (!IsAzureConnected)
                return;
            Debug.WriteLine("\nReceiving cloud to device messges from service");
            tbEventMsg.Text = "Ready to Receive cloud to device messges from service";
            while (true) //Message Loop
            {
                Message receivedMessage = await dc.ReceiveAsync();
                if (receivedMessage == null)
                    continue;
                var msg = Encoding.ASCII.GetString(receivedMessage.GetBytes());
                tbEventMsg.Text = "Last Received Message: " + msg;
                /* ********
                 * 
                 * Message will be formatted for Start / Stop; Gear Shift; Manual / Auto; Speed;
                 * Formats as follow:
                 * Start message will always have two additional parameter - speed and Gear Mode separated by :
                 * "Start:Manual" -> Start button pressed with Manual mode
                 * "Start:050;Auto" -> Start button pressed with Auto mode.
                 * "Stop" -> Stop Button pressed.
                 * "GearShift" -> Gear shift Pressed
                 * "Manual" -> Manual button Pressed
                 * "Auto" -> Button Pressed
                 * "Speed:050" -> Accelerator value is 50
                 * 
                 * ****/
                while (true) //Filter and Process message
                {
                    if (msg.StartsWith("Start")) //Start button pressed. Check for speed and Gear Mode
                    {
                        if (EngineOn) //if the engine is already running, discard Start message
                            return;

                        if (msg.Substring(6) == "Manual") //Make sure the subsequent letters are properly obtained
                        {
                            AutoOn = false;
                            GearModeSelect(true);
                        }
                        else
                        {
                            if (msg.Substring(6) == "Auto")
                            {
                                AutoOn = true;
                                GearModeSelect(false);
                            }
                            else
                                break;
                        }
                        //StartEngine(); // Start the Engine once all the messages are processed
                        break;
                    }

                    if (msg.StartsWith("Stop")) //Stop button pressed.
                    {
                        StopEngine();
                        break;
                    }

                    if (msg.StartsWith("GearShift")) //Gear button pressed
                    {
                        GearShift();
                        break;
                    }

                    if (msg.StartsWith("Manual"))
                    {
                        GearModeSelect(true);
                        break;
                    }

                    if (msg.StartsWith("Auto"))
                    {
                        GearModeSelect(false);
                        break;
                    }

                    if (msg.StartsWith("Speed:"))
                    {
                        if (!Int32.TryParse(msg.Substring(6, 3), out RevSpeed)) //Ensure right value passed and not corrupt data
                            break;
                        sl_Accelerator.Value = RevSpeed;
                        AcceleratorChange();
                        break;
                    }

                    await Task.Delay(500); //Wait for 500ms before processing
                }
                await dc.CompleteAsync(receivedMessage); //Get next message
            }
        }



        private async void RetunGearShiftPosition()
        {
            GearMovement = true;
            Gear = 1;
            GpioPinValue gp = Index.Read();
            if (gp == GpioPinValue.Low) //If the gear is already in the First position nothing to do.
                return;
            pwmpin0.SetActiveDutyCyclePercentage(.6); //Start with 60 % for aligning the gear

            await Task.Delay(MOTORWAIT * 1000); //Wait for motor spin up
            while (true)
            {
                gp = Index.Read();
                if (gp == GpioPinValue.Low)
                    break;
                BumpGearSwitch(true);
                await Task.Delay(MOTORWAIT * 1000); //wait for 3 times the bump switch
            }

        }

        private void BumpGearSwitch(bool breverse = false)
        {
            if (breverse || Gear ==3)
                Reverse.Write(GpioPinValue.High);
            Switch.Write(GpioPinValue.High);
            Stopwatch sw = new Stopwatch();
            sw.Start();
            while (sw.ElapsedMilliseconds< BUMPTIME)
            { }
            sw.Stop();
            
            Switch.Write(GpioPinValue.Low);
            if (breverse)
                Reverse.Write(GpioPinValue.Low);
            ManageIndex();
        }

        private void ManageIndex()
        {
            if (GearMovement)
            {
                if (Gear != 1) //Handled by GpioPin change voltage
                    Gear++;
                if (Gear >= 3)
                {
                    GearMovement = false;
                    Gear = 3;
                }

                tbGear.Text = string.Format("Forward -> Gear: {0}", Gear);
            }
            else
            {
                Gear--;
                if (Gear <= 1)
                {
                    GearMovement = true;
                    Gear = 1;
                }
                else
                    tbGear.Text = string.Format("Backward <- Gear: {0}", Gear);

            }
        }

 
        private void AcceleratorChange()
        {
            if (pwmpin0 == null || AcceleratorChanging)
                return;
            AcceleratorChanging = true; //Wait till this process is complete before accomodating another change
            pwmpin0.SetActiveDutyCyclePercentage((double)RevSpeed / 100);
            PrevSpeed = RevSpeed;

            //Not good. Too many gear changes. Need to work out Backward Gearshift
            if (AutoOn) //Need to change the gears for Automatic
            {
                switch (Gear) 
                {
                    case 1: //Always on Forward motion
                        if (RevSpeed < SECOND) //First Gear
                            break;
                        if (RevSpeed < THIRD) //Second Gear
                        {
                            BumpGearSwitch(); //Gear 1 will be always forward
                            break;
                       }
                        if (RevSpeed >= THIRD) //Third Gear
                        {
                            BumpGearSwitch(); //Need twice to Jump to 3rd Generally Wouldn't happen
                            BumpGearSwitch();
                            break;
                        }
                        break;
                    case 2: //could be forward or backward
                        if (RevSpeed >= SECOND && sl_Accelerator.Value < THIRD) //Second Gear
                            break;
                        if (RevSpeed < SECOND) //First Gear
                        {
                            //if (GearMovement) //Forward moving gears to return to 1 must go through one full loop
                            //{
                                BumpGearSwitch(true); //Reverse to first gear
                            //}
                            //else
                                //BumpGearSwitch(); //First gear, if it is backward movement
                            
                            break;
                        } 
                        if (RevSpeed >= THIRD) //Third Gear
                        {
                            //if (GearMovement)
                            //{
                                BumpGearSwitch(); //Third gear
                            //}
                            //else //Forward moving gears to return to 3 must go through one full loo
                            //{
                            //    BumpGearSwitch(true); //Reverse back to 3rd gear
                            //}
                            break;
                        }
                        break;
                    case 3: //Always on Forward motion
                        if (RevSpeed >= THIRD)
                            break;
                        if (RevSpeed < SECOND) //First gear
                        {
                            BumpGearSwitch(true); //Second Gear
                            BumpGearSwitch(true); //First Gear
                            break;
                        }
                        if (RevSpeed >= SECOND)
                        {
                            BumpGearSwitch(true); // Third Gear to second, Forward motion
                            break;
                        }
                        break;
                }

            }
            AcceleratorChanging = false;

        }


        private void Sl_Accelerator_ValueChange(object sender, Windows.UI.Xaml.Controls.Primitives.RangeBaseValueChangedEventArgs e)
        {
            sl_Accelerator.Value = (int)sl_Accelerator.Value;
            if (!InternetControl)
            {
                RevSpeed = (int)sl_Accelerator.Value;
                if (RevSpeed == PrevSpeed)
                    return;
                AcceleratorChange();
            }
        }


    }
}

Window IoT Application

C#
Complete Visual Studio projject files. Please compile with Microsoft Visual Studio 2015
Note Device Key is omitted. Replace this with your key. Also Device ID and IoT URI must reflect your values.
No preview (download only).

Remote Client Control

C#
This is a simple Windows Application to control the Gears remotely through Internet. Complete Project file is provided. Please use Visual Studio 2015 to compile.
Replace the Shared Access Key with your value
No preview (download only).

Remote Client - Main code file

C#
This is a synopsis of main code file for sending messages remotely to IoT device. Note, replace the Shared access key with your values
using System.Diagnostics;
using System.Text;
using System.Windows;
using Microsoft.Azure.Devices;

namespace GearController
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        static ServiceClient serviceClient;
        static string DeviceId = "W10IoT"; //Device Id obtained from Device Explorer
        static string cs = "HostName=VasuW10IoT.azure-devices.net;SharedAccessKeyName=iothubowner;SharedAccessKey=xxxxxxxx"; //Primary Key
        //static string eventHubName = "iothub-ehub-vasuw10iot-xxxxxxxxx"; //obtained from IoT Cloud-> All Settings/Messaging/Event Hub-compatible name - only used if you have storage space
        static bool Connection = false;
        static int prevSpeed;
        static bool Auto = false;
        static bool MotorOn = false;
        private const float INITIALSPEED = 40;

        /* ********
         * 
         * Message will be formatted for Start / Stop; Gear Shift; Manual / Auto; Speed;
         * Formats as follow:
         * Start message will always have two additional parameter - speed and Gear Mode separated by :
         * "Start:Manual" -> Start button pressed with Manual mode
         * "Start:Auto" -> Start button pressed with Auto mode.
         * "Stop" -> Stop Button pressed.
         * "GearShift" -> Gear shift Pressed
         * "Manual" -> Manual button Pressed
         * "Auto" -> Button Pressed
         * "Speed:050" -> Accelerator value is 50
         *  
         * ****/

        public MainWindow()
        {
            InitializeComponent();
            ConnectToIoTHub();
            prevSpeed = (int)slAccelerator.Value;
        }

        private void ConnectToIoTHub()
        {
            if (Connection) //already connected
                return;
            serviceClient = ServiceClient.CreateFromConnectionString(cs);
            if (serviceClient == null)
                tbMessage.Text = "Unable to connect to IoT hub";
            else
            {
                tbMessage.Text = "Ready to send message to IoT hub";
                Connection = true;
            }
            
        }

        private void btnClose_Click(object sender, RoutedEventArgs e)
        {
            Close();
        }

        private void slAccelerator_Change(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            if (slAccelerator != null && serviceClient!= null)
            {
                slAccelerator.Value = (int)slAccelerator.Value;
                int speed = (int)slAccelerator.Value;
                string msg = string.Format("Speed:{0:D3}",speed);
                if (prevSpeed == speed)
                    return;
                SendToDeviceCloudMessage(msg);
                tbMessage.Text += "\nMessage Sent -> " + msg;
                tbMessage.ScrollToEnd();
                prevSpeed = speed;
            }

        }

        private void SendToDeviceCloudMessage(string msg)
        {
            if (serviceClient == null || !Connection)
                return;
            var commandMessage = new Message(Encoding.ASCII.GetBytes(msg));
            Stopwatch sw = new Stopwatch();
            sw.Start();
            while (sw.ElapsedMilliseconds < 5000) //5 seconds for timeout
            {
                serviceClient.SendAsync(DeviceId, commandMessage);
                break;
            }
            //tbMessage.Text += string.Format("\nTime spent for sending in milliseconds: {0}",sw.ElapsedMilliseconds);
            sw.Stop();
            if (sw.ElapsedMilliseconds > 5000)
            {
                tbMessage.Text = "IoT Hub is not connected. Reconnect";
                Connection = false;
            }
            tbMessage.ScrollToEnd();

        }

        private void btnConnect_Click(object sender, RoutedEventArgs e)
        {
            ConnectToIoTHub();
        }

        private void btnStart_Click(object sender, RoutedEventArgs e)
        {
            if (serviceClient != null)
            {
                slAccelerator.Value = INITIALSPEED;
                string msg;
                if (Auto)
                    msg = string.Format("Start:Auto");
                else
                    msg = string.Format("Start:Manual");
                SendToDeviceCloudMessage(msg);
                tbMessage.Text += "\nMessage Sent -> " + msg;
                MotorOn = true;

            }
            else
                tbMessage.Text += "\nEither not connected to IoT or Engine already running";
            tbMessage.ScrollToEnd();


        }

        private void btnStop_Click(object sender, RoutedEventArgs e)
        {
            if (serviceClient != null && MotorOn)
            {
                string msg;
                msg = "Stop";
                SendToDeviceCloudMessage(msg);
                tbMessage.Text += "\nMessage Sent -> " + msg;
                MotorOn = false;
            }
            else
                tbMessage.Text += "\nEither not connected to IoT or Engine not running";
            tbMessage.ScrollToEnd();

        }

        private void btnGearShift_Click(object sender, RoutedEventArgs e)
        {
            if (Auto)
                return;
            if (serviceClient != null && MotorOn)
            {
                string msg;
                msg = "GearShift";
                SendToDeviceCloudMessage(msg);
                tbMessage.Text += "\nMessage Sent -> " + msg;
            }
            else
                tbMessage.Text += "\nEither not connected to IoT or Engine not running";
            tbMessage.ScrollToEnd();

        }

        private void rbAuto_Click(object sender, RoutedEventArgs e)
        {
            if (Auto)  //Already on cruise control. nothing to do
                return;
            slAccelerator.Value = INITIALSPEED;
            rbManual.IsChecked = false;
            Auto = true;
            if (serviceClient != null)
            {
                string msg;
                msg = "Auto";
                SendToDeviceCloudMessage(msg);
                tbMessage.Text += "\nMessage Sent -> " + msg;
            }
            else
                tbMessage.Text += "\nNot connected to IoT";
            tbMessage.ScrollToEnd();
        }

        private void rb_Manual_Click(object sender, RoutedEventArgs e)
        {
            if (!Auto)  //Already on Manual control. nothing to do
                return;
            slAccelerator.Value = INITIALSPEED;

            rbAuto.IsChecked = false;
            Auto = false;
            if (serviceClient != null)
            {
                string msg;
                msg = "Manual";
                SendToDeviceCloudMessage(msg);
                tbMessage.Text += "\nMessage Sent -> " + msg;
            }
            else
                tbMessage.Text += "\nNot connected to IoT";
            tbMessage.ScrollToEnd();

        }
    }
}

NXT Code for running the Shift and Clutch Motors

snippets
This is the code to ensure proper rotations of Shift and Clutch Motors are provided through NXT. Use the Lego Mindstorm compiler to compile this. Note Language is proprietary to Lego
No preview (download only).

Credits

Vasudevan Vijaya

Vasudevan Vijaya

3 projects • 8 followers
Been into Electronics since childhood. Now retired from professional services, kindling my favourite hobby again.

Comments