Stairs project

Im doing a project to light up 2 sets of stairs for a house, one set goes up, and one down.
I connected them to 2 mosfets and want to control them with 4 PIR sensors, 2 at begging, 2 and end,
Also i want to control them each set concurrently.

This is the code i have so far, but its not working, the threads block each other.
What can i do differently so each set can operate not messing up the other?

 class Program
{
    private const int times = 50;
    private const double factor = 0.02;
    private const int sleep = 30;
    private const int duration = 30000;

    static GpioController gpio = GpioController.GetDefault();

    static GpioPin pirUp1 = gpio.OpenPin(G30.GpioPin.PD2);
    static GpioPin pirUp2 = gpio.OpenPin(G30.GpioPin.PB13);
    static GpioPin pirDown1 = gpio.OpenPin(G30.GpioPin.PB14);
    static GpioPin pirDown2 = gpio.OpenPin(G30.GpioPin.PC12);

    static PwmController controller2 = PwmController.FromName(G30.PwmChannel.Controller2.Id);

    static PwmChannel stairsUp = controller2.OpenChannel(G30.PwmChannel.Controller2.PA0);
    static PwmChannel stairsDown = controller2.OpenChannel(G30.PwmChannel.Controller2.PA1);

    static double duty = 0.01;

    static Thread threadUp = new Thread(new ThreadStart(WorkUp));
    static Thread threadDown = new Thread(new ThreadStart(WorkDown));

    static bool isUpRunning;
    static bool isDownRunning;

    static void Main()
    {
        pirUp1.SetDriveMode(GpioPinDriveMode.InputPullUp);
        pirUp1.ValueChanged += pirUp1_ValueChanged;

        pirUp2.SetDriveMode(GpioPinDriveMode.InputPullUp);
        pirUp2.ValueChanged += pirUp2_ValueChanged;

        pirDown1.SetDriveMode(GpioPinDriveMode.InputPullUp);
        pirDown1.ValueChanged += pirDown1_ValueChanged;

        pirDown2.SetDriveMode(GpioPinDriveMode.InputPullUp);
        pirDown2.ValueChanged += pirDown2_ValueChanged;

        controller2.SetDesiredFrequency(750);

        stairsUp.SetActiveDutyCyclePercentage(0.01);
        stairsDown.SetActiveDutyCyclePercentage(0.01);

        threadUp.Start();
        threadDown.Start();

        Thread.Sleep(Timeout.Infinite);
    }

    private static void DoThreadDown()
    {
        threadDown = new Thread(new ThreadStart(WorkDown));
        threadDown.Start();
    }

    private static void DoThreadUp()
    {
        threadUp = new Thread(new ThreadStart(WorkUp));
        threadUp.Start();
    }

    private static void pirUp1_ValueChanged(GpioPin sender, GpioPinValueChangedEventArgs e)
    {
        if (e.Edge == GpioPinEdge.RisingEdge)
        {
            DoThreadUp();
            Debug.WriteLine("RISING UP1");
        }
        else if (e.Edge == GpioPinEdge.FallingEdge)
        {
            Debug.WriteLine("FALLING UP1");
        }
    }

    private static void pirUp2_ValueChanged(GpioPin sender, GpioPinValueChangedEventArgs e)
    {
        if (e.Edge == GpioPinEdge.RisingEdge)
        {
            DoThreadUp();
            Debug.WriteLine("RISING UP2");
        }
        else if (e.Edge == GpioPinEdge.FallingEdge)
        {
            Debug.WriteLine("FALLING UP2");
        }
    }

    private static void pirDown1_ValueChanged(GpioPin sender, GpioPinValueChangedEventArgs e)
    {
        if (e.Edge == GpioPinEdge.RisingEdge)
        {
            DoThreadDown();
            Debug.WriteLine("RISING DOWN1");
        }
        else if (e.Edge == GpioPinEdge.FallingEdge)
        {
            Debug.WriteLine("FALLING DOWN1");
        }
    }

    private static void pirDown2_ValueChanged(GpioPin sender, GpioPinValueChangedEventArgs e)
    {
        if (e.Edge == GpioPinEdge.RisingEdge)
        {
            DoThreadDown();
            Debug.WriteLine("RISING DOWN2");
        }
        else if (e.Edge == GpioPinEdge.FallingEdge)
        {
            Debug.WriteLine("FALLING DOWN2");
        }
    }

    private static void WorkUp()
    {
        if (!isUpRunning)
        {
            isUpRunning = true;
            stairsUp.Start();
            for (int i = 1; i < times; i++)
            {
                duty += factor;
                stairsUp.SetActiveDutyCyclePercentage(duty);
                Thread.Sleep(sleep);
            }
            Thread.Sleep(duration);
            for (int i = 1; i < times; i++)
            {
                duty -= factor;
                stairsUp.SetActiveDutyCyclePercentage(duty);
                Thread.Sleep(sleep);
            }
            stairsUp.Stop();
            duty = 0.01;
            isUpRunning = false;
        }
    }

