Tutorial overview
This tutorial demonstrates the use case how to develop an application for UCW Platform. The Smart Tracker is an example of how to implement the monitoring of moving objects for UCW Portal (source code). The tracker used in this example is Adafruit GPS FeatherWing micro-controller which employs the Global Positioning System (GPS) technology. Due to its high acceptability and accuracy rate, GPS has been applied in diverse areas such as transportation, agriculture, law enforcement, communication, military and weather forecasting. It uses a system of satellites to provide accurate time and location (position) of objects on the ground (earth). It is owned by the U.S government. Other navigation satellite systems are GLONASS, launched by Russia and Galileo positioning system, created by the European Union.
Requirements
Step 01: Download tutorial sources
In order to download tutorial sources and test their functionality, do as follows:
- Make sure you have downloaded all the requirements above.
- Open a terminal/console and navigate to the directory where you want to work on the tutorial.
Download the sources using following command:
git clone https://github.com/unitycloudware/smart-tracker.git smart-tracker
Move into newly created folder
cd smart-tracker
Test downloaded files by building and running the sources. In order to do so, execute following commands
Unix / Linux based OS./build.sh ./run-portal.sh
or
Windowsbuild.bat run-portal.bat
The application should be up and running on http://localhost:9602 (TODO - add link to setting tomcat port section). After clicking the link you should see the login page. If you see the login page you can proceed to the next step.
You can login into the application using basic credentials (User: admin, Password: admin). (TODO - add link to credential settings)
Step 02: Dashboard
Data captured from the GPS sensor is displayed on a dashboard in a tabular and pictorial form. The framework for organising how the data will be displayed on a dashboard is handled by the nsys-plugin.xml file
<dashboard key="ucw-smarttracker_dashboard" name="UCW Smart Tracker Dashboard"> <description>This dashboard displays data for geolocation coordinates from GPS modules.</description> <label>Smart Tracker</label> <viewId>ucw-smarttracker</viewId> <imageUri>${portalResourcesUrl}/resources/images/ucw_logo.png</imageUri> <actionButtons>ucw.smarttracker.dashboard.header.actions</actionButtons> </dashboard>
Each dashboard is uniquely identified by a name and key.
Step 03: Gadget Configuration
Adding Geolocation data and Geolocation map gadgets to a dashboard using the nsys-plugin.xml file.
adding gadgets<dashboard-gadget key="ucw-smarttracker_geolocation-data-gadget" name="Geolocation Data Gadget" class="com.unitycloudware.portal.tutorial.smarttracker.gadget.GeolocationDataGadget"> <description>Displays a table with geolocation coordinates from GPS modules.</description> <label>Geolocation Data</label> <column>left</column> <order>0</order> <view>ucw-smarttracker</view> </dashboard-gadget> <dashboard-gadget key="ucw-smarttracker_geolocation-map-gadget" name="Geolocation Map Gadget" class="com.unitycloudware.portal.tutorial.smarttracker.gadget.GeolocationMapGadget"> <description>Displays a map with geolocation coordinates from GPS modules.</description> <label>Geolocation Map</label> <column>right</column> <order>0</order> <view>ucw-smarttracker</view> </dashboard-gadget>
A dashboard consists of one or more gadgets. Each gadget has a unique name and key. All gadgets on this dashboard must have the same viewID value as the view of the dashboard
GeolocationData model represents data that are sent from the device to the data stream ucw-smarttracker. It represents JSON in following structure
GeolocationData{"latitude":49.27398114208278,"longitude":-123.12387756842338,"timestamp":1520097514875}
The gadget uses data model to display information about latitude, longitude and time stamp.
Template file: The format in which data is displayed on a gadget is determined by a Velocity template (.vm file) using HTML. For Geolocation Data gadget, the data is displayed in a tabular format, while same data is represented as an image in Geolocation Map gadget.
geolocation-data.vm#parse ("macros.vm") #if ($!{data.isEmpty()}) #showMessage("info", "Status", "No data to display.", false) #else <table class="aui aui-table-interactive aui-table-sortable"> <thead> <tr> <th id="data-lattitude">Lattitude</th> <th id="data-longitude">Longitude</th> <th id="data-timestamp">Timestamp</th> </tr> </thead> <tbody> #foreach ($d in $data) <tr> <td headers="data-lattitude"><span class="aui-lozenge aui-lozenge-current">$String.format("%.2f", $!{d.lattitude})</span></td> <td headers="data-longitude"><span class="aui-lozenge aui-lozenge-current">$String.format("%.2f", $!{d.longitude})</span></td> <td headers="data-timestamp">$date.format("yyyy-MM-dd HH:mm:ss", $!{d.timestamp})</td> </tr> #end </tbody> </table> #end
geolocation-map.vm$portalResourceManager.requireResourcesForContext("ucw.smarttracker.geolocation.map") <br/> <div id="location-map" class="location-map"></div> <script async defer src="https://maps.googleapis.com/maps/api/js?key=AIzaSyB39lqCBWjMzIagsSuQ44e8OHFrRyw34is&callback=initLocationMap"> </script> <br/>
the class value "location-map" in the velocity template file above refers to .css that controls the dimension of the map
location-map.location-map { height: 500px; width: 100%; }
Test Data Utils: The data stream received from sensors to the UCW platform are saved in projects. A project can consist of different datastreams. Each data stream comprises of the deviceID and data (payload). If a project or any of the data stream components do not already exist, the TestDataUtils.java creates each one. It does this by first creating project manager, device manager and data manager
private ProjectManager projectManager; private DeviceManager deviceManager; private DataManager dataManager; public ProjectManager getProjectManager() { if (projectManager == null) { projectManager = ComponentProvider.getInstance().getComponent(ProjectManager.class); } return projectManager; } public DeviceManager getDeviceManager() { if (deviceManager == null) { deviceManager = ComponentProvider.getInstance().getComponent(DeviceManager.class); } return deviceManager; } public DataManager getDataManager() { if (dataManager == null) { dataManager = ComponentProvider.getInstance().getComponent(DataManager.class); } return dataManager; }
then we proceed to create a project, device,and data
protected void createProject() { log.debugFormat("Creating project '%s' with key '%s'...", PROJECT_NAME, PROJECT_KEY); if (getProjectManager().getProjectByKey(PROJECT_KEY) != null) { return; } Project project = new Project(); project.setKey(PROJECT_KEY); project.setName(PROJECT_NAME); project.setDescription(PROJECT_DESC); if (getProjectManager().addProject(project) != null) { getProjectManager().configureProject(PROJECT_KEY); } } protected void createDevice() { log.debugFormat("Creating device '%s' for project '%s'...", DEVICE_NAME, PROJECT_KEY); Project project = getProjectManager().getProjectByKey(PROJECT_KEY); if (project == null || getDeviceManager().getDeviceByName(PROJECT_KEY, DEVICE_NAME) != null) { return; } Device device = getDeviceManager().createDevice(DEVICE_NAME, DEVICE_DESC, DeviceType.GENERIC.name(), 120, PROJECT_KEY); getDeviceManager().addDevice(device); } protected void createDataStream() { log.debugFormat("Creating data stream '%s' for project '%s'...", DATA_STREAM_NAME, PROJECT_KEY); Project project = getProjectManager().getProjectByKey(PROJECT_KEY); if (project == null || getDataManager().getDataStream(PROJECT_KEY, DATA_STREAM_NAME) != null) { return; } DataStream dataStream = new DataStream(); dataStream.setName(DATA_STREAM_NAME); dataStream.setDescription(DATA_STREAM_DESC); dataStream.setProject(project); dataStream.setType(DataStreamType.DATA_MESSAGE); dataStream.setStorageType(StorageType.GENERIC.name()); dataStream.setEnabled(true); getDataManager().addDataStream(dataStream); }
SmartTrackerUtils.java converts the JSON payload received from the micro-controller into GeolocationData object
toGeolocationData methodpublic static GeolocationData toGeolocationData(final DataStreamItem item) { if (item == null || item.getType() != DataStreamType.DATA_MESSAGE) { // Process only data for data stream with type of DATA_MESSAGE return null; } DataMessage dataMessage = (DataMessage) item.getData(); // Transform JSON payload to GeolocationData object GeolocationData geolocationData = JsonUtils.fromJson(dataMessage.getData(), GeolocationData.class); if (geolocationData.getTimestamp() == 0) { geolocationData.setTimestamp(dataMessage.getTimestamp()); } return geolocationData; }
and subsequently adds to data in datastream
public static List<GeolocationData> getData(final List<DataStreamItem> items) { if (items == null || items.isEmpty()) { return null; } List<GeolocationData> data = new ArrayList<GeolocationData>(); for (DataStreamItem item : items) { GeolocationData geolocationData = toGeolocationData(item); if (geolocationData == null) { continue; } data.add(geolocationData); } return data;
Gadget files: GeolocationDataGadget and GeolocationMapGadget perform the following tasks:
a) set the velocity file as gadget templateGeolocation Datapublic static final String TEMPLATE = "/templates/gadget/geolocation-data.vm"; public GeolocationDataGadget() { setTemplate(TEMPLATE); }
Geolocation Mappublic static final String TEMPLATE = "/templates/gadget/geolocation-map.vm"; public GeolocationMapGadget() { setTemplate(TEMPLATE); }
b) Creates a Velocity context, called velocityParams. In the case of "Geolocation data" gadget, the velocity context takes two arguments: a string and list containing GeolocationData data. This is the data that is being read into the table of the Velocity file template.
Geolocation Data velocity context@Override protected Map<String, Object> createVelocityParams(final Map<String, Object> context) { Map<String, Object> velocityParams = new HashMap<String, Object>(); velocityParams.put("data", getData()); return velocityParams; }
Geolocation Map velocity context@Override protected Map<String, Object> createVelocityParams(final Map<String, Object> context) { Map<String, Object> velocityParams = new HashMap<String, Object>(); return velocityParams; }
Step 04: Sensor Data Generator
SensorDataGeneratorJob.java file generates test data, in the absence of real sensor data, to be displayed on the dashboard. It achieves this with the aid of two methods:
Each job has to implement method execute(final Map<String, Object> jobDataMap) that is the entry point of the job and is called every cycle when job runs.
execute(final Map<String, Object> jobDataMap)public void execute(final Map<String, Object> jobDataMap) { getLog().info("Executing sensor data generator..."); if (!jobDataMap.containsKey(ComponentName.DATA_MANAGER)) { getLog().error("Unable to find DataManager component in jobDataMap!"); return; } if (!jobDataMap.containsKey(ComponentName.TEST_DATA_UTILS)) { getLog().error("Unable to find TestDataUtils component in jobDataMap!"); return; } setDataManager((DataManager) jobDataMap.get(ComponentName.DATA_MANAGER)); setTestDataUtils((TestDataUtils) jobDataMap.get(ComponentName.TEST_DATA_UTILS)); generateData(); }
generateData method generates random numbers for the longitude and latitude values. This is done by:
a. obtain a deviceID and datastream using TestDataUtils.java
b. search for data in datastream with deviceID. Use default location if no such data exists
c. create several data values by adding randomly generated numbers between -0.005 to 0.005 to the longitude and latitude of the last/previous location ( this will be the default location for the second data or location)
generateData methodpublic void generateData() { Device device = getTestDataUtils().getDevice(); if (device == null) { return; } DataStream dataStream = getTestDataUtils().getDataStream(); if (dataStream == null) { return; } GeolocationData data = null; if (getDataManager().countStream(dataStream, device) == 0) { // No data in data stream for device data = getTestDataUtils().getDefaultLocation(); } else { List<DataStreamItem> items = getDataManager().loadStream(dataStream, device, 0, 1); if (items == null || items.isEmpty()) { return; } GeolocationData lastData = SmartTrackerUtils.toGeolocationData(items.get(0)); int direction = RandomRange.getRandomInt(0, 1); if (direction == 1) { data = GeolocationData.create(lastData.getLatitude(),lastData.getLongitude() + RandomRange.getRandomDouble(-0.005, 0.005),TimeUtils.getNow().getTime()); } else { data = GeolocationData.create(lastData.getLatitude() + RandomRange.getRandomDouble(-0.005, 0.005),lastData.getLongitude(),TimeUtils.getNow().getTime()); } } String payload = JsonUtils.toJson(data); getLog().debugFormat("Storing generated sensor data... Payload: %s", payload); getDataManager().storeStream(dataStream, device, payload); }
Step 05: UCW Portal Plugin
The SmartTrackerPlugin.java file handles the job scheduling of the SensorDataGeneratorJob.java.
Method scheduleJobs() creates a HashMap pairing a device manager and data manager with device manager and data manager objects respectively
scheduleJobs()protected void scheduleJobs() { // When the sensor data generator is disabled then skip adding job to scheduler. if (!SmartTrackerConfig.isGeneratorEnabled()) { return; } SchedulerService scheduler = ServiceProvider.getInstance().getServiceHost(SchedulerService.class); //Date delay1min = TimeUtils.addMinutes(TimeUtils.getNow(), 1); //long repeatInterval = (600 * 1000) * 2; // 2mins Date delay30sec = TimeUtils.addSeconds(TimeUtils.getNow(), 30); long repeatInterval = 10 * 1000; // 10sec Map<String, Object> jobDataMap = new HashMap<String, Object>(); jobDataMap.put(ComponentName.DATA_MANAGER, ComponentProvider.getInstance().getComponent(DataManager.class)); jobDataMap.put(ComponentName.TEST_DATA_UTILS, ComponentProvider.getInstance().getComponent(TestDataUtils.class)); scheduler.scheduleJob(SensorDataGeneratorJob.class, jobDataMap, delay30sec, repeatInterval); }
Method createTestData() creates a project, device and data stream (if non already exists) using the createData() method from TestDataUtils.java
createTestData()protected void createTestData() { TestDataUtils testDataUtils = ComponentProvider.getInstance().getComponent(TestDataUtils.class); testDataUtils.createData(); }
Result
You can build and run the application and the resulting page should look like this