filipmu
Published © GPL3+

Safe Distance Protection Badge

A wearable badge that warns users if they encroach another user's safe social distance. Low cost and no infrastructure required to operate.

IntermediateFull instructions provided8 hours15,128
Safe Distance Protection Badge

Things used in this project

Hardware components

nRF24L01+ module
Many vendors sell this module, some for under $2.00 each. For Simple Prototype or Custom version
×1
Ultrasonic Sensor - HC-SR04 (Generic)
Ultrasonic Sensor - HC-SR04 (Generic)
For Simple Prototype or Custom Version Desolder the piezo receiver and transmitter transducers to use in this project. They are very sensitive and cheaper than single transducers sold alone.
×1
Piezo buzzer, 5v
For Simple Prototype
×1
Arduino Nano R3
Arduino Nano R3
The Arduino Nano can be used to build a simple prototype. However, it uses too much power to be battery operated for long. A custom design is required with an Atmega 328P. For Simple Prototype
×1
Resistor 1M ohm
Resistor 1M ohm
For Simple Prototype
×1
USB power pack
Source of portable power for the Arduino Nano. For Simple Prototype
×1
Custom Board
For Custom version. See attached Eagle schematic, board design, and detailed BOM for a custom board that has additional features like vibrating motor, LIPO charge control, and on/off switches in a more compact form factor.
×1
LiPo cell 1000mAH capacity, protected
For Custom version. A protected Lipo cell.
×1
Solar Cockroach Vibrating Disc Motor
Brown Dog Gadgets Solar Cockroach Vibrating Disc Motor
For Custom Version 3V vibrating motor, 100mA max. With adhesive backing.
×1
10-pin 2x5 Socket-Socket 1.27mm IDC (SWD) Cable - 150mm long
For Custom Version. In this project we modify this cable to use it to program the Atmega 328P on the Custom board.
×1
SparkFun FTDI Basic Breakout - 3.3V
SparkFun FTDI Basic Breakout - 3.3V
For Custom Version. You will need a USB to serial converter designed for programming an Arduino board without an FTDI chip.
×1

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)
Used to print an enclosure for Custom version of the device.

Story

Read more

Custom parts and enclosures

Case Back

3D Printer file for case back. Print with a layer height of 0.2 mm and support for lanyard attachment. Best material is PETG because if its strength and flexibility, but PLA works also.

Case Front

3D Printer file for case front. Print with a layer height of 0.2 mm. Best material is PETG because if its strength and flexibility, but PLA works also.

Pushbuttons

3D Printer file for pushbuttons. Print 2 of these with a layer height of 0.2 mm. Best material is PETG because if its strength and flexibility, but PLA works also.

Schematics

Eagle Design, Images, and BOM for custom board.

A custom board with an Atmega328P, voltage regulators, and a LIPO charge regulator that implements the full system.

Prototype wiring diagram

Custom board at OSH Park

Code

covid12.ino

Arduino
Firmware for Arduino Nano and the Custom board.
/***************************************************************************
    Covid Safe Distance Badge Firmware v1.0

    Copyright (C) 2020 Filip Mulier

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses></http:>.

 ***********************************************************************/



#include <SPI.h>
#include "nRF24L01.h"
#include "RF24.h"
#include "printf.h"
#include "LowPower.h"


unsigned char txmessage;  //written to rf
unsigned char rxmessage;  //read from rf

volatile unsigned char send_mode;  //if  0 we are off, 1 is in receive mode, 2 we are in send mode
volatile unsigned int send_count;  //  count down for number of pulses to send
volatile unsigned char pwm_out;


// Times in microseconds, converted to timer ticks
//  ticks depend on 16 MHz clock, so ticks = 62.5 ns
//  prescaler can divide that, but we're running fast enough to not need it

#define TIMER1_PRESCALE   1     // clock prescaler value
#define TCCR1B_CS20       0x01  // CS2:0 bits = prescaler selection
#define PERIOD_US   25.0  // 25 is 40khz
#define PERIOD_TICKS (microsecondsToClockCycles(PERIOD_US / 2) / TIMER1_PRESCALE)

//Speed of sound calculations
//  Speed of sound is 343m/s at 20 deg C, that is 343mm/ms
//  Timer 1 interrupt 12.5 usec per period, since it occurs 2x every 25 us
//  12.5 us/p * 1/1000 ms/us * 343mm/ms  =  4.2875 mm/p  = .42875 cm/p
// Convert this to something that calculates fast
//
//  .42875 is 55/128.
//  The max value for time measurement in unsigned int is 65535, so 65535/55 = 1191 is the max LISTEN_TIME

