Thursday, May 2, 2013

high power LED retrofit (part2)

In this second part we're looking at the code that is going to control the LED lamp. Remark that the code discussed is the code as it is today (2/5/2013), for the latest code, go to github.
Also note that this is the code without the auto-dimming as that feature is still under test.

Ports & Plugs

static PortI2C bus1(1);
static LuxPlug lux_plug(bus1, 0x39);
static Port fan_port(2);
static Port temperature_port(3);
static Port relay_port(4);
Here the Jeenode ports are set up. We're using all 4 ports. Port 1 is connected to a software I2C bus, which is used by the lux_plug. Port 2 is a simple port, which we use for fan control. Port 3 is another simple port which we use for temperature control and port 4 is finally used to switch the relay.

global state

static uint8_t counter = 0;
#define DATA_MSG_SIZE 11
static uint8_t data_msg[DATA_MSG_SIZE];
Some global state. A counter which is inserted in responses to have an increasing number for testing, and a message buffer which is used for building up the response to the message that queries the system state.

LED PWM

namespace led_pwm {
  static const byte PIN = 9;
  static byte setting = 35;

  static inline void setup() {
    pinMode(PIN, OUTPUT);
    analogWrite(PIN, setting);
  }

  static inline void set_dim(const uint8_t dim) {
    setting = dim;
    analogWrite(PIN, setting);
  }
}
Pretty straightforward, using Arduino pin 9 for PWM, this is not a pin that is part of one of the Jeenode ports, but is instead available on a separate header.

fan

namespace fan {
  static const uint8_t default_pwm_setting = 35;
  static uint8_t pwm_setting = 0;
  volatile uint16_t pulse_count = 0;
  static uint16_t pulse_count_save = 0;
  static uint8_t pulse_per_second = 10;

  void pulse_interrupt() {
    ++pulse_count;
  }

  static inline void normal() {
    pwm_setting = default_pwm_setting;
    fan_port.anaWrite(pwm_setting);
  }

  static inline void setup() {
    fan_port.mode(OUTPUT);
    attachInterrupt(1, pulse_interrupt, FALLING);
    normal();
  }
  static inline void update_pulse_per_second() {
    const uint16_t l = pulse_count;
    pulse_per_second = l - pulse_count_save;
    pulse_count_save = l;
  }

  static inline void full_blast() {
    pwm_setting = 255;
    fan_port.anaWrite(pwm_setting);
  }

  static inline void off() {
    pwm_setting = 0;
    fan_port.anaWrite(pwm_setting);
  }
}
PWM handing is quite similar to the LED PWM, except here the fan_port Jeenode port is used to access the digital pin. In the setup method an interrupt handler is registered for the INT1 interrupt calling pulse_interrupt() on the falling edge. This causes pulse_count to be incremented for each pulse received from the fan. update_pulse_per_second() is then called every second to calculate a new pulse_per_second by simply looking at the difference.

temperature

namespace temperature {
  static int8_t value = 0;

  static inline void beep(const int16_t len = 100) {
    temperature_port.digiWrite(1);
    delay(len);
    temperature_port.digiWrite(0);
  }

  static inline void setup() {
    temperature_port.mode(OUTPUT);
    beep(500);
  }

  static inline void handle() {
    const int16_t t = temperature_port.anaRead();
    value = map(t, 0, 1023, 0, 330); // 10 mV/C
  }
}
Temperature is pretty straightforward. The temperature port digital pin is used to activate the buzzer on the temperature plug, the analog input pin is used to sample the temperature which is then converted from 0-1023 to 0-330 degrees Celsius.

lux

namespace lux {
  static uint16_t value = 0;

  static inline void setup() {
    lux_plug.begin();
  }

  static inline void handle() {
    lux_plug.getData();
    value = lux_plug.calcLux();
  }
}
Lux measurement is really simple, all the work is nicely hidden by the LuxPlug class.

indicator LED

namespace indicator {
  static const byte PIN = 8;

  static inline void on() {
    digitalWrite(PIN, LOW);
  }
  static inline void off() {
    digitalWrite(PIN, HIGH);
  }

  static inline void setup() {
    pinMode(PIN, OUTPUT);
    for (int8_t i = 0; i < 10; ++i) {
      on();
      delay(100);
      off();
      delay(100);
    }
  }
}
In indicator LED is connected to Arduino pin 8 as the Jeenode has no LED build-in and having some visual feedback is always handy while testing.

relay

namespace relay {

  static bool value = false;

  static inline void off() {
    relay_port.digiWrite(HIGH);
    delay(1000);
    relay_port.digiWrite(LOW);
    value = false;
  }

  static inline void on() {
    relay_port.digiWrite2(HIGH);
    delay(1000);
    relay_port.digiWrite2(LOW);
    value = true;
  }

