Embedded system Fun Blog
























































Find out all the best information, libraries and circuit about the latest Embedded systems.
Showing posts with label sonar. Show all posts
Showing posts with label sonar. Show all posts

Saturday, 7 January 2012

MBED Example: How to use MB1210 Max Botix ultrasonic range finder model 1210 libraries

.from: http://mbed.org/users/Blaze513/programs/MB1210/5zqck


XxXxXxXxXxXxXxXxXxXxXxXxXxXx MB1210.cpp XxXxXxXxXxXxXxXxXxXxXxXxXxXx

//mbed Microcontroller Library
//Max Botix Ultrasonic Range Finder MB1210 Interface
//Copyright 2010
//Thomas Hamilton

#include "MB1210.h"

MB1210::MB1210(PinName pw, PinName an, PinName tx, PinName rx) : OperatingMode(0x00),
    UnitFactor(1), PwmScalingFactor(17014.5), AnalogScalingFactor(1024), Range(0)
{
    if (rx != NC)
    {
        SerialInput = new Serial(NC, rx);
        SerialInput->baud(9600);
        SerialInput->format(8, Serial::None, 1);
        SerialInput->attach(NULL, Serial::RxIrq);
        OperatingMode = 0x02;
    }
    if (an != NC)
    {
        AnalogInput = new AnalogIn(an);
        OperatingMode = 0x01;
    }
    if (pw != NC)
    {
        PwmInput = new PwmIn(pw);
        OperatingMode = 0x00;
    }
    if (tx != NC)
    {
        SerialOutput = new DigitalOut(tx);
        SerialOutput->write(0);
    }
}
    //constructor dynamically allocates memory and cpu time (interrupts)
    //to input objects depending on how the device is connected

MB1210::~MB1210()
{
    delete PwmInput;
    delete AnalogInput;
    delete SerialOutput;
    delete SerialInput;
    delete this;
}
    //input objects must be deallocated

void MB1210::SoundVelocity(float MetersPerSecond)
{
    PwmScalingFactor = (UnitFactor * MetersPerSecond * 50);
}
    //set the velocity of sound for pwm readings

void MB1210::Voltage(float Volts)
{
    AnalogScalingFactor = (UnitFactor * 3379.2 / Volts);
}
    //set the voltage correction factor for analog readings

void MB1210::Unit(float UnitsPerMeter)
{
    PwmScalingFactor *= (UnitsPerMeter / UnitFactor / 100);
    AnalogScalingFactor *= (UnitsPerMeter / UnitFactor / 100);
    UnitFactor = UnitsPerMeter / 100;
}
    //set the unit factor to return the range in units other than cm

void MB1210::Mode(char Selection)
{
    if (SerialInput)
    {
        if (Selection & 0x08)
        {
            SerialInput->attach(this, &MB1210::Interrupt, Serial::RxIrq);
        }
        else
        {
            SerialInput->attach(NULL, Serial::RxIrq);
        }
            //attach or detach the interrupt function
    }
        //interrupts can only be generated if rx pin is connected
    if (SerialOutput)
    {
        SerialOutput->write(Selection & 0x04);
    }
        //synchronous modes can only be set if tx pin is connected
    OperatingMode = Selection & 0x03;
}
    //change the operating mode; SerialOutput controls synchronicity

void MB1210::AttachInterruptBuffer(float* Buffer)
{
    InterruptBuffer = Buffer;
}
    //the user changes the pointer to their own storage area so they can use the interrupt

void MB1210::RequestSyncRead()
{
    if (SerialOutput)
    {
        SerialOutput->write(1);
        wait_us(20);
        SerialOutput->write(0);
    }
}
    //hold pin high for at least 20 us to request a synchronous range reading

void MB1210::DiscardSerialBuffer()
{
    while (SerialInput->readable())
    {
        SerialInput->getc();
    }
}
    //read characters from the buffer until it is empty