    private static void WorkDown()
    {
        if (!isDownRunning)
        {
            isDownRunning = true;
            stairsDown.Start();
            for (int i = 1; i < times; i++)
            {
                duty += factor;
                stairsDown.SetActiveDutyCyclePercentage(duty);
                Thread.Sleep(sleep);
            }
            Thread.Sleep(duration);
            for (int i = 1; i < times; i++)
            {
                duty -= factor;
                stairsDown.SetActiveDutyCyclePercentage(duty);
                Thread.Sleep(sleep);
            }
            stairsDown.Stop();
            duty = 0.01;
            isDownRunning = false;
        }
    }
}

I’ve read through the code once, and first impression is that there’s a bit of extra thread starting and stopping going on.

I don’t think you need the .Start calls on lines 47 and 48. Your PIR interrupts should be used to start the threads. Also, the start and stop calls in WorkUp and WorkDown should not be necessary, and are likely killing the very thread they are running on.

Seems to be that the logic should be : on detection of PIR transition, start a thread that ramps up, waits, ramps down. Removing those start/stop calls will get you there.

The algorighm is still flawed in how it will handle multiple people walking behind each other within the time value of ‘duration’. If I were writing this, I would do it without threads at all. Just write a single state-machine loop that uses variables to determine if it should be ramping up or ramping down, and if it detects another person, it can just reset the ‘duration’ countdown. That will solve your thread problem and result in a better user experience.

For bonus points, and power savings, you could even suspend the state-machine when you are not in a ramp-up, ramp-down, or duration-lit state.

By the way, my rule of thumb is : Never reach for a thread to solve a problem you can solve with a state machine. Threads introduce a lot of hard-to-inspect program states and usually complicate matters. Threads are only appropriate where polling or blocking IO is involved.

this is starting and stopping the mosfets not threads

OK ill look up state machine, any more info would be very helpful.
Thanks

Ah sorry - yes, I see that now.

And yes, I think you’d be better off going that route.

Can you give an example?, ive read some stuff about it and it seems overly complex for my project.

Yeah - It’s an interesting problem. I will post something in the coming day or two.

Ok, so like I said, I thought this was an interesting problem, so I decided to go ahead and create a single-threaded state machine that does what you set out to do.

These are the requirements I started with (from your original post) :

  • Two stairwells (two lighting zones)
  • Each lighting zone has two PIR sensors, presumably one at the top of the stairs and one at the bottom
  • When a PIR is triggered, the associated lighting zone should ramp up, stay on for a while, and then ramp down

I then added a few “user experience” requirements:

  • Multiple people tripping a PIR in the same zone should simply extend the on time. That is, the on time counts down from the last PIR event seen, without triggering more ramp-up/ramp-down animations
  • And because we want the ramp-up/ramp-down to be smooth, use animation techniques to guarantee equal frame times between PWM steps. This is another thing that threading makes near impossible due to non-deterministic execution timing.
  • Events in different zones can overlap with each other without impacting each others’ performance
  • If a PIR event occurs while the lights are dimming (falling), then they simply reverse direction and rise back to an ‘On’ state.

Caveat: Not tested with real MOSFETS or a scope on the outputs - I just made sure I was calling the PWM functions with the right numbers. You will definitely need to change pin definitions for the G30 as I used a different TinyCLR board. I verified that the off/rise/on/fall states work as expected in the debugger.

The main function:

namespace ResponsiveLighting
{
    class Program
    {
        static void Main()
        {
            var lightingManager = new LightingManager();
            lightingManager.Run();
        }
    }
}

The bit doing the work…

using System;
using System.Diagnostics;
using System.Threading;
using GHIElectronics.TinyCLR.Devices.Gpio;
using GHIElectronics.TinyCLR.Devices.Pwm;
using GHIElectronics.TinyCLR.Pins;

namespace ResponsiveLighting
{
    public enum LightingStates
    {
        Off = 0,
        Rising = 1,
        On = 2,
        Falling = 3
    }

    public class LightingZone
    {
        private const int MillisecondsPerFrame = (1000 / LightingManager.FrameDuration);
        private const int RiseAndFallDuration = 3 * MillisecondsPerFrame;
        private const int LightsOnDuration = 60 * MillisecondsPerFrame;

        public LightingStates CurrentState { get; set; } = LightingStates.Off;
        public int Counter { get; set; }

        private PwmChannel pwmOutput;

        public LightingZone(string controllerName, int pin)
        {
            var controller = PwmController.FromName(controllerName);
            this.pwmOutput = controller.OpenChannel(pin);
        }

        public void MotionDetected()
        {

            switch (this.CurrentState)
            {
                case LightingStates.Off:
                    // start lighting up this zone
                    this.CurrentState = LightingStates.Rising;
                    this.Counter = RiseAndFallDuration;
                    break;

                case LightingStates.Falling:
                    // The lights were in the process of turning off
                    // Reverse the direction by setting the state to 'rising';
                    // start with whatever the current lighting level is;
                    // and set the duration to be whatever part of 'falling' we have already done.
                    // The effect is to simply reverse falling into a briefer rising state.
                    this.CurrentState = LightingStates.Rising;
                    this.Counter = RiseAndFallDuration - Counter;
                    break;

                case LightingStates.On:
                    // We're already on, so just extend the "on" time
                    this.Counter = LightsOnDuration;
                    break;

                default:
                case LightingStates.Rising:
                    // We've already been triggered to turn on - nothing to do
                    break;

            }
        }

        public void Step()
        {
            if (--this.Counter <= 0)
            {
                this.Counter = 0;
                // Counter has expired = move to the next state
                switch (this.CurrentState)
                {
                    case LightingStates.Rising:
                        this.CurrentState = LightingStates.On;
                        this.Counter = LightsOnDuration;
                        this.pwmOutput.Start();
                        break;

                    case LightingStates.On:
                        this.CurrentState = LightingStates.Falling;
                        this.Counter = RiseAndFallDuration;
                        break;

                    case LightingStates.Falling:
                        this.CurrentState = LightingStates.Off;
                        break;

                    default:
                    case LightingStates.Off:
                        break;
                }
            }

            SetOutput();
        }

        private void SetOutput()
        {
            double percentage;
            switch (this.CurrentState)
            {
                case LightingStates.Off:
                    this.pwmOutput.SetActiveDutyCyclePercentage(0.0);
                    this.pwmOutput.Stop();
                    break;

                case LightingStates.Rising:
                    percentage = (RiseAndFallDuration - this.Counter) / (double)RiseAndFallDuration;
                    this.pwmOutput.SetActiveDutyCyclePercentage(percentage);
                    break;

                case LightingStates.Falling:
                    percentage = (RiseAndFallDuration - (RiseAndFallDuration - this.Counter)) / (double)RiseAndFallDuration;
                    this.pwmOutput.SetActiveDutyCyclePercentage(percentage);
                    break;

                case LightingStates.On:
                    this.pwmOutput.SetActiveDutyCyclePercentage(1.0);
                    break;
            }
        }
    }

    public class LightingManager
    {
        public const int FrameDuration = 20;

        private const int NumberOfLightingZones = 2;
        // How many mS in each animation frame? A value of 20 means 50 frames per second
        private readonly GpioController gpio = GpioController.GetDefault();
        private LightingZone[] zones = new LightingZone[NumberOfLightingZones];

        private GpioPin[][] pirSensors = new GpioPin[NumberOfLightingZones][];

        public LightingManager()
        {
            this.InitializeHardware();
        }

        public void Run()
        {
            var frameTime = DateTime.UtcNow;
            while (true)
            {
                // Update each lighting zone
                foreach (var zone in this.zones)
                {
                    if (zone.Counter > 0)
                    {
                        zone.Step();
                    }
                }

                // Why not just use sleep?  Because we wnat each trip around the loop
                //  to last exactly the same amount of time regardless of how
                //  much code was run in that loop.  This avoids jitter in the animation.
                var sleepTime = FrameDuration - (DateTime.UtcNow - frameTime).TotalMilliseconds;
                // Sleep for whatever part of our frame duration we haven't already eaten up
                if (sleepTime > 0) // why? because if you are debugging, sleepTime will go negative
                {
                    Thread.Sleep((int)sleepTime);
                }
                frameTime = DateTime.UtcNow;
            }
        }

        private void InitializeHardware()
        {
            // The sensors in the array pirSensors[0] all trigger the first stairwell's lighting
            this.zones[0] = new LightingZone(SC20260.PwmChannel.Controller3.Id, SC20260.PwmChannel.Controller3.PC6);
            this.pirSensors[0] = new GpioPin[2];
            this.pirSensors[0][0] = this.InitializePirSensor(SC20100.GpioPin.PE4);
            this.pirSensors[0][1] = this.InitializePirSensor(SC20100.GpioPin.PE6);

            // And now the second stairwell in pirSensors[1]
            this.zones[1] = new LightingZone(SC20260.PwmChannel.Controller3.Id, SC20260.PwmChannel.Controller3.PC7);
            this.pirSensors[1] = new GpioPin[2];
            this.pirSensors[1][0] = this.InitializePirSensor(SC20100.GpioPin.PC2);
            this.pirSensors[1][1] = this.InitializePirSensor(SC20100.GpioPin.PC3);
        }

        private GpioPin InitializePirSensor(int pin)
        {
            var result = gpio.OpenPin(pin);
            result.SetDriveMode(GpioPinDriveMode.InputPullUp);
            result.ValueChanged += this.PirSensor_ValueChanged;

            return result;
        }

        private void PirSensor_ValueChanged(GpioPin sender, GpioPinValueChangedEventArgs e)
        {
            for (int zone = 0; zone < NumberOfLightingZones; ++zone)
            {
                foreach (var pin in pirSensors[zone])
                {
                    if (pin == sender)
                    {
                        Debug.WriteLine($"I see motion in zone {zone}");
                        this.zones[zone].MotionDetected();
                        return;
                    }
                }
            }
        }

    }
}
4 Likes