  static inline void setup() {
    relay_port.mode(OUTPUT);
    relay_port.digiWrite(LOW);
    relay_port.mode2(OUTPUT);
    relay_port.digiWrite2(LOW);
    off();
  }
}
The relay is handled by the relay_port. Both the digital and the analog pin of that port are used as digital pins and control the on and off signals of the latching relay. The latching relay is driven by a simple breakout board connected to port 4 of the Jeenode, that just brings up the 3.3V digital signals to the 5V needed for the relay.

RF

namespace rf {
  static inline void setup() {
    // this is node 1 in net group 100 on the 868 MHz band
    rf12_initialize(1, RF12_868MHZ, 100);
  }

  static inline bool available() {
    return (rf12_recvDone() && rf12_crc == 0 && rf12_len >= 1);
  }

  static inline uint8_t command() {
    return rf12_data[0];
  }

  static inline void ack() {
    rf12_sendStart(RF12_ACK_REPLY, 0, 0);
    rf12_sendWait(1);
  }
}
The rf namespace provides just a thin wrapped around the Jeenode provided rf12 functionality. The first byte of a received packet is interpreted as a command. Packets with CRC checksum errors are not looked at.

commands

namespace commands {
  static inline void on() {
    rf::ack();
    relay::on();
  }

  static inline void off() {
    rf::ack();
    relay::off();
  }

  static inline void dim_value() {
    if (rf12_len >= 2) {
      led_pwm::set_dim(rf12_data[1]);
    }
    rf::ack();
  }

  static inline void beep() {
    rf::ack();
    temperature::beep();
  }

  static inline void query_data() {
    rf12_sendStart(RF12_ACK_REPLY, data_msg, DATA_MSG_SIZE);
    rf12_sendWait(1);
    ++counter;
  }


  static inline void handle() {
    indicator::on();
    switch (rf::command()) {
      // standard commands
      case 0: on(); break;
      case 1: off(); break;
      case 2: dim_value(); break;
      case 10: query_data(); break;
      case 11: beep(); break;
    }
    indicator::off();
  }
}
The handle() method here handles the possible commands received via RF. Just some simple dispatcher code.

taking measurements

static inline void take_measurements() {
  indicator::on();
  fan::update_pulse_per_second();
  lux::handle();
  temperature::handle();

  data_msg[0] = counter;
  data_msg[1] = relay::value;
  data_msg[2] = led_pwm::auto_mode;
  data_msg[3] = temperature::value;
  data_msg[4] = fan::pulse_per_second;
  data_msg[5] = led_pwm::setting;
  data_msg[6] = led_pwm::auto_setting;
  data_msg[7] = led_pwm::auto_calculated_pwm;
  data_msg[8] = fan::pwm_setting;
  memcpy(data_msg+9, &lux::value, 2);
  indicator::off();
}
This simple method called every second just gets new fan speed, lux and temperature measurements. They are also stored in the data_msg structure in case they are queried over RF later.

safety checks

static inline void safety_checks() {

  if (temperature::value > 50) {
    Serial.println(F("Danger, temp > 50, turning off lamp!"));
    relay::off();
    return;
  }

  if (!fan::forced) {
    if (temperature::value > 40) {
      Serial.println(F("temp > 40, fan full blast!"));
      fan::full_blast();
    } else {
       if (!relay::value && temperature::value < 30) {
         fan::off();
       } else {
         fan::normal();
       }
    }
  }

  if (fan::pulse_per_second < 20 && fan::pwm_setting > 0) {
    Serial.println(F("fan pulse not found, turning off lamp!"));
    relay::off();
    return;
  }

}
This method, also called every second, does some safety checks concerning temperature and fan speed. The lamp is turned off if the temperature raises above 50 degrees Celsius or when the fan is no longer running. When the temperature goes above 40 degrees Celsius the fan is turned fully on to provide extra cooling. Note that the thresholds are still subject to change.

setup

void setup () {
  Serial.begin(9600);

  indicator::setup();
  rf::setup();
  lux::setup();
  temperature::setup();
  fan::setup();
  led_pwm::setup();
  relay::setup();
}
Really simple, just setting up all individual functions.

loop

void loop () {
  static int8_t loop_count = 0;

  if (rf::available()) {
    commands::handle();
    return;
  }

  ++loop_count;

  // take measurements every second
  if (loop_count % 20 == 0) { 

    take_measurements(); 
    loop_count = 0;
    safety_checks();

  }
  delay(50);
}
Finally the main loop of the sketch. Checks for new RF messages every 50 milliseconds and updates measurements and does safety checks every 20 loops, hence every second. That's it for a close look at the software.

To be continued later with another look at the hardware!

Click here for the next post in this series.

No comments: