The introductory post on this project mentioned most of the high level considerations for safety and compliance when shooting a laser into the sky. Here, we will go into the gory details.

Regulatory Process

When lasers will be operated outdoors in the U.S., the Federal Aviation Administration must be given the opportunity to review the safety of the proposed laser operation at least 30 days in advance. Laser beams can potentially affect air traffic and distract or impair pilots during flight, so to mitigate this risk, the FAA reviews the details of the planned operation and responds with a letter of objection or non-objection. The details for how and why to provide this information to the FAA can be found in the Advisory Circular 70-1, with the attached forms “Notice of Proposed Outdoor Laser Operation(s)” and “Laser Configuration Worksheet.” Note that if the laser operations are for entertainment as opposed to scientific use, the Food and Drug Administration must be notified in addition to the FAA.

Beyond the basic information such as date and time of operation, site latitude, longitude, and elevation, etc., it is necessary to calculate and provide energy density values at various distances from the laser’s origin. Every laser diverges to some extent, so that its beam spreads out and becomes less bright over distance. The divergence of a laser is measured in milliradians, which is an angle measurement (1° ≈ 17.45 mrad). The amount of energy in the laser beam stays relatively constant over distance, so as it diverges, the density of the energy decreases. In other words, the amount of energy per unit of area in the cross section of the beam decreases as you take cross sections farther and farther from the laser’s point of origin.

Calculating Divergence

First, we measured the divergence of our laser. Essentially, you need to measure the “thickness” of the laser beam at two known distances from the origin, and then do some trig. The “edge” of the laser beam is defined to be the point at which the energy intensity is 1/e (~0.37) times the intensity at the center of the beam. In the diagram below, the point A1 has 1/e as much energy as A0, and the same goes for B1 and B0. In this diagram, the angle θ is the divergence of the beam.

To accomplish these measurements, we placed a surface a few feet away from our laser and taped a ruler to it. Then we shot the laser at the surface (while wearing appropriate eye protection!) and photographed the laser hitting the surface. The resulting photo showed the beam next to a scale (the ruler). We did this a second time, after moving the surface about twice as far away. We then had two photographs of the laser at known distances from the origin. Finally, we analyzed the photographs using graphical software to find the diameter of the beam in each photograph.

We could then calculate the divergence using the following formula: (from Wikipedia)

$$!\theta = 2 \mathsf{arctan} ( \frac{D_{f} – D_{i}}{2L} )$$

We found that our laser is actually elliptical rather than round. So we calculated two divergence measurements, one larger and one smaller. These are said to belong to the “fast” and “slow” axes (respectively) of the laser.

Note that we were not using any external optics with our laser. This would affect these calculations.

Calculating Energy Densities

Next, we calculated the energy density of the laser at four important distances, which are indicated on page 8 of Appendix 1. These are:

Abbreviation Distance Description Energy level
NOHD Nominal Ocular Hazard Distance The beam is an eye hazard (is above the Maximum Permissible Exposure), from the laser source to this distance. 2.54 mW/cm2
SZED Sensitive Zone Exposure Distance The beam is bright enough to cause temporary vision impairment, from the source to this distance. 100 μW/cm2
CZED Critical Zone Exposure Distance The beam is bright enough to cause a distraction interfering with critical task performance, from the source to this distance. 5 μW/cm2
LFED “Laser-Free” Exposure Distance Beyond this distance, the beam is dim enough that it is not expected to cause a distraction. 5 nW/cm2

The equation for calculating energy density is provided as equations 6.2 and 6.3 in the Laser Configuration Worksheet instructions:

$$!\mathsf{SR} = \frac{32.8}{\phi} \times \sqrt{\frac{1.27 \times \Phi}{\mathsf{EL}}}$$
SR = Slant Range, the term for the distance from laser origin at which the given energy density occurs (feet)
Φ = Power when calculating NOHD, or Visually Corrected Power when calculating the other three values (Watts)
φ = Beam Divergence (mrad)
EL = Energy Level, one of the values from the chart above
32.8 = Conversion factor used to convert centimeters into feet, and radians into milliradians

Note: the MPE comes from Table 2 and the Visually Corrected Power comes from section 3(b) of the Laser Configuration Worksheet.

BEWARE! The values for EL given at the top of page 10 of Appendix 1, part of the worked example of calculating SZED/CZED/LFED, are incorrect. For example, 100 μW/cm2 is 1.0×10-4, not 1.0×10-8 as stated here.

We modified the above equation for an elliptical beam. In the document ANSI Z136.1, equation B46 on page 152 provides the following:

$$!\mathsf{EL} = \frac{1.27 \Phi e^{-\mu r}}{[b + r \phi_{1}] [c + r \phi_{2}]}$$

In some of the examples working with this equation in ANSI Z136.1, the e-μr term is dropped due to considering the atmospheric attenuation to be negligible, and b and c are considered insignificant as well. Making these simplifications, and solving for r (= SR), we get a formula very similar to the SR formula from the FAA doc:

