Electric Skate Board

electric skateboard

 

Describing my electric skateboard build. I have only driven this board seven miles so I cant say how far it will go. But I can say that its scary fast and very fun!

Electrical was a cinch!

Below you can see the layout of the parts that are needed for the electronics. I screwed and glued a waterproof case to contain all this to the bottom of the board.

electric skateboard parts

My big mistake

This board is fast I wish it was higher off the ground with larger inflatable tires. Secondly because electric motor low rpm torque is terrible I wish I had a kit that reduced the gear ratio even more than my current kit which was purchased off amazon. See below and notice how this wheel had a bunch of small holes. Making sure your gear fits your wheel is probably the most difficult part of the build if you order the wrong stuff. Also my belt was at maximum tension and I ended up adding bearings on each side that tension the belt.

electric skateboard wheel

 

electric skateboard pulley idler

 

Controlling software

So I had to code the arduino to accept blue tooth commands. Here the code below loops on the bluetooth signal and the gradually increases or reduces an int value that the esc is programmed to recognize.

 

 

#include <Servo.h>
#include <SoftwareSerial.h>
#include <Arduino.h>


SoftwareSerial mySerial(5,6);
Servo myServo;
int input = 0;
int inChar = 90;
int current = 90;
int waiter = 0;

void setup() {

Serial.begin(9600);
mySerial.begin(9600);
myServo.attach(9);

Serial.println("Arduino Ready!!");
}

void loop() {

waiter ++;

if (mySerial.available() > 0){

input = mySerial.read();

if (input != 0)
{
inChar = input;
}

//Serial.println(inChar);


}

delay(15);
//Serial.println("Set motor");
//Serial.println(inChar);
if (current < inChar)
{
if (waiter > 10){
current ++;
waiter = 0;
Serial.println(current);
}
}else if (current > inChar){

current--;
}
myServo.write(current);

Serial.println(current);

}

 

The android code was lengthy. Below is a view of inside android studio and youll see my constraint layout.

skateboard control android app

 

 

Following that is my two classes composing the skateboard controller.

 

<pre>public class MainActivity extends AppCompatActivity {

    String TAG = "MainActivity";
    TextView textViewSkateBoardInfo, textViewFeedback;
    EditText editTextInputToBoard;
    Button buttonSubmit, buttonBrake, buttonCoast, buttonCruise, buttonMinus, buttonPlus;
    Spinner spinnerDevices;
    SeekBar seekBarSpeed;

    int desiredSpeed = 0;


    String[] deviceNames;
    BluetoothAdapter bluetoothAdapter;
    ArrayAdapter<String> spinnerAdapter;
    int chosenDevice = 0;
    UUID MY_UUID;
    boolean threadControl = false;
    boolean bluetoothConnectionActive = false;
    Set<BluetoothDevice> pairedDevices;
    MyBlueToothService myBlueToothService;
     Handler mHandler;

    ConnectThread connectThread;

    //region----------------------------------------    Overrides & Permissions




    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        this.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);


            setUpUI();

            checkPermissions();







    }



    private void checkPermissions()
    {
        String[] permissions = new String[] {

                Manifest.permission.ACCESS_FINE_LOCATION,
                Manifest.permission.ACCESS_COARSE_LOCATION,
                Manifest.permission.BLUETOOTH,
                Manifest.permission.BLUETOOTH_ADMIN
        };


        ActivityCompat.requestPermissions(MainActivity.this, permissions, 10);


    }



    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);


        setupBlueTooth();

    }


    //endregion


    //region----------------------------------------    UI



    private void setUpUI()
    {
        MY_UUID = UUID.fromString("ca805ff2-39d8-47ca-a9a4-ca6227358943");
        textViewFeedback = findViewById(R.id.textview_FeedBack);
        textViewSkateBoardInfo = findViewById(R.id.textView_SkateBoard);
        editTextInputToBoard = findViewById(R.id.editText_InputToSkateBoard);
        buttonSubmit = findViewById(R.id.button_Submit);
        spinnerDevices = findViewById(R.id.spinner_Devices);
        buttonBrake = findViewById(R.id.button_Brake);
        seekBarSpeed = findViewById(R.id.seekBar_speed);
        buttonCoast = findViewById(R.id.button_Coast);
        buttonCruise = findViewById(R.id.button_Push);
        buttonMinus = findViewById(R.id.button_minusFive);
        buttonPlus = findViewById(R.id.button_plusFive);

        seekBarSpeed.setProgress(90);

        buttonSubmit.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                buttonClick();
            }
        });

        buttonCoast.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                desiredSpeed = 98;
                seekBarSpeed.setProgress(98);
                sendSpeed(desiredSpeed);
                String s = "Speed set at " + String.valueOf(desiredSpeed);
                writeToInternal(s);

            }
        });

        buttonCruise.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                desiredSpeed = 105;
                seekBarSpeed.setProgress(105);
                sendSpeed(desiredSpeed);
                String s = "Speed set at " + String.valueOf(desiredSpeed);
                writeToInternal(s);

            }
        });

        buttonMinus.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                desiredSpeed = desiredSpeed -5;
                seekBarSpeed.setProgress(desiredSpeed);
                sendSpeed(desiredSpeed);
                String s = "Speed set at " + String.valueOf(desiredSpeed);
                writeToInternal(s);

            }
        });

        buttonPlus.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                desiredSpeed = desiredSpeed +5;
                seekBarSpeed.setProgress(desiredSpeed);
                sendSpeed(desiredSpeed);
                String s = "Speed set at " + String.valueOf(desiredSpeed);
                writeToInternal(s);

            }
        });




        seekBarSpeed.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
                desiredSpeed = i;

            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {

            }

            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {
                sendSpeed(desiredSpeed);
                String s = "Speed set at " + String.valueOf(desiredSpeed);
                writeToInternal(s);
            }
        });


        buttonBrake.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                desiredSpeed = 90;
                seekBarSpeed.setProgress(90);
                sendSpeed(desiredSpeed);
                String s = "Speed set at " + String.valueOf(desiredSpeed);
                writeToInternal(s);

            }
        });



        mHandler = new  Handler(){

            @Override
            public void handleMessage(Message msg) {

                if (msg.what == 0){

                byte[] b = (byte[]) msg.obj;
                    // TODO: 5/12/2018 heres the response
                    String s = new String(b);

                    textViewFeedback.setText(s);

                }



            }
        };

    }




    private void buttonClick()
    {
        String s = editTextInputToBoard.getText().toString();

        if (!bluetoothConnectionActive ){
            runConnectThread();
            Log.d(TAG, "buttonClick: connecting");
        }else if(s.equals(""))
        {
            connectThread.cancel();
            myBlueToothService.cancel();
            Log.d(TAG, "buttonClick: dissconnecting");
            setupBlueTooth();
        }


            if (!s.equals(""))
            {
            sendString(s);
                Log.i(TAG, "buttonClick: sending ext input");
            }




    }

    public void writeToInternal(String input)
    {
        final String s = input;

        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                textViewSkateBoardInfo.setText(s);

            }
        });



    }

    public void writeToBlueToothResponse(String input)
    {
        final String s = input;
        Log.d(TAG, "writeToBlueToothResponse: " + input);

        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                textViewFeedback.setText(s);
            }
        });


    }


    public void sendString(String s)
    {
        try {
            byte[] bytes = s.getBytes("UTF-8");
            myBlueToothService.writeToDevice(bytes);
            writeToInternal(s);
        }catch (UnsupportedEncodingException e){
            Log.e(TAG, "sendString: ", e);
            writeToInternal("Failed to send");
        }

    }


    public void sendSpeed(int toSend)
    {


        byte[] b = ByteBuffer.allocate(4).putInt(toSend).array();

        if (bluetoothConnectionActive) {

            myBlueToothService.writeToDevice(b);

            Log.i(TAG, "sendSpeed: sending...");
        }
    }


    //endregion


    //region----------------------------------------    Bluetooth

    private void setupBlueTooth()
    {


        bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        if (bluetoothAdapter == null){
            Toast.makeText(this, "device doesnt support bluetooth", Toast.LENGTH_SHORT).show();
        }

        if (!bluetoothAdapter.isEnabled()){
            Intent enableBTIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
            startActivityForResult(enableBTIntent, 20);
        }

        pairedDevices = bluetoothAdapter.getBondedDevices();


        if (pairedDevices.size() > 0){
            deviceNames = new String[pairedDevices.size()];


            int tick = 0;
            for (BluetoothDevice device :
                    pairedDevices) {

                deviceNames[tick] = device.getName();



                tick++;
            }

            spinnerAdapter = new ArrayAdapter<String>(this,android.R.layout.simple_spinner_item, deviceNames);

            spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);

            spinnerDevices.setAdapter(spinnerAdapter);

            spinnerDevices.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
                @Override
                public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {

                    chosenDevice = i;

                }

                @Override
                public void onNothingSelected(AdapterView<?> adapterView) {

                }
            });

        }



    }



    public void runConnectThread()
    {
    BluetoothDevice deviceToInsert = null;

    for (BluetoothDevice d :
            pairedDevices) {
        if (d.getName().equals(deviceNames[chosenDevice])){
            deviceToInsert = d;
        }
    }
    Log.i(TAG, "runConnectThread: try to connect");

   connectThread = new ConnectThread(deviceToInsert);
    connectThread.begin();

}


    private class ConnectThread implements Runnable
    {
    private  BluetoothSocket mmSocket;
    private final BluetoothDevice mmDevice;
    Thread thread;


    public ConnectThread(BluetoothDevice device)
    {
        if (device == null){
            Toast.makeText(MainActivity.this, "BT Device Null", Toast.LENGTH_SHORT).show();
        }

        BluetoothSocket tmp = null;

        mmDevice = device;


        try{

            tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
        }catch (Exception e){
            Log.e(TAG, "ConnectThread: ",e );
        }


        mmSocket = tmp;

    }




    @Override
    public void run() {

        bluetoothAdapter.cancelDiscovery();

        try {
            mmSocket = (BluetoothSocket) mmDevice.getClass().getMethod("createRfcommSocket", new Class[]{int.class}).invoke(mmDevice, 1);
            mmSocket.connect();

        }catch (Exception e){
            Log.e(TAG, "run: ", e);
            writeToInternal( "failed connection to " + deviceNames[chosenDevice]);

                try {

                    mmSocket.close();
                }catch (Exception e2){
                    Log.e(TAG, "run: ", e2);
                }
                return;



        }

            writeToInternal( "connected to " + deviceNames[chosenDevice]);
            //do something with mmSOcket here
            myBlueToothService = new MyBlueToothService(mmSocket);
            Log.i(TAG, "Connection Success");
            bluetoothConnectionActive = true;
            passSTart();


    }




    public void begin()
    {
        thread = new Thread(this);
        threadControl = true;
        thread.start();
    }


    public void cancel()
    {
        bluetoothConnectionActive = false;
        threadControl = false;
        try {
            mmSocket.close();
        }catch (Exception e){
            Log.e(TAG, "cancel: ", e);
        }

    }


}

    public void passSTart()
    {
        myBlueToothService.passMain(this);
    }




    //endregion



    //region----------------------------------------    lifecycle

    @Override
    protected void onDestroy() {
        super.onDestroy();
        myBlueToothService.cancel();

    }


    //endregion


}</pre>

 

</pre>
<pre>    private interface MessageConstants {
        public static final int MESSAGE_READ = 0;
        public static final int MESSAGE_WRITE = 1;
        public static final int MESSAGE_TOAST = 2;


    }


    public  MyBlueToothService(BluetoothSocket socket)
    {
        connectedThread = new ConnectedThread(socket);
        connectedThread.startListeneing();


    }


    public void writeToDevice(byte[] b)
    {
        connectedThread.write(b);
    }


    public void cancel()
    {
        connectedThread.cancel();
    }

    public void passMain(MainActivity a)
    {
        connectedThread.setMainActivity(a);
    }



    private class ConnectedThread implements Runnable
    {
        Thread thread;
        MainActivity mainActivity;

        private final BluetoothSocket mmSocket;
        private  DataInputStream mmInputStream;
        private DataOutputStream mmOutputStream;
        private byte[] mmBuffer;

        public ConnectedThread(BluetoothSocket socket){

            mmSocket = socket;
            InputStream tmpIn = null;
            OutputStream tmpOut = null;


            try
            {
              tmpIn = socket.getInputStream();
                Log.i(TAG, "ConnectedThread: input stream success");
            }catch (Exception e){
                Log.e(TAG, "ConnectedThread: failed", e);
            }
            try
            {
              tmpOut = socket.getOutputStream();
                Log.i(TAG, "ConnectedThread: output stream success");
            }catch (Exception e){
                Log.e(TAG, "ConnectedThread: failed", e);
            }

            mmInputStream = new DataInputStream(tmpIn);

            mmOutputStream = new DataOutputStream(tmpOut);





        }


        public void run()
        {
            mmBuffer = new byte[1024];
            int numbytes = 0;


            while (threadBool)
            {
                try
                {
                  numbytes = mmInputStream.read(mmBuffer);
                    //Log.i(TAG, "mybluetooth read bufer bypassed");

                    if (numbytes > 0){
                        //Log.d(TAG, "read numbytes > 0");

                        String s = new String(mmBuffer, "UTF-8");
                        mainActivity.writeToBlueToothResponse(s);

                    }

                }catch (Exception e){
                    Log.e(TAG, "run: ", e);
                    break;
                }



            }




        }





        public void write(byte[] bytes)
        {
            byteHolder = bytes;

            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {

                            WriteToBT();

                }
            });
            t.start();

        }





        private void WriteToBT()
        {
            //Log.d(TAG, "WriteToBT: " + String.valueOf(byteHolder.length));


            try{
                mmOutputStream.write(byteHolder);
                //Log.i(TAG, "WriteToBT: passed");
                int i = new BigInteger(byteHolder).intValue();
                //Log.d(TAG, "WriteToBT: " + String.valueOf(i));

            }catch (Exception e){
                Log.e(TAG, "write: ", e);

            }
        }



        public void startListeneing()
        {threadBool = true;
        thread = new Thread(this);

        thread.start();

        }



        public void cancel()
        {
            threadBool = false;
            try{
                mmSocket.close();
            }catch (Exception e){
                Log.e(TAG, "cancel: ", e);
            }


        }



        public void setMainActivity(MainActivity a)
        {
            mainActivity = a;
        }



    }



}</pre>
<pre>

and that’s it!
The post Electric Skate Board appeared first on SignalHillTechnology.

Powered by WPeMatico

Updating the easy time card app

easy time card app android

 

Updated and revised

 

After releasing the easy time card app I realized that it looked very dull and relied heavily on androids basic graphics. It was a utility app I made for myself so early on this was ok. But after releasing it I wanted it to look great but keeps its same super simplistic functionality. As you can see in the image above the display really looks like a time card. I’m very pleased that I was also able to add a csv and pdf attachment to the emails as well. Here is the little youtube video promo I made for it. You can check it our on the google play store 

 

 

 

 

 

 

 

The post Updating the easy time card app appeared first on SignalHillTechnology.

Powered by WPeMatico

Git Hub Tutorial / Cheat Sheet

git tutorial / cheat sheet

 

 

Using Git was a bit of a headache the first time around.

I remember day 1 complaining about it every 5 minutes. After a few days it becomes a trusted friend.

Here is the tutorial I wish I found when learning to use it.

Enjoy…and download the pdf version for the yellow highlighting which helps a lot

 

Download PDF Here

 

GIT CHEAT SHEET

Highlighted yellow are steps to load new project for first time

git version

Make sure git is installed

git config –global user.name “yourname”

git config –global user.email “youremail”

add your contact and signature to commits automatically

git config –list

see you current set up

git help <verb>

git <verb> –help

get help and commands about subject

dir cd “name of folder” cd ..

dir show a list of file and cd takes you there. Cd .. takes you back

git init

This creates a folder in your local code which will keep track of the code and communicate with remote code source. Nothing si automatic. Your “working folder” contents must be added in future steps for the folder git init to work with it.

git status

(See .gitignore at this point)

This will show files that are not being tracked by your git init object.

This will show files that are being tracked and are in the staging area

git add <somefile>

git add -A

This will add files to the staging area. They are still in working folder but git understand you want to work with themin future git commands.

git reset <filename>

git reset

removes this from staging are. Git still sees it but is not working with it anymore.

git commit -m “your message”

commit command creates a data point saying someone updated the project at this point. The -m command is required so you can send a message to let future readers know what this commit was about. (Try git status now. It will say that your working directory is clean. You havent uploaded yet but there is nothing more to upload besides the files buffered into your commit)

git log

You can see the log of commits

git remote add “somename” “somelocation”

This command will link your git init object to the remote repository. “somename might be “origin” and “somelocation might be ” c:projectsfilesthisproject” or “https://bitbucket.org/yourprofile/yourproject/project.git

git push -u origin master

Remember when you made a commit? This takes that commit and pushes it to the remote repository. The -u symbol i have no idea. The origin defines the repository location which was added in the previous instruction. And the master means added to the master branch

git pull origin master

same as above but reverse. Bring remote to local

git clone <url>

Navigate your console to desired download location and clone a repository there wth this command. Remmber this does everything for youso no need to make a git init.just reference project folder probably

git remote -v

view remote repo info

git branch -a

check branches. Unless you a master commit kinda person(your not)

git diff

see code changes between working and local staging

The post Git Hub Tutorial / Cheat Sheet appeared first on SignalHillTechnology.

Powered by WPeMatico

Adding a splash video player on Android

On my app I wanted to include an introductory video loaded directly ino the app instead of pulled form an internet source.

Once the app was created I used a resource called handbrake which compressed my video into just a few megs.

 

The I applied the following code…

 

My main activity has two class object.

FrameLayout frameLayout;
<pre>ImageView im;</pre>

 

My method calls lead to a splashPlayer() which loads the video. The sequence is @Override:onCreate() -> createStart()this loads all my startup stuff -> splashPlayer() this loads the video

The frame layout below is linked to my xml layout as usual. Here  we set up our video. Then call another method addPlayButton()

</pre>
<pre>public void splashPlayer()
{

    try {</pre>
<pre>final VideoView videoHolder = new VideoView(this);

frameLayout.addView(videoHolder);
Uri video = Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.sound);
videoHolder.setVideoURI(video);</pre>
<pre>addPlayButton(true);</pre>
<pre>

The addplay button shows a play button over your black square so people understand its a movie.

</pre>
<pre>public void addPlayButton(boolean show)
{

    if (show){
        im = new ImageView(this);
        im.setImageResource(R.drawable.playicon1);
        final WindowManager.LayoutParams params = new WindowManager.LayoutParams();
        params.width = 50;
        params.height = 50;

        frameLayout.addView(im, params);
    }else
    {
        frameLayout.removeView(im);
    }


}</pre>
<pre>

The where back in splashplayer and setting our controls.

</pre>
<pre>
        videoHolder.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {

                switch ( motionEvent.getAction()) {

                    case MotionEvent.ACTION_DOWN:

                        Log.i(TAG, "onTouch: touched");
                        if (videoHolder.isPlaying()) {
                            Log.i(TAG, "onTouch: 1");
                            videoHolder.pause();
                            addPlayButton(true);

                        } else {
                            Log.i(TAG, "onTouch: 2");
                            videoHolder.start();
                            addPlayButton(false);
                        }
                        break;

                }

                return true;
            }
        });

    }catch (Exception e){
        Log.i(TAG, "splashPlayer: try/catch fail");
    }

}</pre>
<pre>

Total code as follows

</pre>
<pre>public void splashPlayer()
{

    try {

        Log.i(TAG, "splashPlayer: trying...");

        final VideoView videoHolder = new VideoView(this);

        frameLayout.addView(videoHolder);
        Uri video = Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.sound);
        videoHolder.setVideoURI(video);

        addPlayButton(true);

        //videoholder.setoncompletelistener
        videoHolder.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {

                switch ( motionEvent.getAction()) {

                    case MotionEvent.ACTION_DOWN:

                        Log.i(TAG, "onTouch: touched");
                        if (videoHolder.isPlaying()) {
                            Log.i(TAG, "onTouch: 1");
                            videoHolder.pause();
                            addPlayButton(true);

                        } else {
                            Log.i(TAG, "onTouch: 2");
                            videoHolder.start();
                            addPlayButton(false);
                        }
                        break;

                }

                return true;
            }
        });

    }catch (Exception e){
        Log.i(TAG, "splashPlayer: try/catch fail");
    }

}</pre>
<pre>

The post Adding a splash video player on Android appeared first on SignalHillTechnology.

Powered by WPeMatico

Building a pdf programmatically on Android

In my recent project I needed to build a pdf and email it to a user. Android has a great library for this and I found it only took a few hours to get a reasonably readable pdf onto my screen.

 

The basic outline goes as follows

//create the pdf
<pre>PdfDocument reportDocument = new PdfDocument();
//create the page</pre>
<pre>PdfDocument.Page page = reportDocument.startPage(pageInfo);</pre>
<pre>
/*
* Do some stuff here. see further down
*/

</pre>
<pre>//end the page
reportDocument.finishPage(page);</pre>
<pre>//send it away to a file for storage or in my case an email for sending</pre>
<pre> pdfPath = getPDFFile(openProject.getProjectNme());
       FileOutputStream fos = new FileOutputStream(pdfPath);
        reportDocument.writeTo(fos);
        reportDocument.close();
        success = true;</pre>
&nbsp;

Now for writing stuff on the pdf itself

 

//I had  made a chart earlier and wanted it displayed
<pre>page.getCanvas().drawBitmap(charts[0], 5, graphy, paint)

//drawing some text on the page by setting up a paint object</pre>
<pre>    Paint paint = new Paint();
    paint.setColor(Color.BLACK);  
    paint.setTextAlign(Paint.Align.CENTER);
    paint.setTextSize(25);

//then writing</pre>
<pre>page.getCanvas().drawText("Source of Sound",width/2, titley, paint);</pre>
&nbsp;

 

Really this is a super hackey way of doing it. There must be a cleaner API or if i ever need to do this again I will write one. But it works so I guess Ill come back to that at a later time.

 

 

 

Complete code here.

<pre>private void makePDF()
{
    Log.i(TAG, "makePDF: started");

    String sHighestDec;
    String sAverageHighestDecibel;

    //region
    String weighting = "For the purpose of soundproofing a nuisance noise must be 10 decibel lower than the ambient decibels of ";
    String weighting2 = "the rooms future use. So an office/library of 40 decibels would need its soundproofing to bring exterior";
    String weighting3 = "nuisance noise to 30 decibels to not be noticed by end users. Frequently due to uncontrollable ambient ";
    String weighting4 = "factors it will be impossible to measure the new low of your target sound if it drop below 40 db";

    String outerLimits = "Remember this is app is a crude measurement and educational tool.";
    String outerlimits2 = "Your  device may not produce or record some sounds due to hardware limitations";
    String outerlimits3 = "Secondly, you may not be able to hear all range of sounds. Wear ear protection!";


    //endregion
    int width = 1224;
    int height = 1584;
    int left = 5;
    float titley = ((float)(height * .04));
    float titleOney = ((float)(height * .06));
    float titleTwoy = ((float)(height * .08));
    float graphy = ((float)(height * .1));
    float lineOney = ((float)(height * .55));
    float lineTwoy = ((float)(height * .57));
    float lineThreey = ((float)(height * .59));
    float lineFoury = ((float)(height * .61));
    float lineFivey = ((float)(height * .63));
    float lineSixy = ((float)(height * .65));
    float lineSeveny = ((float)(height * .67));
    float lineEighty = ((float)(height * .69));
    float lineNiney = ((float)(height * .71));

    PdfDocument reportDocument = new PdfDocument();

    //page 1
    PdfDocument.PageInfo pageInfo = new PdfDocument.PageInfo.Builder(width,height,1).create();

    PdfDocument.Page page = reportDocument.startPage(pageInfo);

    Paint paint = new Paint();
    paint.setColor(Color.BLACK);

    //title of page
    Log.i(TAG, "makePDF: page 1");
    paint.setTextAlign(Paint.Align.CENTER);
    paint.setTextSize(25);

    //title
    String info1 = openProject.getProjectNme() + " " + openProject.getProjectLocation();
    page.getCanvas().drawText("Source of Sound",width/2, titley, paint);
    page.getCanvas().drawText(info1,width/2, titleOney , paint);
    info1 = openProject.getDate();
    page.getCanvas().drawText(info1,width/2, titleTwoy, paint);


    //chart
    page.getCanvas().drawBitmap(charts[0], 5, graphy, paint);

    paint.setTextAlign(Paint.Align.LEFT);
    //highest decibel and at what frequency recordedHighestAverage
    sHighestDec = "The highest decibel recorded was : " + String.valueOf(valuesChartOne[0]) + "db at approximately " + String.valueOf(valuesChartOne[1] + " hz");
    page.getCanvas().drawText(sHighestDec, left, lineOney, paint);

    //average decibel recorded during total running average
    sAverageHighestDecibel = "The average decibel recorded was : " + String.valueOf(valuesChartOne[2]);
    page.getCanvas().drawText(sAverageHighestDecibel, left, lineTwoy, paint);

    //outerlimits
    page.getCanvas().drawText(outerLimits, left, lineThreey, paint);
    page.getCanvas().drawText(outerlimits2, left, lineFoury, paint);
    page.getCanvas().drawText(outerlimits3, left, lineFivey, paint);


    reportDocument.finishPage(page);


    //page 2
    Log.i(TAG, "makePDF: page 2");
    if  (charts[1] != null) {
        pageInfo = new PdfDocument.PageInfo.Builder(width, height, 2).create();

        page = reportDocument.startPage(pageInfo);

        //title of page
        paint.setTextAlign(Paint.Align.CENTER);
        paint.setTextSize(25);
        page.getCanvas().drawText("Receiving Area", width / 2, titley, paint);

        //chart of recording
        page.getCanvas().drawBitmap(charts[1], 5, graphy, null);

        paint.setTextAlign(Paint.Align.LEFT);
        //highest decibel and at what frequency
        sHighestDec = "The highest decibel recorded was : " + String.valueOf(valuesChartTwo[0]) + "db at approximately " + String.valueOf(valuesChartTwo[1] + " hz");
        page.getCanvas().drawText(sHighestDec, left, lineOney, paint);

        //average decibel and at what frequency
        sAverageHighestDecibel = "The average decibel recorded was : " + String.valueOf(valuesChartTwo[2]);
        page.getCanvas().drawText(sAverageHighestDecibel, left, lineTwoy, paint);

        generateSTC();

        //if barrier what stl or not calculatable
        if (stcSuccess){

            String values = "Db lost : ";

            for (double d :
                    diff) {
                //Log.i(TAG, "makePDF: " + String.valueOf(d));
                int t = (int) Math.round(d);

                values += String.valueOf(t) + " ";

            }



            page.getCanvas().drawText(values , left, lineThreey, paint);
            page.getCanvas().drawText("Stc rating of this barrier is " + String.valueOf(stcRating), left, lineFoury, paint);

        }else{

            page.getCanvas().drawText("Stc rating was not successfully calculated", left, lineThreey, paint);

        }



        // weighting a sound
        page.getCanvas().drawText(weighting, left, lineFivey, paint);
        page.getCanvas().drawText(weighting2, left, lineSixy, paint);
        page.getCanvas().drawText(weighting3, left, lineSeveny, paint);
        page.getCanvas().drawText(weighting4, left, lineEighty, paint);

        reportDocument.finishPage(page);

    }


    //page 3
        if (charts[2] != null) {

            Log.i(TAG, "makePDF: page 3");
            pageInfo = new PdfDocument.PageInfo.Builder(width, height, 3).create();
            page = reportDocument.startPage(pageInfo);

            Material m = chosenMaterial.get(0);


            //title of page
            paint.setTextAlign(Paint.Align.CENTER);
            paint.setTextSize(25);
            page.getCanvas().drawText("Simulation", width / 2, titley, paint);
            info1 = m.getMaterialName() + " - " + m.getMaterialDescription();
            if (info1.length() < 100)
            {
                page.getCanvas().drawText(info1, width / 2, titleOney, paint);
            } else  {

                String one = info1.substring(0, info1.length()/2);
                String two = info1.substring(info1.length()/2 +1, info1.length());

                page.getCanvas().drawText(one, width / 2, titleOney, paint);
                page.getCanvas().drawText(two, width / 2, titleTwoy, paint);
            }


            //chart of recording
            page.getCanvas().drawBitmap(charts[2], left, graphy, null);

            paint.setTextAlign(Paint.Align.LEFT);
            //highest decibel and at what frequency
            sHighestDec = "The highest decibel recorded was : " + String.valueOf(valuesChartThree[0]) + " at approximately " + String.valueOf(valuesChartThree[1] + " hz");
            page.getCanvas().drawText(sHighestDec, left, lineOney, paint);

            //average decibel not being calculated


            //describe green and red marks
            info1 = "The green bars are "A" weighted to human perception based on frequency.";
            page.getCanvas().drawText(info1,left,lineFoury , paint);

            info1 = "The red dots are NC25 rating. (Allowable sound penetration for many use types)";
            page.getCanvas().drawText(info1,left,lineFivey , paint);

            reportDocument.finishPage(page);
        }



    boolean success = false;

    try {


       pdfPath = getPDFFile(openProject.getProjectNme());
       FileOutputStream fos = new FileOutputStream(pdfPath);
        reportDocument.writeTo(fos);
        reportDocument.close();
        success = true;

}catch (IOException f) {
        Log.e(TAG, "makePDF: ", f);
    }finally {

        if (success) {
            notifyObserver(1);
        }else   {
            notifyObserver(2);
        }


    }





}</pre>

The post Building a pdf programmatically on Android appeared first on SignalHillTechnology.

Powered by WPeMatico

Draw an animated bar chart on Android

How I drew a live bar chart in my sound proofing and STC app available on google play

Fast Fourier transform FFT android

 

The above picture is a screenshot of my android app where I needed to live display over 100 data points onto my screen. Here is how I did just that.

Important to know is that these data points came in the form of double array of [156] from my fast Fourier transform. With these 156 data point I needed to do the following;

  1. Draw a background and figure out how much of this data was needed to fill in my chart
  2. Grab that data asynchronously from a separate thread doing the calculations
  3. Take my data point and figure how wide and tall they need to be.
  4. Draw the previous record and then draw the new total over the top of it.
  5. Do it 30 times per second and post back to the UI

 

So let me start with item #2… how the data was getting there. The background thread that was giving me the 156 data points was refreshing very fast. I didn’t need all the data for the purpose of the user display and I chose to simply post it to an array repeatedly. This way it could post as frequently as it wanted and I could grab the data as frequently as a wanted without interruption to either. Loosely coupled might be the appropriate terminology.

 

This is my class wide field I posted too.

private double[][] uiChartBuffer;

This is what I call a controlling method. I have several inner classes within my audiocontrol class which do the work on different worker threads.
My controlling methods allow the classes utilizing the audiocontrol object to make its inner classes perform their functions without actually touching them. Here you
can see that I start recording and I instantiate the inner class drawchart and call its resume method. These inner classes are 100% accessed by their own controlling methods as well. It may
also be important to mention that the audiocontrol's controlling methods are affected by the android lifecycle. Killing the inner classes automatically.

So if the user gets a phone call and moves out of the app there are no leaked resources:
 Fragment.onPause() -> audioControl.onPause(this call a bunch of methods such as the one below) -> drawChart.pause and null -> recordAudio.pause() and null;
public void startRecording(File file)
{
 Log.i(TAG, "startRecording: ");
 running = !running;

 drawChart = new DrawChart(context);

 fileToWork = file;


 recordAudio = new RecordAudio();
 recordAudio.execute(getFile());


 drawChart.resume();

 

At the very bottom is my entire inner drawchart class. There is a resume and pause method, a method to check if the screen update is ready and a method that just loops over and over trying to update the screen.
Anything that doesn't have to do with the user interaction should be in a background thread. This is no exception however android doesn't allow background threads to touch the UI so you have to us a workaround.
In this case I used a surface view which is a very typical way of doing this.






</pre><pre>private class DrawChart extends SurfaceView implements Runnable
{
    SurfaceView surfaceView;
    private SurfaceHolder surfaceHolder;
    private Canvas canvas;
    private Paint paint;
    Thread thread = null;
    TextView textViewDecibelShow;
    int highestDeciebelFromSample;


    private int mBarColorF;
    private int mBarColorS;
    private Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.newhzdb);
    BitmapDrawable bitmapDrawable = new BitmapDrawable(getResources(),bitmap);



    private long nextFrameDue;
    private long frameLength = 100;
    int width, height;
    int barWidth, seqS, seqF,  mod;
    double eWidth, reverse, bHeight;
    float bh;


    public DrawChart(Context context) {
        super(context);

        surfaceView = myFragmentView.findViewById(R.id.surfaceView);
        textViewDecibelShow = myFragmentView.findViewById(R.id.textViewliveDB);
        surfaceHolder = surfaceView.getHolder();

        nextFrameDue = System.currentTimeMillis();



        mBarColorF = ResourcesCompat.getColor(getResources(),R.color.colorBarChartFluctuate,null);
        mBarColorS = ResourcesCompat.getColor(getResources(),R.color.colorBarChartStay, null);
        paint = new Paint();

        width = 0;



    }


    @Override
    public void run() {


        while  (running || playing)
        {

            if (thread.isInterrupted())
            {
                return;
            }


            if (updateRequired())
            {

                highestDeciebelFromSample = 0;

                if (surfaceHolder.getSurface().isValid() &amp;&amp; uiChartBuffer != null) {



                    canvas = surfaceHolder.lockCanvas();
                    // Log.i(TAG, "run: in midst of draws while loop");

                    if (width == 0){

                        width =  surfaceView.getWidth();    //screen dimensions 540
                        height = surfaceView.getHeight();   //screen dimensions 600

                        int almostwidth = (int) Math.round( width *.89) ;
                        barWidth = (int) almostwidth/reqBars;                       //4?
                        int netwidth = reqBars * barWidth;                         //464?
                        int grossWidth = (int) Math.round(netwidth/.90);            //515

                        width = grossWidth;



                        mod = getMod(width);
                        bHeight = height * .965;             // random number i found to keep max reading within chart
                        bh = (float) bHeight;

                        setDbChartScaleFactor(bHeight/myscale);

                        bitmapDrawable.setBounds(0,0,width,height);
                        //scaledBitmap = Bitmap.createScaledBitmap(bitmap,width,height,true);
                    }

                    bitmapDrawable.draw(canvas);





                    for (int i = 1; i &lt; reqBars ; i++) {

                        seqF = i * barWidth + mod; //right
                        seqS = seqF - barWidth; //left



                        int bottom = reverseandCalc(uiChartBuffer[0][i], false);

                        //Log.i(TAG, "run: " + String.valueOf(bottom));

                        if (bottom &gt; highestDeciebelFromSample){
                            highestDeciebelFromSample = bottom;
                        }




                        if (bottom &gt; highestDecibelByBar[i])
                        {// if this data is higher then add to record
                            highestDecibelByBar[i] = bottom;
                        }else {
                            //if not then draw prev record behind it

                            int top = (int) bh - highestDecibelByBar[i];

                            paint.setColor(mBarColorS);
                            canvas.drawRect(seqS, top, seqF, bh, paint);      //draw prev record redbar underneath
                        }


                        int top = (int) bh - bottom;


                        paint.setColor(mBarColorF);
                        canvas.drawRect(seqS, top, seqF, bh, paint); //draw green bar on top
                    }



                    surfaceHolder.unlockCanvasAndPost(canvas);


                }

                //averageDecibelForView = average(highestDeciebelFromSample);

                notifyObserver(3);

            }

        }

    }


    public void resume()
    {
        highestDecibelByBar = new int[reqBars];
        thread = new Thread(this);
        thread.start();
        Log.i(TAG, "resume: draw thread");

    }


    public void destroy()
    {
        thread.interrupt();
    }

    // TODO: 5/1/2018 update based not on time but on fft method calls
    public boolean updateRequired()
    {

        if (nextFrameDue &lt;= System.currentTimeMillis()){

            nextFrameDue = System.currentTimeMillis() + frameLength;


            return true;
        }
        return false;





    }





}
</pre><pre>

 


 

 

The post Draw an animated bar chart on Android appeared first on SignalHillTechnology.

Powered by WPeMatico

Getting Fast Fourier Transform data on Android

How I broke down the sound waves in my sound proofing and STC app available on google play

 

In order to build this app I needed to understand how loud a sound was as well as which frequency I was hearing. Truthfully when I started this app I underestimated how difficult this would be.

 

Building this app requires a few things;

 

  1. You need to sample the pressure of the airwaves hitting the microphone very quickly in order to build a sine wave. I found a great website with some images that included the following and this really gave me the basis of my understanding. Check out this guys website for more info here http://www.vlf.it/fft_beginners/fft_beginners.html

 

In order to accomplish that I built an android class called audio control. I built an interface so that fragments could get an instance of audio control and make sure to call all the methods needed in androids lifecycle.

Here are the fields at the top of the class;








<pre>//   my inner classes
private PlayAudio playAudio;
private RecordAudio recordAudio;
private TestTone testTone;

private AudioRecord audioRecord;
private AudioTrack audioTrack;

private RealDoubleFFT transformer;
// TODO: 4/18/2018 buffer size to 4096 so we narrow our frequency range down a bit?
private int SAMPLE_RATE = 11025;  //chnageable. lowest is 11025 according to newventuresoftware we have a gaurantee capture of 44100/ im dubious based on intial testing of 100 to 20k hz
private int blockSize = 256; // keep here number of component frequency samples that our transform object will output ...so is that 44100 /2 /256 = 86.132??
//but how because you only give it the raw input data
private int halfBlock = blockSize/2;
private int hzSpec;
private int reqBars =  0; //this reworks at 116? bars total
//int reqBars = 30;</pre>

 

Down below I have an inner class which extends Async Class








<pre>
private class RecordAudio extends AsyncTask<File, double[], Boolean>
{
    String TAG = "RECORD AUDIO";

    private static final int AUDIO_SOURCE = MediaRecorder.AudioSource.MIC;
    private static final int CHANNEL_MASK = AudioFormat.CHANNEL_IN_MONO;
    private static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT;
    long startTime =0;
    long endTime = 0;
    FileOutputStream waveOut;



    @Override
    protected Boolean doInBackground(File... files) {

        try {

            dbAverage = new int[smooth];
            recordedHighestAverage = 0;
            totalRunningAverage.clear();
            int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_MASK, ENCODING);
            audioRecord = new AudioRecord(AUDIO_SOURCE, SAMPLE_RATE, CHANNEL_MASK, ENCODING, minBufferSize);

            if (files[0] != null) {
                waveOut = new FileOutputStream(files[0]);
                writeWavHeader(waveOut, CHANNEL_MASK, SAMPLE_RATE, ENCODING);
            } else {
                waveOut = null;
            }

            int bufferReadData;
            byte[] buffer1 = new byte[blockSize];
            long total = 0;

            try {
                startTime = SystemClock.elapsedRealtime();
                audioRecord.startRecording();

            } catch (IllegalStateException e) {

                Log.e(TAG, " Records doInBackground: " + e.toString());
            }


            while (running) {
                //Log.i(TAG, "while");

                bufferReadData = audioRecord.read(buffer1, 0, blockSize);  //we are requesting 256 byte obj and android is sending us 16bit so each byte hold half of a 16 bit!!


                publishProgress( createFFT(bufferReadData, buffer1));




                if (files[0] != null) {

                    createWavFile(total, bufferReadData, buffer1);
                }


            }

        } catch (IOException e) {
            Log.e(TAG, "Records doInBackground: " + e.toString(), e);
            stoprecording();
        } finally {

            Log.i(TAG, "Records doInBackground: calld from 2nd");

            endTime = SystemClock.elapsedRealtime();

        }

        if (waveOut != null) {

            try {
                updateWavHeader(files[0]);

            } catch (IOException e) {
                Log.e(TAG, "doInBackground: ", e);

            }
        }</pre>

 

Notice above where I publish progress to my FFT method. The above code merely show you how to sample the sounds from the air and get them into a byte array for more processing. Next we need to do some data manipulation and get the Fourier Transform and Decibel information.

I found a great website that explains that a sine wave is merely a circle with time expressed as well. Great presentation on their part and although I don’t completely feel I have internalized the lesson it has definitely improved my understanding. https://betterexplained.com/articles/an-interactive-guide-to-the-fourier-transform/

 

I ended up downloading the FFT pack that is free and open source on the internet. You can download it by searching   ->    ca/uol/aig/fftpack

Add it as a library and then use the FFT as follows:

Initialize at top of class above all your methods.

<pre>private RealDoubleFFT transformer;</pre>

 

Here is the method called by our background thread gathering the audio data. As you can see I needed both bytes and short in this software and stumbled through the process of making them as seen in my little sketch below which was my reference point.

Notice below the trasnformer.ft() method is doing all the FFT work. Everything before that is just cramming data into a 16 bit configuration. Or getting me my decibel value.

 

<pre>
protected double[] createFFT(int bufferReadData, byte[] buffer1)
{
    double[] toTransform = new double[blockSize/2];
    double sum = 0;
    int block = halfBlock;
    double REF = 0.00002;
    //ByteBuffer.wrap(buffer1).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().put(buffer);   //this hopefully creates the buffer1 array

    // xx is correct bits. Here we load byte from buffer lilendian to big to feed into the newBuff
    //  xx xx xx xx 00 00 00 00
    //  00 00 00 00 xx xx xx xx
    //  xx xx xx xx xx xx xx xx
    // 0xFF added to fix a left padding problem??

    short newBuff, buffLil, buffBig, count = 0;

    for (int i = 0; i < blockSize/2 && i < bufferReadData/2; i++) //after 128 runs with a double increment it is pulling what data?
    {

        buffLil = buffer1[count];
        buffBig = buffer1[count + 1];
        newBuff = (short) ( buffBig <<8 | buffLil & 0xFF);
        //Log.i(TAG, "createFFT: short " + String.valueOf(newBuff));
        count ++;
        count++;

        /* testing new stuff here */
        if (newBuff > 0) {

            sum += Math.abs(newBuff);

        }else
        {
            block--;
        }


        toTransform[i] = (double) newBuff / 32768.0;             //This takes the short and divides by total short value to give a decimal double value between -1.0 to 1.0 for input into fft

    }


    double x = sum/block;
    //Log.i(TAG, "createFFT: average amplitude " + String.valueOf(x));

    double db = 0;

    if (x != 0) {

        double pressure = x/51805.5336;

        db = (20 * Math.log10(pressure/REF));

        if (db > 0){

            //Log.i(TAG, "createFFT: " + String.valueOf(db));
            int d = (int) db;
            //Log.i(TAG, "createFFT: db" + String.valueOf(d));
            averageDecibelForView = average(d);
        }




    }

    transformer.ft(toTransform);


    return(toTransform);


}</pre>
&nbsp;

 

You may also notice that I am calculating my decibels based on the guesstimated microphone pressure values someone provided me. You may also noticed That I push those into the average() method.

This fft was running very fast despite the math required and I was only refreshing the screen about 30 times per second so not all the data needed to hit the screen and surely the user didnt need to see the decibel amplitude 30 times per second.

Below you can see the result. I will get into drawing the display on my next post but it shows the current decibels top left and moving bar chart below.

 

Fast Fourier transform FFT android

 

 

 

The post Getting Fast Fourier Transform data on Android appeared first on SignalHillTechnology.

Powered by WPeMatico

Sound Proof STC APP – Available for android on google play store

The essence of construction if building a barrier between us and the elements of nature or often, just as importantly, other annoying people.

But how do we do that relating to sound?

 

In the late 60 sound proofing became a more examined topic in construction. It continues to evolve today but very little of that evolution is in the hands of the end user and installer. So despite the importance of sound proofing construction decisions are frequently left to ” Well, this should work” … But will it?

One of the main barriers is the lack of knowledge by both sound engineers in how to get more information to contractors and contractors in understanding how sounds travel through buildings.

 

I have a lot of respect for sound engineers and testing agencies but…

They fail to apply their knowledge to real world situations. Testing in a laboratory is great. It’s accurate and provides crucial research and data. In that same vein taking a leer jet is much faster and safer than driving a car. But the reality is cars get more people more miles every year than leer jets do.

In much the same way the average person can’t stand around waiting for their leer jet to arrive we cannot stand around depending on these tests to make practical use of the information collected. The average property owner or builder needs something cheap and reasonably effective yesterday.

 

I have a lot of respect for contractors and designers but…

Frequently they are guessing. They know they can get someone to analyze acoustics and sometimes they do. But most of the time they use their ears make some assumptions and just start building stuff until the sound gets to a tolerable level.

 

I designed this app to give people making decisions every day something to help in that decision making process.

To allow people to quickly grab some data for review later, or to take someone with zero knowledge and help them make a better informed decision.

Is it perfect…no. But its better than nothing.

 

Below are a few screen shots. Check it out on google play.

 

 

sound proof and stc app android

sound proof and stc app android

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

sound proof and stc app android

sound proof and stc app android

Powered by WPeMatico

Project Recaps

Interested in some recent projects completed?

 

Laboratory

Long Beach, Ca

This project was local and was a cinch to handle. Turning a torn up commercial mixed use building into a laboratory.  The scope of work included a new electrical service, architectural repairs, cabinetry, rooftop HVAC units and other amenities associated with a laboratory. Here is a 4 picture snapshot of start to finish.





 

A picture is worth a thousand words when communicating with multiple parties to overcome obstacles. Yes, I have a thermal camera and yes your jealous.

 





 

 

Warehouse

Long Beach, Ca

Another day, another flip. Freshen up the office and warehouse. Demo the mezzanine. Install some security features. Rinse repeat!

 




 

 





 

 

Professional Office

Los Angeles, Ca

Retail into a professional office.





 

 

Retail Strip Mall

Lomita, Ca

Fixing up a store front strip mall. Painting, roofing signage and other improvements.

 





 

New offices

Los Angeles, Ca

Large open office into private offices.




 

Warehouse Remodel with New Offices

Vernon, Ca

Offices from the beginning of time. Everything must go!





 





 

 

Gym at Retail Strip Mall

Orange County, Ca

Empty space to gym.

building a gym

building a gym

gym metal stud framing

gym metal stud framing

gym construction complete

gym construction complete

gym construction drywall texture

 

 

Powered by WPeMatico

Easy Time Card App – The paper time card alternative

Many offices today are still filling out paper time cards. Why you ask?

2018-03-06 11.12.29

  1. Nefarious software integration
  2. The learning curve
  3. Bugs or “features”

Almost everyone is guilty of it. Software products are designed to work with constant support resulting in monthly or yearly fees. Upgrades, server costs etc. But most people aren’t getting paid to figure out software. Many low skill employees can’t or drag their feet. They want the solution that stays the same. Not one that changes with the weather. As they sort through all the numerous buttons they passively fear that the next upgrade will change things and they will spend more time relearning. Barf.

 

But paper time cards suck!

Paper time cards take time to fill out. There is zero space to write. They get smudged and lost plus people have to hand deliver them back to the office. Someone needs to sort through them. It’s not really a great solution.

 

A better solution

For sale on google play store is the EASY TIME CARD APP. A compromise between the two.  This app is real easy. Two buttons to operate it and if the setup is too hard someone else can do it once and its done. Right is a screen shot from the main screen of the app. You can see the current day being shown as clocked in at the top.

 

At the bottom, when the user is ready to clock out just press the big red button.

 

When the user is ready to report their time they can press the button on the bottom  labeled report time. This send the time card over in an easily readable email without any work.

 

 

2018-03-06 10.57.43

Here you can see the email which is automatically made after the user clicks the “Report Time” button.

It super clear and easy to read. It tracks starting and ending times as well as total work times.

As workers go from job to job they can track how much time they are spending at each location or task.

Best of all the records can be emailed to three people. So now that single time card can be reviewed and saved digitally.

Workers are still on the honor system because these time card emails can be edited manually. This is helpful is you make a mistake and forget to clock in. Simply type in a note on the email or change a start time.

Most importantly this is something 99% of people already know how to do. This app isn’t trying to change your life. Its simply the exact same paper time cards you have relied on in a digital format!

 

Powered by WPeMatico