float MB1210::Read()
{
    switch (OperatingMode)
    {
        case 0:
            if (PwmInput)
            {
                return PwmInput->pulsewidth() * PwmScalingFactor;
            }
            else
            {
                return 0;
            }
        case 1:
            if (AnalogInput)
            {
                return AnalogInput->read() * AnalogScalingFactor;
            }
            else
            {
                return 0;
            }
        case 2:
            if (SerialInput)
            {
                unsigned char i = 0;
                while (SerialInput->readable() && !SerialInput->scanf("R%3f", &Range) && (i < 32))
                {
                    SerialInput->getc();
                    i++;
                }
                    //find R and parse the range out
                return Range * UnitFactor;
            }
            else
            {
                return 0;
            }
        default:
            return 0;
    }
}
    //OperatingMode switches to desired output method;
    //the result is scaled according to voltage, the speed of sound, and desired unit

void MB1210::Interrupt()
{
    *InterruptBuffer = Read();
    DiscardSerialBuffer();
}
    //this is called whenever an interrupt mode is
    //set and a serial rx interrupt is generated;
    //it writes to the user's data storage area

MB1210::operator float()
{
    return Read();
}
    //conversion function acts as shorthand for Read()

XxXxXxXxXxXxXxXxXxXxXxXxXxXx MB1210.h XxXxXxXxXxXxXxXxXxXxXxXxXxXx

//mbed Microcontroller Library
//Max Botix Ultrasonic Range Finder MB1210 Interface
//Copyright 2010
//Thomas Hamilton

#ifndef MB1210Library
#define MB1210Library

#include "mbed.h"
#include "PwmIn.h"

class MB1210
{
    private:
        PwmIn* PwmInput;
        AnalogIn* AnalogInput;
        DigitalOut* SerialOutput;
        Serial* SerialInput;

        char OperatingMode;
        float UnitFactor;
        float PwmScalingFactor;
        float AnalogScalingFactor;
        float Range;
        float* InterruptBuffer;

        void Interrupt();

    public:
        MB1210(PinName pw, PinName an, PinName tx, PinName rx);
            //pulse width modulation input, analog input, serial output, serial input;
            //specify NC if pin is not used;
            //if the pulse width pin is used, interrupts will perform a
            //few microseconds of calculations every time a reading is taken
        ~MB1210();
            //deallocates PwmInput, AnalogInput, SerialOutput, and SerailInput
        void SoundVelocity(float MetersPerSecond);
            //if, for some reason, you need to correct the speed of sound
            //for Pwm modes, enter the new value here in meters per second;
            //default is 340.29 m/s
            //Note: most accurate mode
        void Voltage(float Volts);
            //sets expected operating voltage for Analog modes;
            //user responsibility to ensure operating voltage between 3.3 V and 5 V;
            //default is 3.3 V
        void Unit(float UnitsPerMeter);
            //argument sets the putput units through multiplication;
            //default is cm
        void Mode(char Selection);
            //argument sets operating mode;
            //set flag 0x08 to 0 for polled modes, or 1 for interrupt modes;
            //set flag 0x04 to 0 for synchronous modes, or 1 for asynchronous modes;
            //set flags 0x03 to 0 for pwm, 1 for analog, or 2 for serial;
            //asynchronous modes generate pulse width interrupts, prepare an
            //analog reading, and send a 5 byte serial reading every 99 ms;
            //interrupt modes automatically read the range into the buffer provided
            //by AttachInterruptBuffer with the selected input method every 99 ms
            //if in an asynchronous mode, or 99 ms after RequestSyncRead is called;
            //the rx pin must be connected to use an interrupt mode;
            //the tx pin must be connected to use a synchronous mode;
            //default is 0
        void AttachInterruptBuffer(float* Buffer);
            //if interrupts are used, user must provide address to write result to
        void RequestSyncRead();
            //this tells the device to prepare a synchronous range reading;
            //must be called at least 99 ms before the reading is needed;
            //changes asynchronous mode to synchronous equivalent
        void DiscardSerialBuffer();
            //the serial port has a buffer and only the oldest data is read from it;
            //the buffer has limited space and, when full, the newest data is discarded;
            //this method allows the user to empty the buffer of old data so new data is used
        float Read();
            //get a reading from the device in the set mode;
            //RequestSyncRead() must be called at least 99 ms
            //before this method can be called in a synchronous mode;
            //may be called at any time during asynchronous mode;
        operator float();
            //shorthand for taking a range reading;
            //ex: "float reading = MB1210Object;"
};

#endif

XxXxXxXxXxXxXxXxXxXxXxXxXxXx PwmIn.cpp XxXxXxXxXxXxXxXxXxXxXxXxXxXx

//mbed Microcontroller Library
//Pulse Width Modulation Input Interface
//Copyright 2010
//Thomas Hamilton

#include "PwmIn.h"

PwmIn::PwmIn(PinName pwi) : InterruptIn(pwi), PeriodMeasurement(0), PulseWidthMeasurement(1)
{
    mode(PullDown);
    rise(this, &PwmIn::PulseStart);
    fall(this, &PwmIn::PulseStop);
    start();
}

float PwmIn::read()
{
    return (float)PulseWidthMeasurement / PeriodMeasurement;
}

float PwmIn::period()
{
    return (float)PeriodMeasurement / 1000000;
}

int PwmIn::period_ms()
{
    return PeriodMeasurement / 1000;
}

int PwmIn::period_us()
{
    return PeriodMeasurement;
}

float PwmIn::pulsewidth()
{
    return (float)PulseWidthMeasurement / 1000000;
}

int PwmIn::pulsewidth_ms()
{
    return PulseWidthMeasurement / 1000;
}

int PwmIn::pulsewidth_us()
{
    return PulseWidthMeasurement;
}

void PwmIn::PulseStart()
{
    PeriodMeasurement = read_us();
    reset();
}

void PwmIn::PulseStop()
{
    PulseWidthMeasurement = read_us();
}

XxXxXxXxXxXxXxXxXxXxXxXxXxXx PwmIn.h XxXxXxXxXxXxXxXxXxXxXxXxXxXx

//mbed Microcontroller Library
//Pulse Width Modulation Input Interface
//Copyright 2010
//Thomas Hamilton

#ifndef PwmInLibrary
#define PwmInLibrary

#include "stdint.h"
#include "mbed.h"

class PwmIn : private InterruptIn, Timer
{
    private:
        unsigned int PeriodMeasurement;
        unsigned int PulseWidthMeasurement;

        void PulseStart();
        void PulseStop();

    public:
        PwmIn(PinName pwi);
        float read();
        float period();
        int period_ms();
        int period_us();
        float pulsewidth();
        int pulsewidth_ms();
        int pulsewidth_us();
};

#endif

XxXxXxXxXxXxXxXxXxXxXxXxXxXx main.cpp XxXxXxXxXxXxXxXxXxXxXxXxXxXx

#include "mbed.h"
#include "MB1210.h"

DigitalOut debugled(LED1);
Serial Computer(USBTX, USBRX);

MB1210 RangeFinder(p12, p15, p13, p14);
float Range;
int main()
{
    Computer.baud(9600);
    debugled = 0;
    RangeFinder.Unit(39.370);//change units to inches
    RangeFinder.AttachInterruptBuffer(&Range);
    RangeFinder.Mode(0x0A);
    while(1)
    {
        debugled = !debugled;
        RangeFinder.RequestSyncRead();//request a range reading
        wait_ms(100);//wait for reading to be prepared
        //RangeFinder.Mode(0);//switch to PWM mode
        //Computer.printf("PWM reading: %f in | ", Range);
        //RangeFinder.Mode(1);//switch to Analog mode
        //Computer.printf("Analog reading: %f in | ", Range);
        //RangeFinder.Mode(2);//switch to serial mode
        Computer.printf("Serial reading: %f in | ", Range);
        wait(0.9);
    }
}

XxXxXxXxXxXxXxXxXxXxXxXxXxXx EOF XxXxXxXxXxXxXxXxXxXxXxXxXxXx

MBED Example: How to use MS3DMGX2 3DM-GX2 Sensor libraries

.from: http://mbed.org/users/Blaze513/programs/3DM-GX2/lmj4el

XxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXx MS3DMGX2.cpp XxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXx

#include "MS3DMGX2.h"

MS3DMGX2::MS3DMGX2(PinName tx, PinName rx) : DataLines(tx, rx),
    CommandByte(0xCF), ResponseLength(6), PacketSize(31), Continuous(0)//,
   
    ///////////////////
    //PC(USBTX,USBRX)
    //////////////////
   
{
    DataLines.baud(115200);
    DataLines.format(8, Serial::None, 1);
    DataLines.attach(this, &MS3DMGX2::FillSerialBuffer, Serial::RxIrq);
       
        ////////////////////
    //PC.baud(9600);
    ////////////////////////
}

MS3DMGX2::~MS3DMGX2()
{
    delete this;
}

bool MS3DMGX2::Mode(unsigned char Selection)
{
    bool Result;
    switch (Selection & 0x03)
    {
        case 0x00:
            CommandByte = 0xCF;
            ResponseLength = 6;
            PacketSize = 31;
            break;
                //euler angles and angular rates
        case 0x01:
            CommandByte = 0xD2;
            ResponseLength = 9;
            PacketSize = 43;
            break;
                //gyro-stabilized acceleration, angular rate and magnetometer vector
        case 0x02:
            CommandByte = 0xC8;
            ResponseLength = 15;
            PacketSize = 67;
            break;
                //acceleration, angular rate and orientation matrix
        case 0x03:
            CommandByte = 0xCC;
            ResponseLength = 18;
            PacketSize = 79;
            break;
                //acceleration, angular rate, magnetometer vector, and orientation matrix
    }
        //record desired packet command and packet length as number of 16 bit fields
    if (Selection & 0x04)
    {
        unsigned char lbuff = PacketSize;
        PacketSize = 87;
       
        DataLines.putc(0xC4);
        DataLines.putc(0xC1);
        DataLines.putc(0x29);
        DataLines.putc(CommandByte);
            //send the desired continuous mode command
        Continuous = 1;
            //set synchronous mode to true
        while ((Buffer[BufferStart] != 0xC4) || (Buffer[BufferStart + 1] != CommandByte))
        {
            if (((BufferStart + 2) % PacketSize) != BufferEnd)
            {
                BufferStart++;
                BufferStart%=PacketSize;
            }
        }
            //find the response header
        while (((BufferStart + 8) % PacketSize) != BufferEnd);
        BufferEnd = 0;
        Result = Checksum(BufferStart, 8);
        BufferStart = 0;
        PacketSize = lbuff;////////////////NOTE: if there wasn't a packet waiting while the async cmd was made, there will be no "extra data" here
    }
    else
    {
        if (Continuous)
        {
            DataLines.putc(0xFA);
                //send stop continuous mode command
            Continuous = 0;
                //set synchronous mode to true
        }
        Result = 1;
    }
        //put the IMU into continuous mode if the correct flag is set
        //Computer.printf("stop command success %d \n", Workspace[0]);
        //Computer.printf("sync mode %d \n", SyncMode);
    /*if (Selection & 0x08)
    {
        DataLines.attach(this, &MS3DMGX2::Interrupt, Serial::RxIrq);
            //attach automatic buffer-writing function to serial interrupt
    }
    else
    {
        DataLines.attach(NULL, Serial::RxIrq);
            //attach a null to detach any previous interrupt
    }*/
        //attaches or detaches interrupt function depending on interrupt flag
    return Result;
        //return success or failure
}

bool MS3DMGX2::Readable()
{
    return BufferStart == BufferEnd;
}

/*void MS3DMGX2::AttachInterruptBuffer(float* Buffer)
{
    InterruptBuffer = Buffer;
}
    //store user's data pointer for use in interrupt modes

void MS3DMGX2::AttachInterruptFunction()
{
    DataLines.attach(this, &MS3DMGX2::Interrupt, Serial::RxIrq);
}
void MS3DMGX2::AttachInterruptFunction(void (*Function)())
{
    DataLines.attach(Function);
}
template<class Class> void AttachInterruptFunction(Class* Object, void (Class::*Function)())
{
    DataLines.attach(Object, Function, Serial::RxIrq);
}*/
    //overloads start interrupt modes, allowing user all possible options

void MS3DMGX2::RequestSyncRead()
{
    if (Continuous)
    {
        DataLines.putc(0xFA);
        Continuous = 0;
    }
    DataLines.putc(CommandByte);
}
    //lazy switches to synchronous mode and sends polled mode command byte

