Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plumb through new "matched bluetooth vehicle" from the phone to the server and to the trip #1062

Closed
shankari opened this issue Mar 22, 2024 · 36 comments

Comments

@shankari
Copy link
Contributor

shankari commented Mar 22, 2024

@JGreenlee @louisg1337 @the-bay-kay for visibility and feedback

@shankari
Copy link
Contributor Author

We need to make some design decisions before implementing this feature.

Decision 1: Do we need to store this as a separate object, or should we treat it as a statemachine transition?
When we use the iOS visit API, we generate VISIT_STARTED and VISIT_ENDED transitions in the current state.
Given that our primary use case here is to use the beacon region enter and exit to detect the start and stop of fleet vehicles, can we just use similar BEACON_STARTED and BEACON_ENDED messages?

I think that is not the best option. Some challenges with that approach:

  • the transition object only has the current state and the new transition. For this use case, it is crucial to also include at least the UUID, since we want to map that to the actual type of car
  • in the long-term, non-fleet case, not every enter/exit will be associated with a state machine transition. For example, a transition between a bicycle with a beacon and a bus with a beacon would be a section change, and would not not necessarily reflect a state change.
  • if the enter/exit is noisy, we may transition already using the geofence

The third is not a great argument, since the visit transitions may also not trigger an FSM transition if the geofence has already triggered. But I think that the second is really compelling. So let's go with a separate object.

Decision 2: Should the separate object be a manual/ or a background/?
Per the data model, I think it should be a background/ object. Although it is not necessarily being continuously sensed now, there is no manual intervention required to generate it. And in the future, I guess we could also periodically monitor the signal strength during the course of the trip to ensure that the enter was not an outlier, or with Bluetooth classic across phones, etc.

Decision 3: What should the data structure look like?
Here's a proposal based on copying the BLE beacon + classic structure over and combining it with vehicle information. @JGreenlee, is this consistent with what you had in mind for the values in the dynamic config?

{
    bluetoothType: CLASSIC | BLE
    bluetoothDetails: BluetoothClassicDetails | BLEDetails
    vehicleType: CAR | BUS | BICYCLE | ...
    vehicleDetails: CarDetails (can potentially include BikeDetails | BusDetails | .... in the future as we expand)
}