#define CONV_NUM 55
#define CONV_DENOM 128

#define SAFE_DISTANCE_BEEP 150 //Safe distance in cm
#define SAFE_DISTANCE_VIBRATE 200
#define BEEP_DURATION 10
#define VIBRATE_DURATION 70
#define LISTEN_MAX_RANGE 250 //Max range in cm , max is 510cm
#define LISTEN_TIME (LISTEN_MAX_RANGE * CONV_DENOM / CONV_NUM)


#define SEND_TIME 80*2 //number if interrupt cycles to send ultrasonic tone  2ms

#define SWITCH_TO_INPUT 2*80 // output is set to input at this time  1ms
#define OUTPUT_BLANK 5*80 // output is held off for this time after the signal is sent 3ms

#define BATTMINMV 3300  // minimum voltage for battery


//Matched filtering settings

#define CN 80 //Circular buffer size, should be length of 1 bit
#define HI_THRESH 5 //threshold used for detection half of CN divuded  by 2
#define DELAY_COMPENSATION (2*(HI_THRESH)) //amount of delay introduced due to matching a signal.
volatile signed char circ_buff[CN];
volatile signed char y_current = 0;
volatile signed char y_last = 0;
volatile unsigned char tnow = 0;

volatile unsigned int tm = 0;
volatile unsigned int tm_detect = 0;

unsigned long time_now = 0;
unsigned long next_time = 0;
unsigned long beep_time = 0;
unsigned long vibrate_time = 0;

#define SPI_SS   10   // OCR1B - low-active primary drive
#define INDICATOR_OUT 4  //pin used to light indicator LED
#define VIBRATE_OUT 3  //pin indicates matched output found
#define BEEPER_OUT 2 //pin used to turn on beeper

RF24 radio(5, 8);  // CE, CSN Set up nRF24L01 radio on SPI bus plus pins 5,8
// interrupt output pin is not used.

//Radio address
uint8_t address[][6] = {"0Node","1Node"};

void config_pwm() {

    noInterrupts();

    TCCR1B = 0x00;                 // stop Timer1 clock for register updates

    DDRB = DDRB &(~_BV(DDB1) & ~_BV(DDB2));  // set data direction to input

    // set the period and duty cycle
    ICR1 = PERIOD_TICKS;           // PWM period
    OCR1A = PERIOD_TICKS / 2;      // ON duration = drive pulse width = 50% duty cycle
    OCR1B = PERIOD_TICKS/2;      //  ditto - use separate load due to temp buffer reg


    //Switch to Compare Output Mode, non-PWM to set initial conditions of pins  per datasheet:
    //The easiest way of setting the OC1x value is to use the Force Output Compare (FOC1x) strobe bits in Normal mode.
    //The OC1x Register keeps its value even when changing between Waveform Generation modes.

    pwm_out = 1;
    TCCR1C = _BV(FOC1A) | _BV(FOC1B); //force output compare with strobe bits to initialize

    TCNT1 = OCR1A -1;             // force immediate OCR1x compare on next tick
    DDRB = DDRB | (_BV(DDB1) | _BV(DDB2));  // set data direction to output

    TIMSK1 |= ( _BV(OCF1A) );   //enable overflow interrupt


    // TIMSK1 |= (_BV(TOIE1) |  _BV(ICIE1));   //enable other interrupts


    TCCR1B = _BV(WGM13) | TCCR1B_CS20;  //turn on clock source with /8 prescaler to start timer.

    interrupts();

}


#define PIN_AIN0 6
#define PIN_AIN1 7


void config_comparator() {
    pinMode(PIN_AIN0,INPUT);
    pinMode(PIN_AIN1,INPUT);

    ADCSRB = ADCSRB & 0b10111111;           // (Disable) ACME: Analog Comparator Multiplexer Enable
    DIDR1 = 0x03; //disable digital input buffer on the analog inputs

    //read the comparator output by using bit 5 (ACO) of ACSR register.

}

inline void make_output() {

    DDRD = DDRD | (_BV(DDD6) | _BV(DDD7));
    // Above is to set data direction to output for pin 6 and 7 - equivalent to :
    //pinMode(PIN_AIN0,OUTPUT);
    //pinMode(PIN_AIN1,OUTPUT);
    // but less cycles

    PORTD = PORTD & (~_BV(PORTD6));
    PORTD = PORTD | _BV(PORTD7);
    //Above is equivalent to below, but takes less cycles.
    //digitalWrite(PIN_AIN0,LOW);
    //digitalWrite(PIN_AIN1,HIGH);


}