bool MS3DMGX2::Read(float* Data)
{
    bool Result;
    unsigned char t = 0;
   
    while ((Buffer[BufferStart] != CommandByte) && (t < PacketSize))
    {
        BufferStart++;
        BufferStart %= PacketSize;
        t++;
    }
        //find the header byte
    Result = Checksum(BufferStart, PacketSize);
        //compute checksum
    BufferStart++;
    BufferStart %= PacketSize;
        //move past header byte
    if (t < PacketSize)
    {
        for (unsigned int i = 0; i < ResponseLength; i++)
        {
            for(unsigned int j = 3; j < 4; j--)
            {
                ((unsigned char*)&Data[i])[j] = Buffer[BufferStart];
                BufferStart++;
                BufferStart %= PacketSize;
            }
        }
            //convert big endian bytes to little endian floats
        BufferStart += 6;
            //move index past timer and checksum bytes
        BufferStart %= PacketSize;
    }
        //if the header search did not timeout
   
    return Result;
}

/*MS3DMGX2::operator float*()
{
    Read((float*)&Workspace[8]);
    return (float*)&Workspace[8];
}
    //conversion function acts as shorthand for Read()

void MS3DMGX2::Interrupt()
{
    Read(InterruptBuffer);
}*/
    //this will be called when in an interrupt mode and a serial interrupt is generated

bool MS3DMGX2::Checksum(unsigned char Index, unsigned char Length)
{
    unsigned short Sum = 0;
    for (unsigned char i = 0; i < Length - 2; i++)
    {
        Sum += Buffer[Index];
        Index++;
        Index %= PacketSize;
    }
    return (((unsigned char*)&Sum)[0] == Buffer[Index+1])
        && (((unsigned char*)&Sum)[1] == Buffer[Index]);
}

void MS3DMGX2::FillSerialBuffer()//if the interrupt recurs faster than the time to complete its code, the rest of the system will be suspended indefinitely
{
    while (DataLines.readable())
    {
        Buffer[BufferEnd] = DataLines.getc();
        BufferEnd++;
      
        BufferEnd %= PacketSize;
    }
}
    //this automatically reads in new serial data as it is received to
    //make a software buffer that is big enough to handle an entire
    //packet; the processor is about 1000 times faster than the data lines

XxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXx MS3DMGX2.h XxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXx

#ifndef MS3DMGX2Library
#define MS3DMGX2Library

#include "stdint.h"
#include "mbed.h"

class MS3DMGX2
{
    private:
    /////////////////////
//Serial PC;
//////////////////////////

        Serial DataLines;
        volatile unsigned char Buffer[87];//volatile is for interrupted access
        volatile unsigned char BufferEnd;
        volatile unsigned char PacketSize;
        bool Checksum(unsigned char Index, unsigned char Length);
        void FillSerialBuffer();

    public:
        MS3DMGX2(PinName tx, PinName rx);
            //serial output, serial input
        ~MS3DMGX2();
            //release resources
        unsigned char BufferStart;
       
        unsigned char CommandByte;
        unsigned char ResponseLength;
        unsigned char Continuous; 
       
       
        bool Readable();
       
        bool Mode(unsigned char Selection);
            //argument sets operating mode;
            //set flag 0x08 to 0 for polled modes, or 1 for interrupt modes;
            //set flag 0x04 to 0 for synchronous modes, or 1 for asynchronous modes;
            //set flags 0x03 to 0 for pwm, 1 for analog, or 2 for serial;
            //asynchronous modes read user input command, measures and calculates
            //output, and writes the data packet to the serial buffer every 10 ms;
            //interrupt modes automatically read the packet into the buffer provided
            //by AttachInterruptBuffer with the selected input method every 10 ms
            //if in an asynchronous mode, or 10 ms after RequestSyncRead is called;
            //interrupt mode interrupts are generated if there are bytes on the
            //serial buffer and are only cleared if the serial buffer is emptied;
            //default is 0
        //void AttachInterruptBuffer(float* Buffer);
            //if interrupts are used, user must provide address to write result to
        //void AttachInterruptFunction();
            //this overload reattaches the native interrupt function
        //void AttachInterruptFunction(void (*Function)());
            //this overload attaches a function to the serial interrupt
        //template<class Class> void AttachInterruptFunction
        //    (Class* Object, void (Class::*Function)());
            //this overload attaches a member function to the serial interrupt;
            //to change the interrupt function, call one of the
            //"AttachInterruptFunction" overloads with pointers to the desired function;
            //changes polled mode to interrupt equivalent;
            //the interrupt will not be cleared until the serial buffer is emptied
        void RequestSyncRead();
            //this tells the device to prepare a synchronous reading;
            //must be called at least 10 ms before the reading is needed;
            //changes asynchronous mode to synchronous equivalent
        void DiscardSerialBuffer();
            //the serial port has a buffer and only the oldest data is read from it;
            //the buffer has limited space and, when full, the newest data is discarded;
            //this method allows the user to empty the buffer of old data so new data is used
        bool Read(float* Data);
            //get a reading from the device in the set mode;
            //RequestSyncRead() must be called at least 10 ms
            //before this method can be called in a synchronous mode;
            //may be called at any time during asynchronous mode;
            //it is assumed that the input buffer has enough
            //room to accomodate the desired data packet
        //operator float*();
            //shorthand for taking a reading;
            //ex: "float reading[packetlength]; reading = MB1210Object;"
};