$$!\mathsf{SR} = \sqrt{\frac{1.27 \times \Phi}{\mathsf{EL} \times \phi_{1} \times \phi_{2}}}$$

Here instead of (1/φ) outside the radical, we are multiplying the two divergence angles together in the denominator inside the radical. Finally, we can replace the unit conversion factor of 32.8, or manually convert units.

Safety Interlocks

In addition to the above regulations, we also needed to ensure that nobody would get hurt.  Interlocks protect the nearby humans from excessive exposure.  Our system has several interlocks:

  • The laser is mounted to the top of an 8′ structure, so the origin is above eye level.
  • Detecting motion close to the laser, so that no one can get close to the laser while it is operating. We use two infrared motion detectors inside the structure where it is mounted. These trigger the laser to shut down if any motion is sensed.
  • Detecting whether the structure is being knocked over, so that the laser can be automatically shut off before it can present an eye hazard to anyone. We use an accelerometer that triggers a laser shutdown if it detects tilt above a pre-set threshold.
  • Detecting whether the laser diodes are over safe operating temperature. The laser module came with two built-in thermistors which allow accurate thermal monitoring.
  • The system has four “E-Stop” (emergency stop) buttons which are easily accessible around the structure.
  • The startup sequence to the laser drivers observes a 7-second delay before powering up, in order to meet ANSI safety standards.

The interlocks are monitored via an arduino in real time.  The motion sensors and e-stop buttons are digital, while the thermal monitoring and the accelerometer are analog.

The Arduino sketch we used follows. Please comment if you would like to see the wiring diagrams for the various components as well.

File: everything.ino

// - make accelerometer deal in degrees
// - accelerometer tare?

// ===== Constants =====
// The amount of delay per loop iteration. Mind the step_size.
int loop_frequency_millisecs = 50;
// The amount of time over which the laser power-up should be spread
int laser_warmup_time_millisecs = 10000;
// The amount of time that the system should wait between the moment
// when power-on is requested and actually beginning to power up.
int startup_delay_millisecs = 7000;
// The amount of tilt that is allowed before the accelerometer triggers
// This is an arbitrary number that makes sense to the accelerometer.
// 70 of these roughly equals 90 degrees. See above TODO.
float maxtilt = 20.0;
// The max temperature that is allowed before the thermistor triggers (deg C)
float max_temp = 40.0;

int RXLED = 17;  // The RX LED has a defined Arduino pin
// The TX LED was not so lucky, we'll need to use pre-defined
// macros (TXLED1, TXLED0) to control that.

// operating modes
int MODE_laser_off = 0;
int MODE_laser_on = 1;
int MODE_startup = 2;
int MODE_shutdown = 3;
String mode_status[] = {"Off", "On", "Startup", "Shutdown"};

// digital pins
int DPIN_driver_output = 9;
int DPIN_motion_sensor_1_input = 14;
int DPIN_motion_sensor_2_input = 15;
int DPIN_panic_button_input = 16;

// analog pins
int APIN_thermistor_1_input = 0;
int APIN_thermistor_2_input = 1;
int APIN_accelerometer_x_input = 2;
int APIN_accelerometer_y_input = 3;

// == globals ==
int mode = MODE_laser_off;
// laser drivers
int countdown = 0;
unsigned long startup_requested_at = 0;
int power_level = 0;
// accelerometer
float current_tilt = 0.0;
// motion sensor
int motion_sensed = 0;
// panic buttons
int panic = 0;
// thermistors
float thermistor_temp_1 = 0.0;
float thermistor_temp_2 = 0.0;

int step_size = 255 / (laser_warmup_time_millisecs / loop_frequency_millisecs);

void setup()
  pinMode(RXLED, OUTPUT);  // Set RX LED as an output
  // TX LED is set as an output behind the scenes

  pinMode(DPIN_driver_output, OUTPUT);
  pinMode(DPIN_motion_sensor_1_input, INPUT);
  pinMode(DPIN_motion_sensor_2_input, INPUT);
  pinMode(DPIN_panic_button_input, INPUT);
  Serial.begin(9600); //This pipes to the serial monitor