BluetoothClassicDetails (from https://developer.apple.com/documentation/externalaccessory/eaaccessory?language=objc)
The original spec also has firmware revision and hardware revision. Not sure how many of those incidental fields are required for our use case. Need to experiment and tweak
{
    name:
    manufacturer:
    modelNumber:
    serialNumber:
    protocolStrings:
    connectionID:
}

BLEDetails (basically, region and event type, I am also supporting range although it is not clear if we will need to use it
{
    uuid:
    major:
    minor:
    proximity: UNKNOWN | IMMEDIATE | NEAR | FAR
    rssi:
    tx:
    accuracy:
}

vehicleDetails
{
    license:
    make:
    model:
    year:
    engine: ICE | HYBRID | BEV | HYDROGENV | BIOV
}

@JGreenlee
Copy link

For decision 3, would this be the structure of the stored background/ object?

I thought this would only include the bluetooth beacon information.

And I thought the vehicle information would be in the dynamic config. Something like this:

{
  ...
  "vehicle_identities": [
    {
      "value": "e_car_1234",
      "bluetooth_ids": ["00:1A:2B:3C:4D:5E"],
      "name": "Leaf A",
      "baseMode":"E_CAR",
      "met_equivalent":"IN_VEHICLE",
      "kgCo2PerKm": 0.08216,
      "vehicle_info": {
        "make": "Nissan",
        "model": "Leaf",
        "year": 2018,
        "license": "AAA 1234"
      }
    },
    {
      "value": "e_car_5678",
      "bluetooth_ids": ["00:1A:2B:3C:4D:5F"],
      "name": "Leaf B",
      "baseMode":"E_CAR",
      "met_equivalent":"IN_VEHICLE",
      "kgCo2PerKm": 0.08216,
      "vehicle_info": {
        "make": "Nissan",
        "model": "Leaf",
        "year": 2020,
        "license": "BBB 5678"
      }
    }
  ],
  ...
}

@JGreenlee
Copy link

If we are going to support BLE and Bluetooth classic (maybe in the future), and these have different properties, what about having 2 different types: background/bluetooth_ble and background/bluetooth_classic

@shankari
Copy link
Contributor Author

shankari commented Mar 28, 2024

For decision 3, would this be the structure of the stored background/ object?
Yes.

I thought this would only include the bluetooth beacon information.
And I thought the vehicle information would be in the dynamic config. Something like this:

That could also work. Per my conversation with @the-bay-kay, since we have to have the full list of UUIDs to "register" them, it would be easy enough to have all the information in the background/ object as well. Is that correct, @louisg1337?

Let us think through the various options and their pros and cons.

Must happen native code must receive a list of BLE beacons so that it can create the appropriate "regions". The list must come from the dynamic config so that it can be different for each fleet.
Question 1: How are the beacons configured:

  1. UI reads dynamic config and passes it in to native code via a function call
    a. UI only passes in BLE UUIDs
    b. UI passes in full vehicle_identifier object
  2. The dynamic config is saved into local storage as usual (unchanged)
    • The native code reads the dynamic config directly from native config and configures the regions

Question 2: What do the background objects look like?

  1. Native code generates background/ objects with only beacon UUID
    • When UI displays "draft" trips, it reads the background/ objects, maps them to the dynamic config, and creates the draft composite trips with the vehicle information filled in
    • On the server, we read the dynamic config match the background/ objects to sections and copy them into the confirmed and composite objects. Note that reading the dynamic config on the server is relatively new.
  2. Native code generates background/ objects with beacon UUID + whole vehicle information
    - When UI displays "draft" trips, it reads the background/ objects and displays them
    - On the server, we match the background/ objects to sections and copy them (no need to read and handle the dynamic config)

For the decisions:
Q1: I think (2) is better because I am not sure how long the regions persist and when they are deleted. On android, for example, IIRC that all location-based geofences are deleted when the app is updated. If we have the native code read the values and register directly, we can do so even if the user hasn't launched the app.
Q2: I don't have a strong preference. (2) is a bit easier to implement, but will take up a little more storage.

@JGreenlee do you have a preference?

@shankari
Copy link
Contributor Author

Also, I had a question around must happen (aka native code must receive a list of BLE beacons so that it can create the appropriate "regions"). From the iOS docs, it looks like we can have multiple beacons with the same UUID and different major and minor values https://developer.apple.com/documentation/corelocation/determining_the_proximity_to_an_ibeacon_device?language=objc

The UUID (universally unique identifier) is a 128-bit value that uniquely identifies your app’s beacons.
The major value is a 16-bit unsigned integer that you use to differentiate groups of beacons with the same UUID.

...

The most important point to me here is the part which says "your app’s beacons". Since the beacons are really intended for use with OpenPATH, maybe we can configure them all with the same UUID, that is hardcoded (or put into an app-level config) into OpenPATH. Then we only need to listen for one UUID, note that the major and minor values can be wildcards in the scan. This might also be good for long-term scalability, so that we don't run into the maximum number of geofences we can create. At least for location-based regions on iOS, this limit seems to be 20.

Regions are shared resources that rely on specific hardware capabilities. To ensure that all apps can participate in region monitoring, Core Location prevents any single app from monitoring more than 20 regions simultaneously. To work around this limitation, monitor only regions that are close to the user’s current location. As the user moves, update the list based on the user’s new location.

I did not find a similar number of beacon-based regions, but the message above says "Core Location" and this SO post claims that the limit also applies to beacon regions
https://stackoverflow.com/questions/8734101/what-is-the-maximum-number-of-regions-that-can-be-monitored-with-startmonitoring#comment52676399_8734329
(20 for location, 20 for beacons)

Further, the example for how to use the beacons
https://developer.apple.com/documentation/corelocation/determining_the_proximity_to_an_ibeacon_device?language=objc
makes it very clear that the UUID and the identifier are expected to be fixed on a per-app basis.

@louisg1337, is it possible for us to experiment with multiple beacons configured with the same UUID? If we leave the major and minor keys as blank while registering the region, does the callback return them? Or do we have to start scanning to get that information? It is fairly clear that the major and minor values will show up if we start scanning.

We can't test this with the current UI since the hardcoded values have the major and minor key defined. Fortunately I made a change last night that allows us to put new entries into the list. Or of course, you can just edit the current app to remove major and minor keys.

We need to answer this first thing because it will affect everything downstream

If we are going to search for a hardcoded (or app-level configured) UUID, then the native code does not need a list of UUIDs from the dynamic config, which means that we have a strong vote for the second option for Question 2.

@JGreenlee
Copy link

@JGreenlee do you have a preference?

After seeing the approaches for Q2 laid out, I don't have a strong preference either.

One reason I might express a slight preference for (2) is that it allows the native code to be agnostic of vehicles. It would only have to worry about beacons, so the native code would be a bit simpler and more generic.

On the server, we read the dynamic config match the background/ objects to sections and copy them into the confirmed and composite objects. Note that reading the dynamic config on the server is relatively new.

Would we necessarily have to do this? What if the trip object just had value?. This would be similar to what we do for mode+purpose labels; the trip object just has the string value for the label and the UI looks up baseMode, met, and co2PerKm from the label_options config.

@shankari
Copy link
Contributor Author

Added two, about to add third entry After adding the third entry
simulator_screenshot_A54968C6-320E-4943-828F-E75724152532 simulator_screenshot_46DCCD1F-D3A9-49D9-98FF-DB0055DA3588

shankari added a commit to shankari/e-mission-phone that referenced this issue Mar 29, 2024
- Creates a new set of fields at the botton of the page for the UUID,
  identifier, major and minor fields
- Adds a new "Add" button
- When the "Add" button is pressed, we add a new UUID entry to the default list
- After the new entry is added, we clear out the old values to prepare for a
  new entry

Testing done:
Added three new entries. They were displayed in the list
e-mission/e-mission-docs#1062 (comment)
@shankari
Copy link
Contributor Author

shankari commented Mar 29, 2024

The plugin that we use requires both an identifier and a UUID. This is because the call to create a beacon used to require both a UUID and an identifier.
https://developer.apple.com/documentation/corelocation/clbeaconregion/3183025-initwithuuid?language=objc

However, that call is now deprecated. We should not be using CLBeaconRegion anymore, we should be using CLBeaconIdentityCondition. This new class only requires a UUID. It has no reference to an identifier.

Even in the old days, the identifier was static and hardcoded

        // Match all beacons with the specified UUID
        let proximityUUID = UUID(uuidString: 
               "39ED98FF-2900-441A-802F-9C398FC199D2")
        let beaconID = "com.example.myBeaconRegion"
            
        // Create the region and begin monitoring it.
        let region = CLBeaconRegion(proximityUUID: proximityUUID!,
               identifier: beaconID)

Note also that the UUID in the new method is of type NSUUID, which is a 128-bit value created to conform to RFC 4122. It cannot be an arbitrary string. So from the example above, we must use "39ED98FF..." and we cannot use "com.example.myBeaconRegion"

Since the identifier is still required in the plugin, I am going to use a hardcoded value for it, edu.berkeley.eecs.emission to be consistent with the example.

Note also that the way to avoid specifying the major and minor values is to use undefined
https://github.com/louisg1337/cordova-plugin-ibeacon?tab=readme-ov-file#specify-wildcard-uuid-android-only

@shankari
Copy link
Contributor Author

shankari commented Mar 29, 2024

Optional major and minor Refactored display to highlight UUID and major/minor
before_ui_changes simulator_screenshot_F50E191C-D5D8-46C9-8702-E8EFC985784A

shankari added a commit to shankari/e-mission-phone that referenced this issue Mar 29, 2024
@shankari
Copy link
Contributor Author

I am working on a PR to simulate BLE scans e-mission/e-mission-phone#1140

shankari added a commit to shankari/e-mission-phone that referenced this issue Mar 29, 2024
Instead of the statically coded name/identifier
This involved changing the text size so that we could see the UUID
I wonder if we should make the UUID the subtitle and the major:minor the title

Testing done:
e-mission/e-mission-docs#1062 (comment)
@shankari
Copy link
Contributor Author

It's going to be a bit tricky to know how to display the matches to see if we have minor and major values without running the code, since there is not much documentation (from the iOS level up) around what this may look like. Let's just stringify the result and display it for now.

@louisg1337
Copy link

@louisg1337, is it possible for us to experiment with multiple beacons configured with the same UUID? If we leave the major and minor keys as blank while registering the region, does the callback return them? Or do we have to start scanning to get that information? It is fairly clear that the major and minor values will show up if we start scanning.

I'm not quite sure, but I can definitely test this out especailly since I have 2 beacons now. I also really do like this idea of using one UUID and variable major/minor values a lot better than creating X amount of regions. The latter seems a bit wasteful resource wise, and it also complicates things quite a bit.

Jack also made a great point here how we may need to change up our implementation a bit, but it doesn't seem like too crazy of a pivot. I'll give all of this a go and update accordingly as I get some answers!

@louisg1337
Copy link

Quick follow up to my comment above, I did two tests.

  1. Provided only the UUID and attempted to monitor for beacons entering our region.
    When we detect a beacon in the region, we get a callback with int state, Region region as our parameters. I figured maybe the region could maybe have the major and minor values of the beacons it detected in it. I set my accent beacon to have the same UUID as my Blue Charm one, and when I monitored the region it detected two beacons, but whenever I tried to return the major and minor values using region.toString() (API docs, it shows up as null. Jack hit the nail on the head with that one, the region variable is just the region that we define.

  2. I then tried the ranging functionality as Jack suggested instead while providing only the UUID.
    This one seems like it works! Upond ranging we get Collection<Beacons> beacons as a parameter, and each value in beacons carries its major and minor value, despite us not providing it in the region.

With that in mind, looks like a fixed UUID and variable major/minor values could be the way to go!

@shankari
Copy link
Contributor Author

Just started scan fake callback for existing beacon Add new beacon with no major or minor Fake callback for new beacon
simulator_screenshot_2237CD71-BE67-4243-8D37-63D415C9BDC8 simulator_screenshot_87E3A742-A796-4571-BF9E-19B9732A2AAC simulator_screenshot_C64214D5-9BD9-4368-AC6C-85B84E075D4A simulator_screenshot_C2BC9C98-60BD-4D50-935C-1823962F03C6

shankari added a commit to shankari/e-mission-phone that referenced this issue Mar 29, 2024
We don't know what the result will look like when there are multiple beacons with the same UUID, so we just stringify and display the result.
We cannot test this without having an actual BLE beacon, so I have added a "fake callback button to simulate a callback"

Changes:
- stringify the result in `didDetermineStateForRegion` and pass it in to
  `setRangeStatus`
- Change the BluetoothCard to display the `device.result` as Card.Content
- Add a new button to fake a callback for a device by getting the delegate and
  invoking the `didDetermineStateForRegion` method on it with the device
- Remove the existing result in the device before invoking the callback to
  avoid nested results

Testing done:
e-mission/e-mission-docs#1062 (comment)
@shankari
Copy link
Contributor Author

Given these results, I think that the answers to the questions above are:
Must happen native code must receive a list of BLE beacons so that it can create the appropriate "regions". The list must come from the dynamic config so that it can be different for each fleet.

We can have a single hardcoded value for e-mission/OpenPATH

Question 1: How are the beacons configured:
This is N/A since the beacon UUID will be hardcoded

Question 2: What do the background objects look like?

  • Given that the UUID is hardcoded, the native code generates background/ objects with beacon UUID and major + minor values ONLY
  • When UI displays "draft" trips, it reads the background/ objects, maps them to the dynamic config, and creates the draft composite trips with the vehicle information filled in
  • On the server, we read the dynamic config match the background/ objects to sections and copy them into the confirmed and composite objects. Note that reading the dynamic config on the server is relatively new.
    Native code generates background/ objects with beacon UUID + whole vehicle information

@JGreenlee you also need to change the dynamic config to have major/minor values instead of the bluetooth_ids, but I don't think we will need to change a lot else. You might also want to think about how we represent the info for other vehicles (e.g. buses, trains and bikes). Trains will not have a license number and bikes may not. And buses will have a route, which is likely very important information. We don't need to fully design these now, but we need to make sure that we can support other types of vehicle information in the future without having to redesign everything. Maybe a vehicle_type to go with the vehicle_info?

@JGreenlee
Copy link

JGreenlee commented Mar 29, 2024

Here is an updated config proposal.

I changed bluetooth_ids to bluetooth_major_minor. It is in the format major:minor, each as 4 characters hexadecimal. It's probably good practice for each deployment to have a designated major that is used for all beacons in that deployment. in this example I went with dfc0

I added type and engine in vehicle_info.

{
  ...
  "vehicle_identities": [
    {
      "value": "e_car_1234",
      "bluetooth_major_minor": ["dfc0:1234"],
      "name": "Leaf A",
      "baseMode":"E_CAR",
      "met_equivalent":"IN_VEHICLE",
      "kgCo2PerKm": 0.08216,
      "vehicle_info": {
        "type": "car",
        "make": "Nissan",
        "model": "Leaf",
        "engine": "BEV",
        "year": 2018,
        "license": "AAA 1234"
      }
    },
    {
      "value": "e_car_5678",
      "bluetooth_major_minor": ["dfc0:5678"],
      "name": "Leaf B",
      "baseMode":"E_CAR",
      "met_equivalent":"IN_VEHICLE",
      "kgCo2PerKm": 0.08216,
      "vehicle_info": {
        "type": "car",
        "make": "Nissan",
        "model": "Leaf",
        "engine": "BEV",
        "year": 2020,
        "license": "BBB 5678"
      }
    }
  ],
  ...
}

@shankari
Copy link
Contributor Author

in this example I went with dfc0

It is so cool that this deployment name lent itself so well to hex 😄
This looks good to me. I don't know if we will need the engine type, but I suspect it will be easier to remove than to add.

@shankari
Copy link
Contributor Author

shankari commented Mar 30, 2024

To pick this up again, with the proposed changes, the new background object will have a key of background/bluetooth_ble and a value of

{
    "ts": // always present
    "eventType": REGION_ENTER | REGION_EXIT | RANGE_UPDATE
     "uuid": // will always be present
     "major": // for our use case, missing for REGION_ENTER or REGION_EXIT
     "minor": // for our use case, missing for REGION_ENTER or REGION_EXIT
     "proximity": // only available for RANGE_UPDATE
     "rssi": // only available for RANGE_UPDATE
     "accuracy": // only available for RANGE_UPDATE
}

While creating raw sections, we will use the values for section segmentation. Exact algorithm TBD, but at a high level, if we see a change in BLE beacons, it is likely a change from one vehicle to another or from a vehicle to walking.

Design decision: should we fill in the vehicle values right here, or fill in the major:minor key here and fill in the full value for the cleaned section?
It doesn't make a huge difference. Let's start with filling in the major:minor key in the segment_current_sections stage. Note that these can be filled in directly from the background object and do not need to read the dynamic config.
Then, in the clean_and_resample stage, we can fill in the values from the dynamic config by simply copying the matching value in. This will allow the UI to read the baseMode, met_equivalent and kgCo2PerKm as needed.

@JGreenlee As an aside, I think we should standardize on camelCase or under_score_case. Right now, baseMode and kgCo2PerKm are in camelCase but met_equivalent is in under_score_case.

I think we should also have a summary of the values in the confirmed and composite trips, similar to the current inferred_section_summary and cleaned_section_summary to make it easier to find the primary mode instead of having to iterate over the sections. This could be called ble_sensed_section_summary and the keys would be the baseMode @JGreenlee is this consistent with what you had in mind?

I think that is about it. I am now starting with adding the new data type and plumbing it through to the server. Since we do not generate these objects in the native code yet, I will simulate them using the UI. After that is done, I just need to figure out an initial algorithm for segment_current_sections and we are done!

There will be a ton of tuning that will need to happen with real world testing next month to address all the weird corner cases that will undoubtedly show up.

@shankari
Copy link
Contributor Author

shankari commented Mar 30, 2024

Design decision These feels foundational enough that I am going to create a new data type instead of storing it as JSON. This will allow us to read the fields from the object directly instead of parsing through a JSON object and dealing with exceptions. We will use the object in the FSM, so this seems fairly important.
https://github.com/e-mission/e-mission-docs/blob/master/docs/dev/back/adding_a_new_data_type.md

@JGreenlee
Copy link

@JGreenlee As an aside, I think we should standardize on camelCase or under_score_case. Right now, baseMode and kgCo2PerKm are in camelCase but met_equivalent is in under_score_case

Ah, I did it that way to match the fields in the label options spec.

https://github.com/e-mission/nrel-openpath-deploy-configs/blob/main/label_options/example-study-label-options.json
It has the same inconsistency where met_equivalent is "snake_case" while baseMode and kgCo2PerKm are "camelCase".

That inconsistency has been around for a while, even before label options were customizable.

There are also more cases of this elsewhere in the dynamic config, and some places where a third one, "kebab-case" is used.

I don't know an easy way to fix it. I think we would have to implement a temporary backwards compat patch on the phone to convert local configs to one standard case, regardless of what case the source used. Then we could update all the configs on GitHub to use the standard case. And remove the temporary patch after we're sure everyone has gotten it.

For now, I think we should just decide what 'case' we want to end up with and use that for new fields going forward.
I'd vote for "snake_case" since the e-mission platform is primarily a Python ecosystem.

So for the vehicle_identities spec we would use base_mode and kg_co2_per_km.

@shankari
Copy link
Contributor Author

Ah, I did it that way to match the fields in the label options spec.

Ah, that is a good enough reason then. Note that the label options are also read in python now, to support custom labels in the public dashboard.

Let's keep it unchanged for historical reasons and deal with unifying it later. Much later. When the rest of the code is so polished that we have nothing else to polish.

shankari added a commit to shankari/e-mission-phone that referenced this issue Apr 1, 2024
- Move the major and minor boxes to the same row to take up less space
- Split the fake callbacks into enter, exit and range
- generalize `fakeMonitorCallback` to work for both enter and exit
- do not call rangeCallback directly from monitor callback since there
  are now separate buttons
- for the range callback, use the device major and minor values if they
  exist
- for the range callback, reset monitor and range results to avoid
  duplicates

This is the first step for testing the data models defined in:
e-mission/e-mission-docs#1062 (comment)
@JGreenlee
Copy link

JGreenlee commented Apr 4, 2024

I think there should be an additional field in the config spec to differentiate programs that will only track Bluetooth-sensed trips vs. programs that will still track all trips and just include Bluetooth sensing as a supplement to the existing tracking.

The "fleet" version of OpenPATH is first scenario, but I think we want to leave the door open for the second scenario too.

{
  ...
  "tracking": {
    "bluetooth_only": true
  },
  ...
}

In total,

{
  ...
  "vehicle_identities": [
    {
      "value": "e_car_1234",
      "bluetooth_major_minor": ["dfc0:1234"],
      "name": "Leaf A",
      "baseMode":"E_CAR",
      "met_equivalent":"IN_VEHICLE",
      "kgCo2PerKm": 0.08216,
      "vehicle_info": {
        "type": "car",
        "make": "Nissan",
        "model": "Leaf",
        "engine": "BEV",
        "year": 2018,
        "license": "AAA 1234"
      }
    },
    {
      "value": "e_car_5678",
      "bluetooth_major_minor": ["dfc0:5678"],
      "name": "Leaf B",
      "baseMode":"E_CAR",
      "met_equivalent":"IN_VEHICLE",
      "kgCo2PerKm": 0.08216,
      "vehicle_info": {
        "type": "car",
        "make": "Nissan",
        "model": "Leaf",
        "engine": "BEV",
        "year": 2020,
        "license": "BBB 5678"
      }
    }
  ],
  "tracking": {
    "bluetooth_only": true
  },
  ...
}

@shankari
Copy link
Contributor Author

I have now created separate buttons for simulating bluetooth transitions. I now have a quick set of design decisions to make. How do I generate and save the values?

There are a couple of options:

  1. when the buttons are clicked in the card, directly save the entry and generate the transition. This means that the callbacks in the bluetooth scan page will only work with real beacons. It also means that the real beacon callbacks will only be displayed in the UI and not stored.
  2. when the buttons are clicked in the card, continue calling the methods in the javascript delegate. This method will now save the entry and generate transitions. This will ensure that when we have real beacon callbacks, they will also be stored correctly in the database.

I am currently leaning towards (2) because:

  • it is closer to the final native code functionality (entries saved in the DB)
  • it allows us to test reading objects in addition to writing

@shankari
Copy link
Contributor Author

So far, I have primarily been testing this on iOS. I tested on android before pushing the changes to simulate the data storage and transitions, and found a few differences. I have accounted for some of the differences in the code, but we may need to either unify the plugin or do some additional cleanup later.

  • android generates an exit call when we start scanning, if the beacon is not within range
  • android continues to make callbacks from the native code to the range delegate even if there are no beacons (with a blank beacon array)
    • this can result in issues with the transition check - after we generate the transition for the first time, the blank beacon callbacks will continue to generate transitions forever. This is currently fixed by ignoring those callbacks, but we will need to stop ranging or change the configuration in the future.

shankari added a commit to shankari/e-mission-phone that referenced this issue Apr 12, 2024
Consistent with the plans in:
e-mission/e-mission-docs#1062 (comment)

add support for storing simulated BLE responses and transitions in the
usercache

Concretely:
- when there is a monitor entry event, we store a `bluetooth_ble` entry with
  `REGION_ENTER`
- when there is a range event, we store a `bluetooth_ble` entry with
  `RANGE_UPDATE`. if we receive three consecutive range updates within 5 minutes, we generate a `BLE_BEACON_FOUND` transition
- when there is a monitor exit event, we store a `bluetooth_ble` entry with `REGION_EXIT` and generate a `BLE_BEACON_LOST` transition

Note that this has some fixes found while testing android:
e-mission/e-mission-docs#1062 (comment)

In addition, the callbacks exposed that the format of the range callback
that we were using earlier was incorrect. The `beacons` array is at the
same level as the `region`

Testing done on both iOS and android:
- Scan for BLE beacons
- Region enter
- Range (3-4 times); see `ble_found` transition
- Region exit; see `ble_lost` transition
@JGreenlee
Copy link

@shankari
In the background/bluetooth_ble object, how are major and minor represented?

Are they i) decimal integers or ii) strings of hexadecimal digits ?

@shankari
Copy link
Contributor Author

shankari commented Apr 12, 2024

@JGreenlee decimal integers

The raw iOS values return NSNumber, which is a char, short int, int, long int, long long int, float, or double or as a BOOL
I am pretty sure that alt-beacon will be consistent with iOS because that's what they focus on after all

JGreenlee added a commit to JGreenlee/e-mission-phone that referenced this issue Apr 12, 2024
@shankari
Copy link
Contributor Author

Trying out the native wrapper classes with a hack

    if ([transition isEqualToString:CFCTransitionBeaconFound]) {
        BluetoothBLE* enterWrapper = [BluetoothBLE new];
        enterWrapper.eventType = @"REGION_ENTER";
        enterWrapper.ts = [BuiltinUserCache getCurrentTimeSecs];
        [[BuiltinUserCache database] putSensorData:@"key.usercache.bluetooth_ble" value:enterWrapper];

        for (int i = 0; i < 5; i++) {
            BluetoothBLE* rangeWrapper = [BluetoothBLE new];
            rangeWrapper.eventType = @"RANGE_UPDATE";
            rangeWrapper.ts = [BuiltinUserCache getCurrentTimeSecs];
            [[BuiltinUserCache database] putSensorData:@"key.usercache.bluetooth_ble" value:rangeWrapper];
        }
        NSArray* currentEntries = [[BuiltinUserCache database] getLastSensorData:@"key.usercache.bluetooth_ble" nEntries:2 wrapperClass:StatsEvent.class];
        NSLog(@"[BLE native] Found %lu entries", currentEntries.count);
        // NSLog(@"[BLE native] First entry is %@, last entry is %@", currentEntries[0], currentEntries[currentEntries.count-1]);
        NSLog(@"[BLE native] while handling transition %@", transition);
     }

and am running into some very weird behavior.

The first save (of enterWrapper) succeeds, but the save of the rangeWrapper and the reads generate a bad access exception. Not immediately, but when the related object is garbage collected. So for rangeWrapper, the crash happens when we exit the first iteration of the for loop. For the read, the error happens when we exit handleAction, unless we comment in the line to access the first element, in which case it crashes then

@shankari
Copy link
Contributor Author

Removing the for loop, we crash while exiting the if check
Adding two more unrolled calls, can confirm that we go through them and then crash while exiting the if check

        BluetoothBLE* rangeWrapper1 = [BluetoothBLE new];
        rangeWrapper1.eventType = @"RANGE_UPDATE";
        rangeWrapper1.ts = [BuiltinUserCache getCurrentTimeSecs];
        [[BuiltinUserCache database] putSensorData:@"key.usercache.bluetooth_ble" value:rangeWrapper1];

        BluetoothBLE* rangeWrapper2 = [BluetoothBLE new];
        rangeWrapper2.eventType = @"RANGE_UPDATE";
        rangeWrapper2.ts = [BuiltinUserCache getCurrentTimeSecs];
        [[BuiltinUserCache database] putSensorData:@"key.usercache.bluetooth_ble" value:rangeWrapper2];

I bet this is because of the inheritance from CLBeacon. It is a nice trick, but several of the properties are read-only, so we can't set them, and I bet they are uninitialized and crashing when we free the related memory. I vaguely remember something like this happening with the location wrapper; that is why SimpleLocation does not inherit from CLLocation

@shankari
Copy link
Contributor Author

shankari commented Apr 13, 2024

Confirmed that just after changing @interface BluetoothBLE : CLBeacon to @interface BluetoothBLE : NSObject all the access errors went away. Restoring the code and testing again...

Also need to convert the UUID to a string instead of NSUUID so that it can be serialized.
And change the field name to uuid instead of UUID

shankari added a commit to shankari/e-mission-data-collection that referenced this issue Apr 13, 2024
…etooth code

The data format is very similar to the beacon object in iOS.

We consciously patterned the object to be similar to `CLBeacon` since the
alt-beacon library for android is an effort to be compatible with iOS to the
extent possible.

Our only changes are to change the fields in iOS to be more easily serializable
e-mission/e-mission-docs#1062 (comment)
e-mission/e-mission-docs#1062 (comment)

Testing done:
After adding a hack to read/write messages on every ble_found transition,
e-mission/e-mission-phone#1144 (comment)
was able to verify that both read and write worked properly

Android:

```
sqlite> select * from userCache where type == "sensor-data";
1713026512.887||America/Los_Angeles|sensor-data|background/battery||{"android_health":"GOOD","android_plugged":"UNKNOWN","android_technology":"Li-ion","android_temperature":250,"android_voltage":250,"battery_level_pct":100.0,"battery_status":4,"ts":1.713026512887E9}
```

```
04-13 09:45:21.365 18332 18332 I TripDiaryStateMachineRcvr: TripDiaryStateMachineReciever onReceive(android.app.ReceiverRestrictedContext@9e3bd75, Intent { act=local.transition.ble_beacon_found flg=0x10 pkg=edu.berkeley.eecs.emission cmp=edu.berkeley.eecs.emission/.cordova.tracker.location.TripDiaryStateMachineReceiver }) called
04-13 09:45:21.385 18332 18332 I TripDiaryStateMachineService: Service created. Initializing one-time variables!
04-13 09:45:21.396 18332 18332 D TripDiaryStateMachineService: service started with flags = 0 startId = 1 action = local.transition.ble_beacon_found
04-13 09:45:21.404 18332 18332 D TripDiaryStateMachineService: after reading from the prefs, the current state is local.state.waiting_for_trip_start
04-13 09:45:21.424 18332 18332 D BuiltinUserCache: Added value for key statemachine/transition at time 1.713026721405E9
04-13 09:45:21.432 18332 18332 D TripDiaryStateMachineService: handleAction(local.state.waiting_for_trip_start, local.transition.ble_beacon_found) called
04-13 09:45:21.448 18332 18332 D BuiltinUserCache: Added value for key background/battery at time 1.713026721433E9
04-13 09:45:21.464 18332 18332 D BuiltinUserCache: Added value for key background/bluetooth_ble at time 1.713026721449E9
04-13 09:45:21.479 18332 18332 D BuiltinUserCache: Added value for key background/bluetooth_ble at time 1.713026721465E9
04-13 09:45:21.493 18332 18332 D BuiltinUserCache: Added value for key background/bluetooth_ble at time 1.71302672148E9
04-13 09:45:21.520 18332 18332 D BuiltinUserCache: Added value for key background/bluetooth_ble at time 1.713026721493E9
04-13 09:45:21.532 18332 18332 D BuiltinUserCache: Added value for key background/bluetooth_ble at time 1.71302672152E9
04-13 09:45:21.546 18332 18332 D BuiltinUserCache: Added value for key background/bluetooth_ble at time 1.713026721533E9
04-13 09:45:21.548 18332 18332 I System.out: [BLE native] Found 5 entries
04-13 09:45:21.548 18332 18332 I System.out: [BLE native] First entry is edu.berkeley.eecs.emission.cordova.tracker.wrapper.BluetoothBLE@afc8f12 last entry is edu.berkeley.eecs.emission.cordova.tracker.wrapper.BluetoothBLE@b0c96e3
04-13 09:45:21.548 18332 18332 I System.out: [BLE native] while handling transition local.transition.ble_beacon_found
```

```
sqlite> select * from userCache where type == "sensor-data";
1713026512.887||America/Los_Angeles|sensor-data|background/battery||{"android_health":"GOOD","android_plugged":"UNKNOWN","android_technology":"Li-ion","android_temperature":250,"android_voltage":250,"battery_level_pct":100.0,"battery_status":4,"ts":1.713026512887E9}
1713026710.433||America/Los_Angeles|sensor-data|background/battery||{"android_health":"GOOD","android_plugged":"UNKNOWN","android_technology":"Li-ion","android_temperature":250,"android_voltage":250,"battery_level_pct":100.0,"battery_status":4,"ts":1.713026710432E9}
1713026712.809||America/Los_Angeles|sensor-data|background/battery||{"android_health":"GOOD","android_plugged":"UNKNOWN","android_technology":"Li-ion","android_temperature":250,"android_voltage":250,"battery_level_pct":100.0,"battery_status":4,"ts":1.713026712808E9}
1713026721.433||America/Los_Angeles|sensor-data|background/battery||{"android_health":"GOOD","android_plugged":"UNKNOWN","android_technology":"Li-ion","android_temperature":250,"android_voltage":250,"battery_level_pct":100.0,"battery_status":4,"ts":1.713026721433E9}
1713026721.449||America/Los_Angeles|sensor-data|background/bluetooth_ble||{"accuracy":100.0,"eventType":"REGION_ENTER","major":4538,"minor":1256,"proximity":"ProximityNear","rssi":10,"ts":1.713026721E9,"uuid":"0e27c613-ff78-486c-b523-950776777d16"}
1713026721.465||America/Los_Angeles|sensor-data|background/bluetooth_ble||{"accuracy":100.0,"eventType":"RANGE_UPDATE","major":4538,"minor":1256,"proximity":"ProximityNear","rssi":10,"ts":1.713026721E9,"uuid":"d8635e19-4133-493c-b2fd-c04fdd920c6a"}
1713026721.48||America/Los_Angeles|sensor-data|background/bluetooth_ble||{"accuracy":100.0,"eventType":"RANGE_UPDATE","major":4538,"minor":1256,"proximity":"ProximityNear","rssi":10,"ts":1.713026721E9,"uuid":"5efe9e8b-ce30-4417-80e3-79ce9ff53ca9"}
1713026721.493||America/Los_Angeles|sensor-data|background/bluetooth_ble||{"accuracy":100.0,"eventType":"RANGE_UPDATE","major":4538,"minor":1256,"proximity":"ProximityNear","rssi":10,"ts":1.713026721E9,"uuid":"d9283862-2926-4a0c-b377-0e80718e6525"}
1713026721.52||America/Los_Angeles|sensor-data|background/bluetooth_ble||{"accuracy":100.0,"eventType":"RANGE_UPDATE","major":4538,"minor":1256,"proximity":"ProximityNear","rssi":10,"ts":1.713026721E9,"uuid":"cef2f3df-30c5-49d5-8707-2f234530ef18"}
1713026721.533||America/Los_Angeles|sensor-data|background/bluetooth_ble||{"accuracy":100.0,"eventType":"RANGE_UPDATE","major":4538,"minor":1256,"proximity":"ProximityNear","rssi":10,"ts":1.713026721E9,"uuid":"326876e5-9869-407d-976c-2de438bec330"}
1713026800.123||America/Los_Angeles|sensor-data|background/battery||{"android_health":"GOOD","android_plugged":"UNKNOWN","android_technology":"Li-ion","android_temperature":250,"android_voltage":250,"battery_level_pct":100.0,"battery_status":4,"ts":1.713026800121E9}
1713026800.788||America/Los_Angeles|sensor-data|background/battery||{"android_health":"GOOD","android_plugged":"UNKNOWN","android_technology":"Li-ion","android_temperature":250,"android_voltage":250,"battery_level_pct":100.0,"battery_status":4,"ts":1.713026800788E9}
```

iOS

```
$ sqlite3 /Users/kshankar/Library/Developer/CoreSimulator/Devices/42B6F8A1-A925-4CAD-A48A-4835D69595ED/data/Containers/Data/Application/13D8F42C-921C-4754-9420-89C4350CE63F/Library/LocalDatabase/userCacheDB
SQLite version 3.39.5 2022-10-14 20:58:05
Enter ".help" for usage hints.
sqlite> select * from userCache where type == "sensor-data";
sqlite>
```

```
2024-04-13 09:34:56.566596-0700 emission[17352:9891606] In TripDiaryStateMachine, received transition T_BLE_BEACON_FOUND in state STATE_ONGOING_TRIP
2024-04-13 09:34:56.566796-0700 emission[17352:9891606] DEBUG: In TripDiaryStateMachine, received transition T_BLE_BEACON_FOUND in state STATE_ONGOING_TRIP
2024-04-13 09:34:56.568786-0700 emission[17352:9891606] data has 92 bytes, str has size 92
2024-04-13 09:34:56.570995-0700 emission[17352:9891606] data has 69 bytes, str has size 69
2024-04-13 09:35:12.781616-0700 emission[17352:9891606] data has 177 bytes, str has size 177
2024-04-13 09:35:12.784409-0700 emission[17352:9891606] data has 176 bytes, str has size 176
2024-04-13 09:35:12.786659-0700 emission[17352:9891606] data has 177 bytes, str has size 177
2024-04-13 09:35:12.789160-0700 emission[17352:9891606] data has 177 bytes, str has size 177
2024-04-13 09:35:12.791407-0700 emission[17352:9891606] data has 176 bytes, str has size 176
2024-04-13 09:35:12.793945-0700 emission[17352:9891606] data has 177 bytes, str has size 177
2024-04-13 09:35:17.900501-0700 emission[17352:9891606] [BLE native] Found 5 entries
2024-04-13 09:35:17.900666-0700 emission[17352:9891606] [BLE native] First entry is <BluetoothBLE: 0x6000010b8c80>, last entry is <BluetoothBLE: 0x6000010ba3f0>
2024-04-13 09:35:17.900773-0700 emission[17352:9891606] [BLE native] while handling transition T_BLE_BEACON_FOUND
2024-04-13 09:35:20.434450-0700 emission[17352:9891606] Got unexpected transition T_BLE_BEACON_FOUND in state STATE_ONGOING_TRIP, ignoring
```

```
sqlite> select * from userCache where type == "sensor-data";
1713026112.78185||America/Los_Angeles|sensor-data|background/bluetooth_ble||{"major":4538,"minor":1256,"rssi":10,"eventType":"REGION_ENTER","ts":1713026111.2200561,"uuid":"35355A74-E587-4F09-B114-D6718E925DC0","proximity":"ProximityNear","accuracy":100}
1713026112.78457||America/Los_Angeles|sensor-data|background/bluetooth_ble||{"major":4538,"minor":1256,"rssi":10,"eventType":"RANGE_UPDATE","ts":1713026112.784344,"uuid":"3448C47C-BE3B-40B8-A11F-4BD3CC1C2F63","proximity":"ProximityNear","accuracy":100}
1713026112.78698||America/Los_Angeles|sensor-data|background/bluetooth_ble||{"major":4538,"minor":1256,"rssi":10,"eventType":"RANGE_UPDATE","ts":1713026112.7865958,"uuid":"A2778875-CDF8-4AA8-A2E1-0639C9B4B159","proximity":"ProximityNear","accuracy":100}
1713026112.78947||America/Los_Angeles|sensor-data|background/bluetooth_ble||{"major":4538,"minor":1256,"rssi":10,"eventType":"RANGE_UPDATE","ts":1713026112.7891002,"uuid":"EE5129DA-6D64-4B25-BA68-3FB3D0FE6BD6","proximity":"ProximityNear","accuracy":100}
1713026112.79173||America/Los_Angeles|sensor-data|background/bluetooth_ble||{"major":4538,"minor":1256,"rssi":10,"eventType":"RANGE_UPDATE","ts":1713026112.791352,"uuid":"5B6A86F3-BE4D-498E-A3FD-0CAB6039914E","proximity":"ProximityNear","accuracy":100}
1713026112.7941||America/Los_Angeles|sensor-data|background/bluetooth_ble||{"major":4538,"minor":1256,"rssi":10,"eventType":"RANGE_UPDATE","ts":1713026112.7938981,"uuid":"71E73F8E-4138-4346-8E5F-D05E045A6D69","proximity":"ProximityNear","accuracy":100}
```
shankari added a commit to shankari/e-mission-server that referenced this issue Apr 14, 2024
These are the server side changes related to
e-mission/e-mission-docs#1062

The changes are fairly straightforward, and consistent with
https://github.com/e-mission/e-mission-docs/blob/2665b39e1335ea04896b6944a4a065e7887b6cdc/docs/dev/back/adding_a_new_data_type.md?plain=1#L4

Concretely:
- we add a new `bluetoothble` wrapper
- we add references to it to `entry.py` and `builtin_timeseries.py`
- we add formatters for both android and iOS

A new wrinkle this time is that we are modifying the FSM, so there are also
new transitions. Those needed to be added to the enums in the transition
wrapper, and to the maps in the formatters so that the enums could be created
properly.

Bonus fix: check for the `None` transition properly on android and iOS to
avoid spurious errors

```
>>> broken_transition_example = {'_id': ObjectId('661b129fc271a44bb612b464'), 'metadata': {'time_zone': 'America/Los_Angeles', 'plugin': 'none', 'write_ts': 1713050268.574551, 'platform': 'ios', 'read_ts': 0, 'key': 'statemachine/transition', 'type': 'message'}, 'user_id': UUID('f1aaae55-fc42-4527-bf7f-33f84d7c8c2f'), 'data': {'currState': 'STATE_ONGOING_TRIP', 'transition': None, 'ts': 1713050268.574418}}
>>> broken_transition_example_entry = ad.AttrDict(broken_transition_example)
>>> enufit.format(broken_transition_example_entry)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/kshankar/Desktop/data/e-mission/gis_branch_tests/emission/net/usercache/formatters/ios/transition.py", line 64, in format
    data.transition = transition_map[entry.data.transition].value
KeyError: None

----- fixed code ------

>>> importlib.reload(enufit)
<module 'emission.net.usercache.formatters.ios.transition' from '/Users/kshankar/Desktop/data/e-mission/gis_branch_tests/emission/net/usercache/formatters/ios/transition.py'>
>>> enufit.format(broken_transition_example_entry)
AttrDict({'_id': ObjectId('661b129fc271a44bb612b464'), 'user_id': UUID('f1aaae55-fc42-4527-bf7f-33f84d7c8c2f'), 'metadata': AttrDict({'time_zone': 'America/Los_Angeles', 'plugin': 'none', 'write_ts': 1713050268.574551, 'platform': 'ios', 'read_ts': 0, 'key': 'statemachine/transition', 'type': 'message', 'write_local_dt': LocalDate({'year': 2024, 'month': 4, 'day': 13, 'hour': 16, 'minute': 17, 'second': 48, 'weekday': 5, 'timezone': 'America/Los_Angeles'}), 'write_fmt_time': '2024-04-13T16:17:48.574551-07:00'}), 'data': AttrDict({'curr_state': 2, 'transition': None, 'ts': 1713050268.574551, 'local_dt': AttrDict({'year': 2024, 'month': 4, 'day': 13, 'hour': 16, 'minute': 17, 'second': 48, 'weekday': 5, 'timezone': 'America/Los_Angeles'}), 'fmt_time': '2024-04-13T16:17:48.574551-07:00'})})
```

Testing done:
Used the corresponding changes in
e-mission/e-mission-phone#1144
to simulate BLE as follows:
- Region exit
- A few range updates until the `ble_beacon_found` transition was generated
- Turned on location mocking from the android and iOS simulators, and manually
  generated the start trip transition on android
- Clicked "range update" at random times during the simulated trip
- BLE beacon lost from the UI
- Turn off location mocking
- Force end trip transition

Testing Results:

Android:

```
START 2024-04-13 17:36:02.096342 POST /usercache/put
END 2024-04-13 17:36:02.313529 POST /usercache/put ebc13f1b-671b-4094-bce6-fed342da7e9c 0.2171182632446289
START 2024-04-13 17:36:02.583812 POST /usercache/get
END 2024-04-13 17:36:02.591868 POST /usercache/get ebc13f1b-671b-4094-bce6-fed342da7e9c 0.007989168167114258
```

```
>>> edb.get_usercache_db().count_documents({"metadata.key": "background/bluetooth_ble"})
57
>>> edb.get_timeseries_db().count_documents({"metadata.key": "background/bluetooth_ble"})
0
```

```
2024-04-13 17:37:57,635:DEBUG:140704655566784:write_ts = 1713054811.255
2024-04-13 17:37:57,635:DEBUG:140704655566784:module_name = emission.net.userca
che.formatters.android.bluetooth_ble
2024-04-13 17:37:57,636:DEBUG:140704655566784:write_ts = 1713054811.294
2024-04-13 17:37:57,636:DEBUG:140704655566784:module_name = emission.net.userca
che.formatters.android.bluetooth_ble
2024-04-13 17:37:57,636:DEBUG:140704655566784:write_ts = 1713054811.316
2024-04-13 17:37:57,636:DEBUG:140704655566784:module_name = emission.net.userca
che.formatters.android.bluetooth_ble
2024-04-13 17:37:57,637:DEBUG:140704655566784:write_ts = 1713054811.339
2024-04-13 17:37:57,637:DEBUG:140704655566784:module_name = emission.net.userca
che.formatters.android.bluetooth_ble
2024-04-13 17:37:57,637:DEBUG:140704655566784:write_ts = 1713054811.369
```

```
>>> edb.get_usercache_db().count_documents({"metadata.key": "background/bluetooth_ble"})
0
>>> edb.get_timeseries_db().count_documents({"metadata.key": "background/bluetooth_ble"})
57
```

iOS

```
START 2024-04-13 16:17:50.707151 POST /usercache/put
START 2024-04-13 16:17:50.737703 POST /usercache/get
END 2024-04-13 16:17:50.763880 POST /usercache/get f1aaae55-fc42-4527-bf7f-33f84d7c8c2f 0.026064157485961914
END 2024-04-13 16:17:51.340867 POST /usercache/put f1aaae55-fc42-4527-bf7f-33f84d7c8c2f 0.6329052448272705
```

```
>>> edb.get_usercache_db().count_documents({"metadata.key": "background/bluetooth_ble"})
74
```

```
DEBUG:root:module_name = emission.net.usercache.formatters.ios.bluetooth_ble
DEBUG:root:write_ts = 1713050204.075974
DEBUG:root:module_name = emission.net.usercache.formatters.ios.bluetooth_ble
DEBUG:root:write_ts = 1713050204.077563
DEBUG:root:module_name = emission.net.usercache.formatters.ios.bluetooth_ble
DEBUG:root:write_ts = 1713050204.078974
DEBUG:root:module_name = emission.net.usercache.formatters.ios.bluetooth_ble
DEBUG:root:write_ts = 1713050207.32417
DEBUG:root:module_name = emission.net.usercache.formatters.ios.bluetooth_ble
DEBUG:root:write_ts = 1713050207.326033
```

```
>>> edb.get_usercache_db().count_documents({"metadata.key": "background/bluetooth_ble"})
0
>>> edb.get_timeseries_db().count_documents({"metadata.key": "background/bluetooth_ble"})
74
>>> edb.get_timeseries_error_db().count_documents({"metadata.key": "background/bluetooth_ble"})
0
```
@shankari
Copy link
Contributor Author

I think that the plumbing support works fairly well, so I'm going to review and try to merge at least the android implementation. We should be able to test that using the UI simulator as well. After I am done with that, I can try to merge the phone display changes and,if it works, potentially push out to staging.

@shankari
Copy link
Contributor Author

One challenge with merging in the android changes (and potentially the iOS changes down the road), is that the transition names are different. We should merge to a separate branch so that we can pick unified names and test before merging. Also, at least the android version is not saving anything; we will need to fix that before the UI can work.

The UI seems fairly basic and straightforward and supports multiple matching beacons. It just picks the beacon that occurs most frequently, so that should not be too bad.

@shankari
Copy link
Contributor Author

shankari commented Apr 14, 2024

Starting with the android changes: here are the new transitions added:

    <string name="transition_checking_for_beacon">local.transition.checking_for_beacon</string>
    <string name="transition_beacon_found">local.transition.beacon_found</string>
    <string name="transition_beacon_not_found">local.transition.beacon_not_found</string>

The middle one maps nicely to our ble_beacon_found. We should probably add ble_beacon_not_found to everything.
Do we need checking_for_beacon? That sounds almost like a state and not a transition.

I don't think we need a separate transition for checking_for_beacon. If we find it, we just start the service.
We can do that just as well from the handleTripStart state

        if (actionString.equals(ctxt.getString(R.string.transition_checking_for_beacon))) {
            Log.d(this, TAG, "Checking for beacons!");
            // Start up the bluetooth service to check for beacons
            Intent foregroundStartBluetooth = new Intent(ctxt, TripDiaryStateMachineForegroundService.class);
            foregroundStartBluetooth.setAction("foreground_start_bluetooth");
            ctxt.startService(foregroundStartBluetooth);

And with that, I don't think we actually even need beacon_not_found. If we don't get the beacon_found transition, we will just stay in waiting_for_trip_start.

So the changes needed for the android version are:

  • change the transition name
  • store ble_scan entries
  • test with the UI 😄

JGreenlee added a commit to JGreenlee/e-mission-phone that referenced this issue Apr 14, 2024
The background/bluetooth_ble data type has "major" and "minor" as decimal integers. (e-mission/e-mission-docs#1062 (comment))
But in the vehicle_identities spec, we have major:minor pairs as hexadecimal strings. So we will need to convert.

decimalToHex handles this and allows us to add padding, ensuring the converted major and minor are always 4 hex characters.
(so 1 becomes "0001", 255 becomes "00ff", etc.)
@shankari
Copy link
Contributor Author

While saving the values, I noticed that the alt-beacon values are not a direct match to the ibeacon values.
So we fall back to the standalone cordova plugin https://github.com/petermetz/cordova-plugin-ibeacon to figure out how to interpret the values correctly

Current assumptions are:

  • proximity is always "Near"
  • accuracy is the same as distance (rounded)

other values are copied over directly. I do not have a beacon yet (will order one tomorrow) so I don't have a way to test this. But I will at least make sure that it compiles before deploying to staging for others to test.

shankari added a commit to louisg1337/e-mission-data-collection that referenced this issue Apr 14, 2024
Conflict was due to the newly added transitions
Per e-mission/e-mission-docs#1062 (comment)
resolved conflict by removing transitions from this PR, and changing
`beacon_found` to `ble_beacon_found`
shankari added a commit to louisg1337/e-mission-data-collection that referenced this issue Apr 14, 2024
Consistent with
e-mission/e-mission-docs#1062 (comment)
- removed the newly added transitions
- made them consistent with the plumbed through values
- simplified the tracking logic in `TripDiaryStateMachineService`
- minor refactoring to pull the isFleet check into an instance variable that we
  can reuse without having to read it every time

Testing done:

Non-fleet case

- Start sending location
    - Not seeing any locations
- Manually exit geofence
```
04-13 22:51:45.962 23297 23297 I TripDiaryStateMachineRcvr: TripDiaryStateMachineReciever onReceive(android.app.ReceiverRestrictedContext@a7c085e, Intent { act=local.transition.exited_geofence flg=0x10 pkg=edu.berkeley.eecs.emission cmp=edu.berkeley.eecs.emission/.cordova.tracker.location.TripDiaryStateMachineReceiver }) called
04-13 22:51:46.118 23297 23297 D TripDiaryStateMachineService: handleAction(local.state.waiting_for_trip_start, local.transition.exited_geofence) called
04-13 22:51:46.160 23297 23297 D TripDiaryStateMachineService: Geofence exit in non-fleet mode, starting location tracking
```

- Stop sending locations
- Manually end trip
```
04-13 22:54:41.792 23297 23297 D TripDiaryStateMachineService: handleAction(local.state.ongoing_trip, local.transition.stopped_moving) completed, waiting for async operations to complete
```

Fleet case

- Start sending location
    - Not seeing any locations
```
$ grep LocationChangeIntentService /tmp/logcat.log | tail
04-13 22:54:32.555 23297 32018 D LocationChangeIntentService: FINALLY! Got location update, intent is Intent { cmp=edu.berkeley.eecs.emission/.cordova.tracker.location.LocationChangeIntentService (has extras) }
```

- Manually exit geofence
```
04-13 23:07:58.760 23297 23297 D TripDiaryStateMachineService: handleAction(local.state.waiting_for_trip_start, local.transition.exited_geofence) called
04-13 23:07:58.798 23297 23297 D TripDiaryStateMachineService: Geofence exit in fleet mode, checking for beacons before starting location tracking
04-13 23:07:58.807 23297 23297 D TripDiaryStateMachineService: handleAction(local.state.waiting_for_trip_start, local.transition.exited_geofence) completed, waiting for async operations to complete

$ grep LocationChangeIntentService /tmp/logcat.log | tail
04-13 22:54:32.555 23297 32018 D LocationChangeIntentService: FINALLY! Got location update, intent is Intent { cmp=edu.berkeley.eecs.emission/.cordova.tracker.location.LocationChangeIntentService (has extras) }
```

- Generate BLE event
```
04-13 23:11:26.244 23297 23297 D TripDiaryStateMachineService: handleAction(local.state.waiting_for_trip_start, local.transition.ble_beacon_found) called
04-13 23:11:26.293 23297 23297 D TripDiaryStateMachineService: TripDiaryStateMachineReceiver handleTripStart(local.transition.ble_beacon_found) called
04-13 23:11:26.319 23297 23297 D TripDiaryStateMachineService: Found beacon in fleet mode, starting location tracking
04-13 23:11:26.387 23297 23297 D ActivityRecognitionActions: Starting activity recognition with interval = 30000
04-13 23:11:26.490 23297 23297 D TripDiaryStateMachineService: handleAction(local.state.waiting_for_trip_start, local.transition.ble_beacon_found) completed, waiting for async operations to complete
04-13 23:11:26.568 23297 23297 D TripDiaryStateMachineService: newState after handling action is local.state.ongoing_trip

$ grep LocationChangeIntentService /tmp/logcat.log | tail
04-13 23:20:58.573 23297 14437 D LocationChangeIntentService: FINALLY! Got location update, intent is Intent { cmp=edu.berkeley.eecs.emission/.cordova.tracker.location.LocationChangeIntentService (has extras) }

$ grep LocationChangeIntentService /tmp/logcat.log | tail
04-13 23:22:28.651 23297 15241 D LocationChangeIntentService: FINALLY! Got location update, intent is Intent { cmp=edu.berkeley.eecs.emission/.cordova.tracker.location.LocationChangeIntentService (has extras) }
```

- Stop sending locations

- Manually end trip
```
04-13 23:23:24.649 23297 23297 D TripDiaryStateMachineService: handleAction(local.state.ongoing_trip, local.transition.stopped_moving) called
04-13 23:23:24.921 23297 23297 D TripDiaryStateMachineService: newState after handling action is local.state.waiting_for_trip_start
04-13 23:23:24.958 23297 23297 D TripDiaryStateMachineService: newState saved in prefManager is local.state.waiting_for_trip_start

04-13 23:22:54.173 23297 23297 D LocationChangeIntentService: onDestroy called
04-13 23:22:54.173 23297 23297 D LocationChangeIntentService: onDestroy called
04-13 23:22:54.173 23297 23297 D LocationChangeIntentService: onDestroy called
```
@shankari
Copy link
Contributor Author

After my fixes to e-mission/e-mission-data-collection#219 I believe that the android side should work with demo quality. @JGreenlee if you pull the data collection plugin from the integrate_ble branch, and the server changes from e-mission/e-mission-server#963 you should be able to test against a real device. I have looked at the iOS changes and they don't appear to be very tricky. I might be able to finish them tomorrow and then push out a release to staging.

shankari added a commit to the-bay-kay/e-mission-data-collection that referenced this issue Apr 14, 2024
…d non-fleet cases

The previous code had the non-fleet versions commented out.
However, when we push this to production, we need to support both fleet and
non-fleet versions. Making sure we keep the original functionality for the
fleet case.

Also, consistent with
e-mission/e-mission-docs#1062 (comment)
and
e-mission/e-mission-docs#1062 (comment)

used the standard transitions from the data model instead

NOTE: that the `isFleet` is currently hardcoded, we need to read it properly
from the dynamic config

Testing done:

2024-04-14 14:47:18.722084-0700 emission[53427:11554764] In TripDiaryStateMachine, received transition T_EXITED_GEOFENCE in state STATE_WAITING_FOR_TRIP_START
2024-04-14 14:47:18.728881-0700 emission[53427:11554764] Got transition T_EXITED_GEOFENCE in state STATE_WAITING_FOR_TRIP_START with fleet mode, checking for beacons before starting location tracking

2024-04-14 14:50:45.818623-0700 emission[53427:11554764] In TripDiaryStateMachine, received transition T_BLE_BEACON_FOUND in state STATE_WAITING_FOR_TRIP_START
2024-04-14 14:50:45.823268-0700 emission[53427:11554764] Got transition T_BLE_BEACON_FOUND in state STATE_WAITING_FOR_TRIP_START with fleet mode, starting location tracking

2024-04-14 14:51:53.498788-0700 emission[53427:11554764] DEBUG: In TripDiaryStateMachine, received transition T_BLE_BEACON_LOST in state STATE_ONGOING_TRIP

START 2024-04-14 14:51:53.685614 POST /usercache/put
START 2024-04-14 14:51:53.723916 POST /usercache/get
END 2024-04-14 14:51:53.779754 POST /usercache/get 4c152d72-a66d-4472-93f4-62b07c9e110f 0.055525779724121094
END 2024-04-14 14:51:54.061900 POST /usercache/put 4c152d72-a66d-4472-93f4-62b07c9e110f 0.3760108947753906

```
2024-04-14 14:57:32.309087-0700 emission[53427:11554764] DEBUG: In TripDiaryStateMachine, received transition T_BLE_BEACON_FOUND in state STATE_WAITING_FOR_TRIP_START
2024-04-14 14:57:32.313790-0700 emission[53427:11554764] Got transition T_BLE_BEACON_FOUND in state STATE_WAITING_FOR_TRIP_START with fleet mode, starting location tracking
2024-04-14 14:57:32.314013-0700 emission[53427:11554764] started fine location tracking with accuracy = -1 and distanceFilter = 1

2024-04-14 14:57:32.316945-0700 emission[53427:11554764] In TripDiaryStateMachine, received transition T_TRIP_STARTED in state STATE_WAITING_FOR_TRIP_START
2024-04-14 14:57:32.323449-0700 emission[53427:11554764] DEBUG: Moved from STATE_WAITING_FOR_TRIP_START to STATE_ONGOING_TRIP
```

```
2024-04-14 15:21:38.026872-0700 emission[53427:11554764] Got transition T_BLE_BEACON_FOUND in state STATE_WAITING_FOR_TRIP_START with fleet mode, starting location tracking

2024-04-14 15:28:31.763608-0700 emission[53427:11554764] Got transition T_TRIP_END_DETECTED in state STATE_ONGOING_TRIP with fleet mode, still seeing beacon but no location updates, ignoring

2024-04-14 15:29:27.951348-0700 emission[53427:11554764] DEBUG: In TripDiaryStateMachine, received transition T_BLE_BEACON_LOST in state STATE_ONGOING_TRIP
2024-04-14 15:29:27.977814-0700 emission[53427:11554764] In TripDiaryStateMachine, received transition T_END_TRIP_TRACKING in state STATE_ONGOING_TRIP

2024-04-14 15:29:27.986602-0700 emission[53427:11554764] In TripDiaryStateMachine, received transition T_TRIP_ENDED in state STATE_ONGOING_TRIP
2024-04-14 15:29:28.033050-0700 emission[53427:11554764] DEBUG: Moved from STATE_ONGOING_TRIP to STATE_WAITING_FOR_TRIP_START
```

```
2024-04-14 15:40:45.930239-0700 emission[65361:11644200] In TripDiaryStateMachine, received transition T_EXITED_GEOFENCE in state STATE_WAITING_FOR_TRIP_START
2024-04-14 15:40:45.938188-0700 emission[65361:11644200] In TripDiaryStateMachine, received transition T_TRIP_STARTED in state STATE_WAITING_FOR_TRIP_START
2024-04-14 15:40:45.968094-0700 emission[65361:11644200] In TripDiaryStateMachine, received transition T_TRIP_STARTED in state STATE_ONGOING_TRIP
2024-04-14 15:40:45.978604-0700 emission[65361:11644200] In TripDiaryStateMachine, received transition T_TRIP_RESTARTED in state STATE_ONGOING_TRIP
2024-04-14 15:40:45.987830-0700 emission[65361:11644200] In TripDiaryStateMachine, received transition T_TRIP_RESTARTED in state STATE_ONGOING_TRIP
2024-04-14 15:41:12.876066-0700 emission[65361:11644200] In TripDiaryStateMachine, received transition T_BLE_BEACON_FOUND in state STATE_ONGOING_TRIP
2024-04-14 15:41:22.486777-0700 emission[65361:11644200] In TripDiaryStateMachine, received transition T_BLE_BEACON_FOUND in state STATE_ONGOING_TRIP
2024-04-14 15:41:24.861993-0700 emission[65361:11644200] In TripDiaryStateMachine, received transition T_BLE_BEACON_FOUND in state STATE_ONGOING_TRIP
2024-04-14 15:41:27.220213-0700 emission[65361:11644200] In TripDiaryStateMachine, received transition T_BLE_BEACON_FOUND in state STATE_ONGOING_TRIP
2024-04-14 15:41:31.110132-0700 emission[65361:11644200] In TripDiaryStateMachine, received transition T_BLE_BEACON_LOST in state STATE_ONGOING_TRIP
2024-04-14 15:41:37.788507-0700 emission[65361:11644200] In TripDiaryStateMachine, received transition T_BLE_BEACON_LOST in state STATE_ONGOING_TRIP
2024-04-14 15:41:37.796704-0700 emission[65361:11644200] In TripDiaryStateMachine, received transition T_BLE_BEACON_LOST in state STATE_ONGOING_TRIP
2024-04-14 15:41:53.319772-0700 emission[65361:11644200] In TripDiaryStateMachine, received transition T_TRIP_END_DETECTED in state STATE_ONGOING_TRIP
2024-04-14 15:41:53.871614-0700 emission[65361:11644200] In TripDiaryStateMachine, received transition (null) in state STATE_ONGOING_TRIP
2024-04-14 15:41:55.577755-0700 emission[65361:11644200] In TripDiaryStateMachine, received transition T_END_TRIP_TRACKING in state STATE_ONGOING_TRIP
2024-04-14 15:41:55.583780-0700 emission[65361:11644200] In TripDiaryStateMachine, received transition T_TRIP_ENDED in state STATE_ONGOING_TRIP
2024-04-14 15:41:56.022684-0700 emission[65361:11644200] In TripDiaryStateMachine, received transition T_DATA_PUSHED in state STATE_WAITING_FOR_TRIP_START
```
shankari added a commit to the-bay-kay/e-mission-data-collection that referenced this issue Apr 15, 2024
Consistent with
e-mission/e-mission-docs#1062 (comment)
store the BLE scan results so that we can use them while matching the mapping

While storing the changes, I realized that the region enter and exit calls
returned a CLBeaconRegion object and the range scans returned a CLRegion
object.
e-mission#220 (comment)

This needed us to change the wrapper class as well:
- Add a new initFromCLBeaconRegion that only copies non-range values and sets
  default values for the range values (proximity, accuracy and rssi)
- Change the existing initFromCLBeacon to not need an event type, since it is
  only called for the scans, and the event type is always `RANGE_UPDATE` in
  that case.

Testing done:
- Compiled
- Launched the app
- Note that we cannot test this code without a physical beacon, but it is very
  straightforward and compiles. The only reason why it may fail is if values
  are not filled out in unexpected ways.
shankari added a commit to shankari/e-mission-data-collection that referenced this issue Apr 15, 2024
The current implementation on android starts ranging for beacons after a
geofence exit and gives up after 4 retries. This is known to work when the
beacon is available at the start of the trip - e.g. walking to work with a
beacon tracks, walking to work without a beacon does not.

But this may not work when the beacon is not at the start location. For
example, if the geofence triggers as soon as a user leaves their work building,
and then they have to walk several minutes to get to their car, the ranging
will timeout and the trip will be missed. This is a bit less likely because our
geofence radius is set to ~ half a block, but we still don't want to miss
trips.

We should really handle this with monitoring versus ranging, but for now, we
will at least bump up the duration for which the ranging happens.

The range callback is roughly once a minute
https://github.com/AltBeacon/android-beacon-library/blob/7af8419c404329ac7dc6001917b5962d2cd53a11/lib/src/main/java/org/altbeacon/beacon/RangeNotifier.java#L39

```
    /**
     * Called once per second to give an estimate of the mDistance to visible beacons
     * @param beacons a collection of <code>Beacon<code> objects that have been seen in the past second
     * @param region the <code>Region</code> object that defines the criteria for the ranged beacons
     */
    public void didRangeBeaconsInRegion(Collection<Beacon> beacons, Region region);
```

So let's scan 5 * 60 times (roughly 5 minutes) before giving up

This is the last change for e-mission/e-mission-docs#1062
before we move to staging
@shankari
Copy link
Contributor Author

Per @the-bay-kay, things are not crashing. Going to merge this set of changes and get it on to staging to test.
Can do another release tomorrow morning with UI changes (if any) since we can at least get the native code changes shaken out a bit before pushing to production.

@shankari
Copy link
Contributor Author

The plumbing seems to work well enough for an "alpha" version. We may need to make changes as we polish, but closing this issue for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants