domenica 2 ottobre 2016

Moving


Even though I didn't have any free time in the last  six months to update the blog, I've felt like it was time to upgrade to a more "serious" solution for blogging that could let me have total control over what I was doing and how I present my work.

This week I invested in a vps and I've officially bought the domain http://randomguydev.com where I'll continue to deliver contents about the work I do from time to time.

see you there :)

sabato 16 gennaio 2016

Implementing bluetooh LE on android

Intro

Recently I found myself working on this really simple application while testing bluetooth le implementation on android for a particular and obscure device. Only after I've already started to work on the application I've found out that the device I was trying to communicate with was actually using bluetooth 2.0, because of this you will find the code to actually handle both LE and 2.0 bluetooth technology.
Of course all the information you need is available on android developers webpage, but you can find here an already working example (even tough it's incomplete, I only had few hours to work on this).

Code

Here the code of the main activity, useful info can be found in the comments.


import butterknife.Bind;
import butterknife.ButterKnife;

public class MainActivity extends AppCompatActivity {

    //
    // View & View control
    //

    @Bind(R.id.container)
    LinearLayout container;
    @Bind(R.id.supported_text)
    TextView supportedText;
    @Bind(R.id.list)
    ListView listView;

    DeviceArrayAdapter devicesArrayAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        checkPhoneReq();
        setupLayout();

        //for classic scan
        // Register the BroadcastReceiver
        IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
        registerReceiver(mReceiver, filter); // Don't forget to unregister during onDestroy
    }

    void setupLayout() {
        devices = new ArrayList<>();
        devicesArrayAdapter = new DeviceArrayAdapter(this, R.layout.list_item);
        listView.setAdapter(devicesArrayAdapter);
        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView parent, View view, int position, long id) {
                //start details activity
                BluetoothDevice device = devicesArrayAdapter.getItem(position);
                startDetailsActivity(device);
            }
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        unregisterReceiver(mReceiver);
    }

    private class DeviceArrayAdapter extends ArrayAdapter {
        Context context;
        int res;

        public DeviceArrayAdapter(Context context, int resource) {
            super(context, resource, devices);
//            this.devices = devices;
            this.context = context;
            res = resource;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            LayoutInflater inflater = LayoutInflater.from(context);
            View row = inflater.inflate(res, parent, false);
            BluetoothDevice device = devices.get(position);
            TextView tv = (TextView) row.findViewById(R.id.label1);

            tv.setText("TYPE: " + device.getType() + " CLASS: " + device.getBluetoothClass());
            tv = (TextView) row.findViewById(R.id.label2);

            tv.setText("NAME: " + device.getName() + " STATE: " + device.getBondState());
            //STATE 10 = none 11 = bounding 12= bounded
            tv = (TextView) row.findViewById(R.id.label3);
            tv.setText(device.getAddress());
            return row;
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        if (id == R.id.action_settings) {
            log("Not implemented");
            return true;
        }
        if (id == R.id.action_scan) {
            if (!mScanning) {
                log("Start scanning for devices");
                scanForLeDevices(true);
            } else {
                log("Stopping");
                scanForLeDevices(false);
            }
            return true;
        }
        if (id == R.id.action_scan_classic) {
            log("Start scanning for devices (Classic)");
            scanForDevicesClassic();
            return true;
        }
        if (id == R.id.action_query) {
            queryPaired();
        }
        if (id == R.id.action_clear) {
            clearList();
        }
        return super.onOptionsItemSelected(item);
    }

    void log(String s) {
        Snackbar.make(container, s, Snackbar.LENGTH_LONG)
                .setAction("Action", null).show();
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == REQUEST_ENABLE_BT) {
            // Make sure the request was successful
            if (resultCode == RESULT_OK) {
                //...
                log("Bluetooth enabled.");
            }
            if (requestCode == RESULT_CANCELED) {
                log("Bluetooth must be enabled.");
            }
        }
    }

    //
    // Behaviour
    //

    BluetoothAdapter mBluetoothAdapter;
    BluetoothLeScanner mBluetoothScanner; //new
    private final int REQUEST_ENABLE_BT = 1;
    private boolean mScanning;
    private Handler mHandler;
    // Stops scanning after 10 seconds.
    private static final long SCAN_PERIOD = 10000;
    ArrayList devices;

    void clearList(){
        devices.removeAll(devices);
        devicesArrayAdapter.notifyDataSetChanged();
    }

    void checkPhoneReq() {
        // Use this check to determine whether BLE is supported on the device. Then
        // you can selectively disable BLE-related features.
        if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
            log("Bluetooth LE not supported");
            supportedText.setText("not supported");
        }

        // Initializes Bluetooth adapter.
        final BluetoothManager bluetoothManager =
                (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
        mBluetoothAdapter = bluetoothManager.getAdapter(); //Req min API 18

        // Ensures Bluetooth is available on the device and it is enabled. If not,
        // displays a dialog requesting user permission to enable Bluetooth.
        if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
            //error prompting the user to go to Settings to enable Bluetooth:
            Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
            startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
        }
    }

    //Battery-Intensive operation
    //Guidelines:
    // As soon as you find your device, stop.
    // Never in a loop
    // Set a time limit
    //Need 2 implementation if support is needed for < API 21
    private void scanForLeDevices(boolean enable) {
        int apiVersion = android.os.Build.VERSION.SDK_INT;
        mHandler = new Handler(Looper.getMainLooper());
        if (apiVersion > android.os.Build.VERSION_CODES.KITKAT) {
            useScanner(enable);
        } else {
            useScannerOld(enable);
        }
    }

    void useScanner(boolean enable) {
        if (enable) {
            mBluetoothScanner = mBluetoothAdapter.getBluetoothLeScanner();
            // scan for devices but only for a specified time
            mHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    mScanning = false;
                    if (mBluetoothScanner != null) {
                        mBluetoothScanner.stopScan(mScanCallback);
                        log("Scan ended after 10s");
                    }
                }
            }, SCAN_PERIOD);

            mBluetoothScanner.startScan(mScanCallback);

        } else {
            mScanning = false;
            if (mBluetoothScanner != null)
                mBluetoothScanner.stopScan(mScanCallback);
        }
    }

    //for kitkat and below
    void useScannerOld(boolean enable) {
        // targetting kitkat or bellow
        if (enable) {
            mHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    mScanning = false;
                    mBluetoothAdapter.stopLeScan(mLeScanCallback);
                }
            }, SCAN_PERIOD);

            mBluetoothAdapter.startLeScan(mLeScanCallback);
        } else {
            mScanning = false;
            mBluetoothAdapter.stopLeScan(mLeScanCallback);
        }
    }

    //DEVICE SCAN CALLBACK

    //FOR NEW DEVICES
    ScanCallback mScanCallback = new ScanCallback() {
        @Override
        public void onScanResult(int callbackType, ScanResult result) {
            BluetoothDevice device = result.getDevice();
            addDeviceToList(device);
        }

        @Override
        public void onScanFailed(int errorCode) {
            super.onScanFailed(errorCode);
            log("Scan failed with code: " + errorCode);
        }
    };

    //FOR OLD DEVICES
    BluetoothAdapter.LeScanCallback mLeScanCallback = new BluetoothAdapter.LeScanCallback() {
        @Override
        public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    addDeviceToList(device);
                }
            });
        }
    };

    //Adds the device to a List Adapter
    void addDeviceToList(BluetoothDevice device) {
        Log.v("test", "Found something.");
        devices.add(device);
        devicesArrayAdapter.notifyDataSetChanged();
    }

    ///
    void queryPaired() {
        Set deviceSet = mBluetoothAdapter.getBondedDevices();
        if (deviceSet.size() > 0) {
            for (BluetoothDevice device : deviceSet) {
                addDeviceToList(device);
            }
        }
    }

    ////

    // Create a BroadcastReceiver for ACTION_FOUND
    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            // When discovery finds a device
            if (BluetoothDevice.ACTION_FOUND.equals(action)) {
                // Get the BluetoothDevice object from the Intent
                BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                // Add the name and address to an array adapter to show in a ListView
                addDeviceToList(device);
            }
        }
    };

    void scanForDevicesClassic() {
        /*
        The process is asynchronous and the method will immediately return with
        a boolean indicating whether discovery has successfully started.
        The discovery process usually involves an inquiry scan of about 12 seconds,
        followed by a page scan of each found device to retrieve its Bluetooth name.
         */
        mBluetoothAdapter.startDiscovery();
    }


    public final static String EXTRA_BTDEVICE = "btdevice";

    //on list click
    void startDetailsActivity(BluetoothDevice device){
        Intent i = new Intent(this, DetailsActivity.class);
        i.putExtra(EXTRA_BTDEVICE, device);
        startActivity(i);
    }
}