void loop()
  if (Serial.available() > 0) {
    // read the incoming byte:
    byte incomingByte =;

    if (incomingByte == 'a') {
      Serial.println("Starting up");
      if (startup_delay_millisecs > 0) {
        Serial.print("Note: Delaying by ");
        Serial.println(" ms before beginning power up...");
        startup_requested_at = millis();
        countdown = startup_delay_millisecs / 1000;
      mode = MODE_startup;

    } else if (incomingByte == 'x') {
      Serial.println("Shutting down");
      mode = MODE_shutdown;

    } else if (incomingByte == 's') {

    } else {
      Serial.println("Unrecognized command; ignored");

  if (mode == MODE_startup) {
   if (millis() < startup_requested_at + startup_delay_millisecs) {
     int old_countdown = countdown;
     countdown = (startup_delay_millisecs / 1000) - ((millis() - startup_requested_at) / 1000);
     if (countdown != old_countdown) {
   } else {
     power_level = min(255, power_level + step_size);
     Serial.print("  ... new power level: ");
     analogWrite(DPIN_driver_output, power_level);
     if (power_level == 255) {
       mode = MODE_laser_on;
       Serial.println("Powerup complete.");

  } else if (mode == MODE_shutdown) {
   Serial.println("Power down complete.");

  } else {
  // monitor all components here
 // ------ blinky -------

 digitalWrite(RXLED, HIGH);   // set the LED on
 TXLED1; //TX LED is not tied to a normally controlled pin
 delay(loop_frequency_millisecs / 2);
 digitalWrite(RXLED, LOW);    // set the LED off
 delay(loop_frequency_millisecs / 2);

int is_laser_on() {
  return (mode == MODE_laser_on || mode == MODE_startup);

void print_status() {
  Serial.println("=== status ===");
  Serial.println("Laser Mode: " + mode_status[mode]);
  Serial.print("  ... step size: ");

  Serial.print("  ... panic buttons: ");
  Serial.println(panic ? "YES" : "no");

  Serial.print("  ... accelerometer: ");
  if (current_tilt > maxtilt) {
    Serial.print(" -- TILT");
  Serial.print("  ... motion sensor: ");
  Serial.println(motion_sensed ? "YES" : "no");
  Serial.print("  ... thermistors: ");
  Serial.print(" deg C, ");
  Serial.print(" deg C (max is ");
  Serial.print("deg C)");
  Serial.println(((thermistor_temp_1 > max_temp) || (thermistor_temp_2 > max_temp)) ? " -- OVERHEATED" : "");

void shut_laser_down() {
  power_level = 0;
  analogWrite(DPIN_driver_output, power_level);
  mode = MODE_laser_off;

void check_panic_buttons() {
  panic = !digitalRead(DPIN_panic_button_input);

  if (is_laser_on() && panic) {
   Serial.println("Panic button(s) hit. Shutting laser down.");

void check_thermistors() {
 int value0 = analogRead(APIN_thermistor_1_input);
 int value1 = analogRead(APIN_thermistor_2_input);
 float r0 = thermistor_calc_resistance(value0);
 float r1 = thermistor_calc_resistance(value1);
 thermistor_temp_1 = thermistor_calc_temperature(r0);
 thermistor_temp_2 = thermistor_calc_temperature(r1);
 if (is_laser_on() && ((thermistor_temp_1 > max_temp) || (thermistor_temp_2 > max_temp))) {
   Serial.println("Thermistor out of acceptable range. Shutting laser down.");
   Serial.print("temp1: ");
   Serial.print(" deg C, temp2: ");
   Serial.print(" deg C (max is ");
   Serial.println(" deg C)");

void check_accelerometer() {
 int valueX = analogRead(APIN_accelerometer_x_input);
 int valueY = analogRead(APIN_accelerometer_y_input);
 //Serial.print("X: ");
 //Serial.print(", Y: ");

 // 337 is the value for level (0g) -- half of 3.3V (when 1023 == 5V)
 // 271 is the lowest I could make it go by making it vertical (-1g) -- theoretically 225 is possible
 // 414 is the highest I could make it go by making it vertical in the other direction (1g) -- theoretically 449
 // ~70 = 90 degrees
 float xtilt = abs(337 - valueX);
 float ytilt = abs(337 - valueY);
 current_tilt = sqrt(xtilt*xtilt + ytilt*ytilt);
 if (is_laser_on() && current_tilt > maxtilt) {
   Serial.println("Accelerometer out of acceptable range. Shutting laser down.");
   Serial.print("Xtilt: ");
   Serial.print(", Ytilt: ");
   Serial.print(", current_tilt: ");
   Serial.print(", maxtilt: ");
 // TODO: fancy angle handling
 // 1. convert to g's
 // 2. convert to angle -

void check_motion_detector() {
  int val1 = digitalRead(DPIN_motion_sensor_1_input);
  int val2 = digitalRead(DPIN_motion_sensor_2_input);
  if (val1 || val2) {
    motion_sensed = 1;
  } else {
    motion_sensed = 0;
  if (is_laser_on() && motion_sensed) {
   Serial.println("Motion sensed. Shutting laser down.");

// Calculates the resistance in kOhm
// Input is the voltage as a number from 0 to 1023, where 1023 means 5V
float thermistor_calc_resistance(int voltage) {
  float fv = (float)voltage;
  return ((fv / 1023.0) * 10.0) / (1.0 - (fv / 1023.0));

// Calculates the temperature in C based on the B-parameter equation for NTC thermistors
// (The B-parameter was found in this obscure pdf:
// Input is resistance in kOhm
float thermistor_calc_temperature(float resistance) {
  float b = 3380.0;
  return (1.0 / ( (1.0/298.15) + (log(resistance / 10.0) / b))) - 273.15;

Posted in: Show-n-Tell  

Add A Comment