#endif

XxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXx main.cpp XxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXx

#include "mbed.h"
#include "MS3DMGX2.h"

MS3DMGX2 IMU(p9, p10);
DigitalOut led(LED1);
DigitalOut led4(LED4);
Serial PC(USBTX,USBRX);
int main()
{
led=0;
led4=0;
PC.baud(9600);
    float data[9];
    bool isvalid;
    wait(1);
    isvalid = IMU.Mode(0x05);
    PC.printf("Data Valid: %d\r\n\r\n", isvalid);
    //IMU.RequestSyncRead();
    wait(1);
   
    while (1)
    {
        while(!IMU.Readable());
        isvalid = IMU.Read(data);
       
        PC.printf("IMU Accel x Reads %f\r\n",data[0]);//NOTE: the compiler will not even calculate unused variables
        PC.printf("IMU accel y Reads %f\r\n",data[1]);//If the data retrieved above was not used, it wouldn't even be retrieved and the validity check would not pass
        PC.printf("IMU accel z Reads %f\r\n",data[2]);//this is called "compiler optimization"
       
        PC.printf("IMU ang rate x Reads %f\r\n",data[3]);
        PC.printf("IMU ang rate y Reads %f\r\n",data[4]);
        PC.printf("IMU ang rate z Reads %f\r\n",data[5]);
       
        PC.printf("IMU mag x Reads %f\r\n",data[6]);
        PC.printf("IMU mag y Reads %f\r\n",data[7]);
        PC.printf("IMU mag z Reads %f\r\n",data[8]);
        PC.printf("Validity: %d\r\n\r\n", isvalid);
       
        wait_ms(1000);
    }
}

XxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXx EOF XxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXx

MBED Example: How to read MaxSonar EZ1 using mbed AnalogIn

.from: http://mbed.org/users/shimniok/programs/MaxSonar_EZ1_Analog/llju06


#include "mbed.h"

// MaxSonar EZ1 test program
// by Michael Shimniok http://www.bot-thoughts.com/
//
// Based on datasheet here: http://www.maxbotix.com/uploads/LV-MaxSonar-EZ1-Datasheet.pdf
// Reads from AN (analog) pin connected to mbed p20, assumes 3.3V supply to EZ1 module.
//
// mbed -> EZ1
// -----------
// VOUT -> +5
// GND  -> GND
// p20  -> AN
//

AnalogIn ain(p20);
Serial pc(USBTX, USBRX); // tx, rx

int main() {
    float adc, volts, inches;
    int feet, in;
   
    pc.baud(115200);
   
    while (1){
        adc = ain.read();           // read analog as a float
        volts = adc * 3.3;          // convert to volts
        inches = volts / 0.0064;    // 3.3V supply: 6.4mV per inch
        feet = (int) inches / 12;   // inches to feet (trunc)
        in = (int) inches % 12;     // remainder -> in(ches)
       
        pc.printf("%8.2f adc %8.2fV %8.2f in %d'%d\"\n", adc, volts, inches, feet, in);

        wait(0.05);                 // 20Hz update rate ; note we aren't truly synchronized to the device or anything...  
    }
}