Download

Source code on git

domenica 15 novembre 2015

Using redis as a base for a spatial model

Recently I've been working with on a server application that could maintain the spatial location of its clients and make them query their neighbours. Following are listed some of the design choices for this project and we discuss how redis is perfect as a base for the spatial computing model.

Design

The system we want to build is a key component for the spatial computing system, its task is to mediate interactions between neighbours and maintain a consistent representation of the network of devices.

As shown in the picture, this system is made up of three exential logical parts.
The connector, is the component of the system that enables network communications between server and devices.
A resource is an object with a type, associated data, relationships to other resources, and a set of methods that operate on it. It models an important concept of the system that can be accessed remotely with standard methods like HTTP GET, POST and PUT.
The model must support a consistent representation of the device network and enables spatial query to be made considering the position of each node. Given the nature of the system, further consideration must be made on the model; data must be fresh and an outdated information must expire after a given time, querying the model should be fast and thread safe and must perform well even with large quantity of data.

In sight of simplicity and the fact that the server should be able to manage as much as requests as possible we decided to build a system that is based on the ReST model.
Using ReST architecture means using a simple client-server request/response protocol for communication, moreover it enables the designer to think about the concept of resource and its representation. Next we define what resources are made of and how they behave in relation to HTTP methods GET, PUT, POST, etc.