inline void make_input() {
    DDRD = DDRD & (~_BV(DDD6))&( ~_BV(DDD7));  // set data direction to input
    PORTD = PORTD & (~_BV(PORTD6)) & (~_BV(PORTD7));  //turn off pullup

}

//Set the compiler to high optimization for speed of code in the interrupt
#pragma GCC optimize ("-O3")
#pragma GCC push_options

ISR(TIMER1_COMPA_vect)  {

    unsigned char a_in;
    signed char y_abs;

// since interrupt time is an issue, we have logic to toggle between send and receive code

    if(send_mode == 1) {

        //input detection and updating circular buffer


        a_in = (ACSR & _BV(ACO)) >> (ACO);  //get comparator output and put in least significant bit

        y_current = (-y_last-a_in)+ circ_buff[tnow];
        y_last = y_current;
        circ_buff[tnow] = a_in;

        if ( tnow<(CN-1) )
            tnow = tnow +1;
        else
            tnow = 0;


        //update overall time
        tm = tm - 1;

        if(tm==0  || tm_detect > 0)
        {
            send_mode = 0;  //shut off listening
        }


        else {


            //test circular buffer for capture of signal
            if(y_current>HI_THRESH  || y_current <-HI_THRESH)  //check if abs value is greater than hi threshold
            {
                tm_detect = tm;
            }
        }

    } else if (send_mode == 2) { //send mode

        if(pwm_out) {
            PIND= 0b11000000;  //toggle pin 6 and 7 to generate a signal
        }
        send_count = send_count -1;
        // multiple byte output

        if(send_count == (OUTPUT_BLANK + SWITCH_TO_INPUT)) {
            // Set both output pins low
            PORTD = PORTD & (~(_BV(PORTD6) | _BV(PORTD7)));
            //shut down pwm output
            pwm_out = 0;

        } else if(send_count == SWITCH_TO_INPUT) {
            DDRD = DDRD & (~_BV(DDD6))&( ~_BV(DDD7));  // set data direction to input
            PORTD = PORTD & (~_BV(PORTD6)) & (~_BV(PORTD7));  //turn off pullup
        }
        else if (send_count == 0)
            send_mode = 0; //shut off send mode


    }  //end send mode

}

#pragma GCC pop_options




void setup() {
    noInterrupts();
    Serial.begin(115200);
    Serial.println("Covid badge Sonar");
    printf_begin();  //needed for debugging output


    pinMode(SPI_SS, OUTPUT);  //must set as output for SPI to work
    pinMode(INDICATOR_OUT, OUTPUT);
    pinMode(VIBRATE_OUT, OUTPUT);
    pinMode(BEEPER_OUT, OUTPUT);


    config_comparator();
    config_pwm();
    interrupts();

    radio.begin();
    radio.setChannel(100);
    radio.setDataRate(RF24_2MBPS);  //RF24_250KBPS for 250kbs, RF24_1MBPS for 1Mbps, or RF24_2MBPS for 2Mbps
    radio.setPALevel(RF24_PA_MIN); //RF24_PA_MIN, RF24_PA_LOW, RF24_PA_HIGH and RF24_PA_MAX
    radio.setRetries(0,0);  //minimal delay, no retries
    radio.setPayloadSize(sizeof(txmessage));

    radio.openWritingPipe(address[0]);
    radio.openReadingPipe(1,address[0]);

    radio.startListening();
    radio.printDetails();

    delay(50);
    prep_read_ultrasonic();
    time_now = millis();
    next_time = time_now + random(50, 75);


}


long readVcc() {
    // Read 1.1V reference against AVcc
    // set the reference to Vcc and the measurement to the internal 1.1V reference
#if defined(__AVR_ATmega32U4__) || defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
    ADMUX = _BV(REFS0) | _BV(MUX4) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
#elif defined (__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)
    ADMUX = _BV(MUX5) | _BV(MUX0);
#elif defined (__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__)
    ADMUX = _BV(MUX3) | _BV(MUX2);
#else
    ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
#endif

    delay(2); // Wait for Vref to settle
    ADCSRA |= _BV(ADSC); // Start conversion
    while (bit_is_set(ADCSRA,ADSC)); // measuring

    uint8_t low  = ADCL; // must read ADCL first - it then locks ADCH
    uint8_t high = ADCH; // unlocks both

    long result = (high<<8) | low;

    result = 1125300L / result; // Calculate Vcc (in mV); 1125300 = 1.1*1023*1000
    return result; // Vcc in millivolts
}


unsigned char checkVcc() {
    // Read 1.1V reference against AVcc
    // set the reference to Vcc and the measurement to the internal 1.1V reference
    // return 0 if voltage less than the limit for battery

#if defined(__AVR_ATmega32U4__) || defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
    ADMUX = _BV(REFS0) | _BV(MUX4) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
#elif defined (__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)
    ADMUX = _BV(MUX5) | _BV(MUX0);
#elif defined (__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__)
    ADMUX = _BV(MUX3) | _BV(MUX2);
#else
    ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
#endif

    delay(2); // Wait for Vref to settle
    ADCSRA |= _BV(ADSC); // Start conversion
    while (bit_is_set(ADCSRA,ADSC)); // measuring

    uint8_t low  = ADCL; // must read ADCL first - it then locks ADCH
    uint8_t high = ADCH; // unlocks both

    long result = (high<<8) | low;

    if (result > (1125300/BATTMINMV))  // check limit
        return 0;  // return 0 if voltage below limit
    else
        return 1;  //return 1 if voltage above limit


}

//Ultrasonic Routines

void prep_read_ultrasonic(void) {
    tnow = 0;
    tm_detect=0;
    y_last = 0;

    //zero circular buffer
    for( int i = 0; i < CN; i = i + 1 )
        circ_buff[i] = 0;
    make_input();  // go back to no output
}

unsigned int read_ultrasonic(unsigned int tm_listen_time) {

    tm = tm_listen_time;
    send_mode = 1; //start listening
    while(send_mode);  // listen for allotted time

    return(tm_listen_time - tm_detect);
}



void prep_write_ultrasonic(unsigned int duration) {

    send_count = duration + OUTPUT_BLANK + SWITCH_TO_INPUT;

}

void write_ultrasonic(void) {
    // Write Ultrasonics
    make_output();  // turn input pins to output //debug

    // set this flag to start the output
    pwm_out = 1;
    send_mode = 2;
    while (send_mode); //wait until send complete
}

bool write_status;

int count = 0;
unsigned int travel_time;

void loop() {

// Listen for RF
    if (radio.available()) { //Heard RF
        digitalWrite(INDICATOR_OUT,HIGH);
        radio.read(&rxmessage, sizeof(rxmessage));
        radio.stopListening();
        prep_write_ultrasonic(SEND_TIME);
        write_ultrasonic();
        prep_read_ultrasonic();
        radio.startListening();
        time_now = millis();
        next_time = time_now + random(50, 75);

    }

    else if ( millis() > next_time) {

        if(checkVcc() == 0) {
            Serial.println("Low Battery");
            digitalWrite(BEEPER_OUT,HIGH);
        }

        txmessage = 1 ; // we don't use the message for anything
        radio.stopListening();
        digitalWrite(INDICATOR_OUT,HIGH);
        write_status = radio.write(&txmessage, sizeof(txmessage));
        digitalWrite(INDICATOR_OUT,LOW);
        travel_time = read_ultrasonic(LISTEN_TIME);

        //Serial.println(readVcc());

        travel_time = (travel_time - DELAY_COMPENSATION)*CONV_NUM/CONV_DENOM;  //calculate in cm
        if (travel_time >0 && travel_time <SAFE_DISTANCE_VIBRATE)
        {
            digitalWrite(VIBRATE_OUT,HIGH);
            time_now = millis();
            vibrate_time = time_now + VIBRATE_DURATION;
            Serial.print("Danger:");
            Serial.println(travel_time);
        }
        else
        {
            Serial.print("Safe-v:");
            Serial.println(travel_time);
        }

        if (travel_time >0 && travel_time <SAFE_DISTANCE_BEEP)
        {
            digitalWrite(BEEPER_OUT,HIGH);
            time_now = millis();
            beep_time = time_now + BEEP_DURATION;
            Serial.print("Danger:");
            Serial.println(travel_time);
        }
        else {


            Serial.print("Safe-b:");
            Serial.println(travel_time);
        }


        prep_read_ultrasonic();
        time_now = millis();
        next_time = time_now + random(100, 150);
        radio.startListening();

    }

    // limit the time for vibration and beep
    if (digitalRead(BEEPER_OUT)) {
        if(  millis() > beep_time)
            digitalWrite(BEEPER_OUT,LOW);
    }

    if (digitalRead(VIBRATE_OUT)) {
        if(  millis() > vibrate_time)
            digitalWrite(VIBRATE_OUT,LOW);
    }


}

Credits

filipmu

filipmu

2 projects • 12 followers
Engineer since the age of 10. In that time let the smoke out of many components and had many burned fingers while soldering.
Thanks to TMRh20 and Scott Daniels.

Comments