Redis Introduction

Redis is an open-source, in-memory data structure storage that is build to work with large quantity of data. It’s simple to use and with a well-made documentation, regardless it offers the following features that makes it perfect of our applicative scenario:

Redis has it’s own set of action that can be performed on data structures, the full set of commands includes:
  • SET and GET to store and fetch information using a key string.
  • SADD and ZADD to add items to a set or sorted set.
  • SREM and ZREM to remove an item from a set or sorted set.
  • GEOADD to add an item with longitude and latitude to a geo index.
  • GEORADIUS to get items from a geo index inside a radius of given meters from a point.

Redis Implementation

Information of one node should be indexed by the network id of our model and the node id itself, this means that we can use these two types of data structure for storing our model:
  • a simple string indexed with a combination of the netId and nodeId for saving all information about a note state (using for example json formalism).
    • that can be made expirable after a given amount of seconds.
  • a geospatial index (geolist) indexed by the netId containing all nodes at a given latitude and longitude.
    • that can be used for neighbourhood search.


The reason we don’t simply use the geospatial index is that redis can’t mark as expirable elements of the geospatial index as well for elements of sets and lists. This means that consistency between the geolist and active nodes in a network should be given by the application, this could be done every time we fetch nodes from a network in four simple steps:

  1. Get the list of node ids from the geolist
  2. For every id in the list fetch the respective string representation from the db
    1. if the string is null mark the node id for deletion.
    2. otherwise parse the string and save the node into a list.
  3. Remove all marked node ids from the geolist in a single redis call.
  4. Return the list of parsed nodes.

Of course because interleaving of these operation can occur, case in which a node is removed from the geospatial index even if it was just added to the network exists. However we consider the price of using synchronism too high to pay since in our scenario we have to deal with devices that continuously update their state and losing track of a device for a given time is expected.

For communicating with redis lettuce was used as a client api, no authentication mechanism was implemented, all needed informations are redis machine ip number and port location.

Redis Error Measurement

Quoting redis.io webpages on distance measurement from two geopoints: “The distance is computed assuming that the Earth is a perfect sphere, so errors up to 0.5% are possible in edge cases.”
We want to test some cases to assert the quality of the measurement for our applicative scenario.

We take some distinctive points in Cesena (Italy):

// Piazza del popolo 44.137199, 12.241922
LatLonPosition piazza = new LatLonPosition(44.137199, 12.241922);
String idPiazza = addNewNode(44.137199, 12.241922);
// Piazza (pappa reale) 44.137560, 12.241320
addNewNode(44.137560, 12.241320);
// Rocca 44.135996, 12.240365.
addNewNode(44.135996, 12.240365);
// via chiaramonti (biblioteca) 44.140027, 12.242564
addNewNode(44.140027, 12.242564);
// via sacchi 3 (facoltà) 44.139623, 12.243427
addNewNode(44.139623, 12.243427);

And we check near places in a range of 100m from Piazza del popolo using the provided range search of redis implemented in the system.

NBR:[
idPappaReale
[pos: [lat:44.13756,lon:12.24132], values: {}, sensors: {}]
Distance: 62.60068090367517
idPiazza
[pos: [lat:44.137199,lon:12.241922], values: {}, sensors: {}]
Distance: 0.0
]

As expected the system shows two nodes, one being the center of the range search. More tests confirms that the georadius function of redis returns the expected values, but since we want to study the quality of these results we check possible error in measuring the distance from two points because that could result in obtaining more or less points from georadius.

Using Google Maps to obtain the “real” distance between two points, we test and see error given by redis.

Real distance
Redis distance
Error
62.45 m
62.60 m
0,15 m
183.67 m
185.13 m
1,46
315.23 m
318.52 m
3,29
295.96 m
274.38 m
21,58
1.07 km
1.07 km
< 0.01 km
669.89 km
670.07 km
0,18 km
642.52 km
642.71 km
0,19 km

Data shows that redis measurement have an error in meters that increases based on the range used for the range search. However since in our scenario we are interested in short distance range search between 5m and 200m at least, the error provided by the application is marginal.

Repository



martedì 8 settembre 2015

cake quest


I think I've grown quite a fan of regular show trough the years, so when in latest episode: Birthday Gift Rigby decided to make a videogame for his friend, of course I took the chance to make a playable version of it. I know you won't be disappointed !

I really don't know why I invested 1 hour and half on making this, but I thought it might be funny! All credits and art goes obviously to regular show creators and cartoon network!

Download Links:
Windows version
Web version

Images


Episode preview