From 5f53e5301e39ea53717f6ea3ac4b030b053c03fe Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Tue, 2 Apr 2024 23:53:12 -0400 Subject: [PATCH 01/22] include 'sections' in UnprocessedTrips To be consistent with processed trips, and so we can use them in the same way, unprocessed trips should have the 'sections' property. Unprocessed trips / 'draft' trips are assumed to be unimodal, so we can fairly easily reconstruct a section spanning the entirety of the trip and include that as the only section. In doing so, I modified points2TripProps. I realized that many of the props will actually be the same between the trip and the section. So I did a bit of refactoring; to construct the unprocessed trip I first construct baseProps (which are used in both the section and the trip); then I construct the section; then finally I construct the trip in the return statement. Something else I noticed is that all the trip props are computed in points2TripProps except the start_loc and end_loc which were computed in the outer function transitionTrip2TripObj. Since we already have variables for the start and end coordinates in the points2TripProps function, it seems more logical to handle start_loc and end_loc there with the rest of the trip props. Then we can declare the return type of points2TripProps as UnprocessedTrip; it has all the required properties now. --- www/js/diary/timelineHelper.ts | 62 +++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index f140f1750..81c32956a 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -14,6 +14,7 @@ import { TimestampRange, CompositeTrip, UnprocessedTrip, + SectionData, } from '../types/diaryTypes'; import { getLabelInputDetails, getLabelInputs } from '../survey/multilabel/confirmHelper'; import { LabelOptions } from '../types/labelTypes'; @@ -288,7 +289,7 @@ const dateTime2localdate = (currtime: DateTime, tz: string) => ({ second: currtime.second, }); -function points2TripProps(locationPoints: Array>) { +function points2TripProps(locationPoints: Array>): UnprocessedTrip { const startPoint = locationPoints[0]; const endPoint = locationPoints[locationPoints.length - 1]; const tripAndSectionId = `unprocessed_${startPoint.data.ts}_${endPoint.data.ts}`; @@ -318,24 +319,51 @@ function points2TripProps(locationPoints: Array>) { speed: speeds[i], })); - return { - _id: { $oid: tripAndSectionId }, - key: 'UNPROCESSED_trip', - origin_key: 'UNPROCESSED_trip', - additions: [], - confidence_threshold: 0, + // baseProps: these are the properties that are the same between the trip and its section + const baseProps = { distance: dists.reduce((a, b) => a + b, 0), duration: endPoint.data.ts - startPoint.data.ts, end_fmt_time: endTime.toISO() || displayErrorMsg('end_fmt_time: invalid DateTime') || '', + end_loc: { + type: 'Point', + coordinates: [endPoint.data.longitude, endPoint.data.latitude], + } as Point, end_local_dt: dateTime2localdate(endTime, endPoint.metadata.time_zone), end_ts: endPoint.data.ts, - expectation: { to_label: true }, - inferred_labels: [], - locations: locations, source: 'unprocessed', start_fmt_time: startTime.toISO() || displayErrorMsg('start_fmt_time: invalid DateTime') || '', + start_loc: { + type: 'Point', + coordinates: [startPoint.data.longitude, startPoint.data.latitude], + } as Point, start_local_dt: dateTime2localdate(startTime, startPoint.metadata.time_zone), start_ts: startPoint.data.ts, + }; + + // section: baseProps + some properties that are unique to the section + const singleSection: SectionData = { + ...baseProps, + _id: { $oid: `unprocessed_section_${tripAndSectionId}` }, + cleaned_section: { $oid: `unprocessed_section_${tripAndSectionId}` }, + key: 'UNPROCESSED_section', + origin_key: 'UNPROCESSED_section', + sensed_mode: 4, // MotionTypes.UNKNOWN (4) + sensed_mode_str: 'UNKNOWN', + trip_id: { $oid: tripAndSectionId }, + }; + + // the complete UnprocessedTrip: baseProps + properties that are unique to the trip, including the section + return { + ...baseProps, + _id: { $oid: tripAndSectionId }, + additions: [], + confidence_threshold: 0, + expectation: { to_label: true }, + inferred_labels: [], + key: 'UNPROCESSED_trip', + locations: locations, + origin_key: 'UNPROCESSED_trip', + sections: [singleSection], user_input: {}, }; } @@ -386,19 +414,7 @@ function transitionTrip2TripObj(trip: Array): Promise Date: Wed, 3 Apr 2024 00:39:36 -0400 Subject: [PATCH 02/22] update types relating to `UnprocessedTrip`s The main thing I was doing in this commit was adding 'sections' to the type signature of `UnprocessedTrip`. But I also noticed some other things amiss. `UnprocessedTrip` was missing some other properties; end_ts and start_fmt_time "LocationCoord" is not needed as it's the same as `Point` from 'geojson'. `SectionData` was missing a bunch of properties. Once those are filled in, the 'sections' property in `CompositeTrip` and `UnprocessedTrip` can be typed as `SectionData[]` --- www/js/types/diaryTypes.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/www/js/types/diaryTypes.ts b/www/js/types/diaryTypes.ts index 75c43b2d6..12102477d 100644 --- a/www/js/types/diaryTypes.ts +++ b/www/js/types/diaryTypes.ts @@ -45,11 +45,6 @@ export type TripTransition = { ts: number; }; -export type LocationCoord = { - type: string; // e.x., "Point" - coordinates: [number, number]; -}; - type CompTripLocations = { loc: { coordinates: number[]; // e.g. [1, 2.3] @@ -68,12 +63,15 @@ export type UnprocessedTrip = { end_fmt_time: string; end_loc: Point; end_local_dt: LocalDt; + end_ts: number; expectation: any; // TODO "{to_label: boolean}" inferred_labels: any[]; // TODO key: string; locations?: CompTripLocations[]; origin_key: string; // e.x., UNPROCESSED_trip + sections: SectionData[]; source: string; + start_fmt_time: string; start_local_dt: LocalDt; start_ts: number; start_loc: Point; @@ -107,7 +105,7 @@ export type CompositeTrip = { locations: any[]; // TODO origin_key: string; raw_trip: ObjectId; - sections: any[]; // TODO + sections: SectionData[]; source: string; start_confirmed_place: BEMData; start_fmt_time: string; @@ -188,23 +186,25 @@ export type Location = { latitude: number; fmt_time: string; // ISO mode: number; - loc: LocationCoord; + loc: Point; ts: number; // Unix altitude: number; distance: number; }; -// used in readAllCompositeTrips export type SectionData = { + _id: ObjectId; end_ts: number; // Unix time, e.x. 1696352498.804 - end_loc: LocationCoord; + end_loc: Point; start_fmt_time: string; // ISO time end_fmt_time: string; + key: string; + origin_key: string; trip_id: ObjectId; sensed_mode: number; source: string; // e.x., "SmoothedHighConfidenceMotion" start_ts: number; // Unix - start_loc: LocationCoord; + start_loc: Point; cleaned_section: ObjectId; start_local_dt: LocalDt; end_local_dt: LocalDt; @@ -213,7 +213,7 @@ export type SectionData = { distance: number; }; -// used in timelineHelper's `transitionTrip2TripObj` +// used in timelineHelper's `transitionTrip2UnprocessedTrip` export type FilteredLocation = { accuracy: number; altitude: number; From 12db91a30c1f3d0c1b852a2efe1e4512c34d9a5f Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 3 Apr 2024 00:47:12 -0400 Subject: [PATCH 03/22] rename functions related to unprocessed trips After a bunch of refactoring, I think these functions could use a naming update. I always found this part of the codebase fairly opaque anyway and I think it can now be made more easily comprehensible. 'points2TripProps' now returns a full UnprocessedTrip object so it is renamed 'points2UnprocessedTrip' And 'transitions2Trips' is renamed 'transitions2TripTransitions' because it doesn't really return trip objects; it returns pairs of transitions that represent trips. To follow suit, 'transitionTrip2TripObj' is renamed 'tripTransitions2UnprocessedTrip'. Added a bit of JSDoc to help clarify what these functions do. --- www/js/diary/timelineHelper.ts | 52 ++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index 81c32956a..006cc0788 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -289,7 +289,10 @@ const dateTime2localdate = (currtime: DateTime, tz: string) => ({ second: currtime.second, }); -function points2TripProps(locationPoints: Array>): UnprocessedTrip { +/** + * @description Given an array of location points, creates an UnprocessedTrip object. + */ +function points2UnprocessedTrip(locationPoints: Array>): UnprocessedTrip { const startPoint = locationPoints[0]; const endPoint = locationPoints[locationPoints.length - 1]; const tripAndSectionId = `unprocessed_${startPoint.data.ts}_${endPoint.data.ts}`; @@ -371,7 +374,11 @@ function points2TripProps(locationPoints: Array>): Unp const tsEntrySort = (e1: BEMData, e2: BEMData) => e1.data.ts - e2.data.ts; // compare timestamps -function transitionTrip2TripObj(trip: Array): Promise { +/** + * @description Given an array of 2 transitions, queries the location data during that time and promises an UnprocessedTrip object. + * @param trip An array of transitions representing one trip; i.e. [start transition, end transition] + */ +function tripTransitions2UnprocessedTrip(trip: Array): Promise { const tripStartTransition = trip[0]; const tripEndTransition = trip[1]; const tq = { @@ -413,8 +420,7 @@ function transitionTrip2TripObj(trip: Array): Promise) { // Logger.log("Returning false"); return false; } -/* - * This is going to be a bit tricky. As we can see from - * https://github.com/e-mission/e-mission-phone/issues/214#issuecomment-286279163, - * when we read local transitions, they have a string for the transition - * (e.g. `T_DATA_PUSHED`), while the remote transitions have an integer - * (e.g. `2`). - * See https://github.com/e-mission/e-mission-phone/issues/214#issuecomment-286338606 - * - * Also, at least on iOS, it is possible for trip end to be detected way - * after the end of the trip, so the trip end transition of a processed - * trip may actually show up as an unprocessed transition. - * See https://github.com/e-mission/e-mission-phone/issues/214#issuecomment-286279163 - * - * Let's abstract this out into our own minor state machine. + +/** + * @description Given an array of transitions, finds which transitions represent the start and end of a detected trip and returns them as pairs. + * @returns An 2D array of transitions, where each inner array represents one trip; i.e. [start transition, end transition] */ -function transitions2Trips(transitionList: Array>) { +function transitions2TripTransitions(transitionList: Array>) { + /* This is going to be a bit tricky. As we can see from + * https://github.com/e-mission/e-mission-phone/issues/214#issuecomment-286279163, + * when we read local transitions, they have a string for the transition + * (e.g. `T_DATA_PUSHED`), while the remote transitions have an integer + * (e.g. `2`). + * See https://github.com/e-mission/e-mission-phone/issues/214#issuecomment-286338606 + * + * Also, at least on iOS, it is possible for trip end to be detected way + * after the end of the trip, so the trip end transition of a processed + * trip may actually show up as an unprocessed transition. + * See https://github.com/e-mission/e-mission-phone/issues/214#issuecomment-286279163 + * + * Let's abstract this out into our own minor state machine. + */ let inTrip = false; const tripList: [BEMData, BEMData][] = []; let currStartTransitionIndex = -1; @@ -536,12 +546,12 @@ export function readUnprocessedTrips( return []; } else { logDebug(`Found ${transitionList.length} transitions. yay!`); - const tripsList = transitions2Trips(transitionList); + const tripsList = transitions2TripTransitions(transitionList); logDebug(`Mapped into ${tripsList.length} trips. yay!`); tripsList.forEach((trip) => { logDebug(JSON.stringify(trip, null, 2)); }); - const tripFillPromises = tripsList.map(transitionTrip2TripObj); + const tripFillPromises = tripsList.map(tripTransitions2UnprocessedTrip); return Promise.all(tripFillPromises).then( (rawTripObjs: (UnprocessedTrip | undefined)[]) => { // Now we need to link up the trips. linking unprocessed trips From 826190a0f842a61c205fb206ef11d8939d8023d0 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 3 Apr 2024 01:29:03 -0400 Subject: [PATCH 04/22] flesh out more types Filled in more trip object types, and specifically some literal types that define unprocessed trips. (like "source" will always be 'unprocessed'). CompTripLocations changed from `number[]` for coordinates to geojson's `Position`, since that is more descriptive. Renamed CompTripLocations to CompositeTripLocation (this type represents only 1 location). Used the CompositeTripLocation type in timelineHelper. in locations2GeojsonTrajectory, the return type needed `properties` for it to be considered a Geojson `Feature`. formattedSectionProperties types as the return type of the function --- www/js/diary/timelineHelper.ts | 12 ++++++++---- www/js/types/diaryTypes.ts | 26 +++++++++++++------------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index 006cc0788..6c87cd0b9 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -15,6 +15,7 @@ import { CompositeTrip, UnprocessedTrip, SectionData, + CompositeTripLocation, } from '../types/diaryTypes'; import { getLabelInputDetails, getLabelInputs } from '../survey/multilabel/confirmHelper'; import { LabelOptions } from '../types/labelTypes'; @@ -216,10 +217,10 @@ const location2GeojsonPoint = (locationPoint: Point, featureType: string): Featu */ function locations2GeojsonTrajectory( trip: CompositeTrip, - locationList: Array, + locationList: CompositeTripLocation[], trajectoryColor?: string, -) { - let sectionsPoints; +): Feature[] { + let sectionsPoints: CompositeTripLocation[][]; if (!trip.sections) { // this is a unimodal trip so we put all the locations in one section sectionsPoints = [locationList]; @@ -243,6 +244,9 @@ function locations2GeojsonTrajectory( color for the sensed mode of this section, and fall back to dark grey */ color: trajectoryColor || getBaseModeByKey(section?.sensed_mode_str)?.color || '#333', }, + properties: { + feature_type: 'section_trajectory', + }, }; }); } @@ -341,7 +345,7 @@ function points2UnprocessedTrip(locationPoints: Array> } as Point, start_local_dt: dateTime2localdate(startTime, startPoint.metadata.time_zone), start_ts: startPoint.data.ts, - }; + } as const; // section: baseProps + some properties that are unique to the section const singleSection: SectionData = { diff --git a/www/js/types/diaryTypes.ts b/www/js/types/diaryTypes.ts index 12102477d..8b8de469c 100644 --- a/www/js/types/diaryTypes.ts +++ b/www/js/types/diaryTypes.ts @@ -5,7 +5,7 @@ import { BaseModeKey, MotionTypeKey } from '../diary/diaryHelper'; import { MultilabelKey } from './labelTypes'; import { BEMData, LocalDt } from './serverData'; -import { FeatureCollection, Feature, Geometry, Point } from 'geojson'; +import { FeatureCollection, Feature, Geometry, Point, Position } from 'geojson'; type ObjectId = { $oid: string }; @@ -45,9 +45,9 @@ export type TripTransition = { ts: number; }; -type CompTripLocations = { +export type CompositeTripLocation = { loc: { - coordinates: number[]; // e.g. [1, 2.3] + coordinates: Position; // [lon, lat] }; speed: number; ts: number; @@ -56,7 +56,7 @@ type CompTripLocations = { // Used for return type of readUnprocessedTrips export type UnprocessedTrip = { _id: ObjectId; - additions: UserInputEntry[]; + additions: []; // unprocessed trips won't have any matched processed inputs, so this is always empty confidence_threshold: number; distance: number; duration: number; @@ -64,19 +64,19 @@ export type UnprocessedTrip = { end_loc: Point; end_local_dt: LocalDt; end_ts: number; - expectation: any; // TODO "{to_label: boolean}" - inferred_labels: any[]; // TODO - key: string; - locations?: CompTripLocations[]; - origin_key: string; // e.x., UNPROCESSED_trip + expectation: { to_label: true }; // unprocessed trips are always expected to be labeled + inferred_labels: []; // unprocessed trips won't have inferred labels + key: 'UNPROCESSED_trip'; + locations?: CompositeTripLocation[]; + origin_key: 'UNPROCESSED_trip'; sections: SectionData[]; - source: string; + source: 'unprocessed'; start_fmt_time: string; start_local_dt: LocalDt; start_ts: number; start_loc: Point; starting_trip?: any; - user_input: UserInput; + user_input: {}; // unprocessed trips won't have any matched processed inputs, so this is always empty }; /* These are the properties received from the server (basically matches Python code) @@ -96,13 +96,13 @@ export type CompositeTrip = { end_local_dt: LocalDt; end_place: ObjectId; end_ts: number; - expectation: any; // TODO "{to_label: boolean}" + expectation: { to_label: boolean }; expected_trip: ObjectId; inferred_labels: InferredLabels; inferred_section_summary: SectionSummary; inferred_trip: ObjectId; key: string; - locations: any[]; // TODO + locations: CompositeTripLocation[]; origin_key: string; raw_trip: ObjectId; sections: SectionData[]; From 24304f325afe51e724b7e3a8f594b27f607d2b59 Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Mon, 8 Apr 2024 13:02:17 -0700 Subject: [PATCH 05/22] add collectCoverageForm to track all files for Jest coverage --- jest.config.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/jest.config.js b/jest.config.js index 6a1dcd42b..939834e51 100644 --- a/jest.config.js +++ b/jest.config.js @@ -18,4 +18,9 @@ module.exports = { moduleDirectories: ["node_modules", "src"], globals: {"__DEV__": false}, collectCoverage: true, + collectCoverageFrom: [ + "www/js/**/*.{ts,tsx,js,jsx}", + "!www/js/**/index.{ts,tsx,js,jsx}", + "!www/js/types/**/*.{ts,tsx,js,jsx}", + ], }; From 2cc07a0d007a52e31d352858f94620e3cf334bea Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Fri, 12 Apr 2024 15:23:37 -0400 Subject: [PATCH 06/22] add type defs for BLE data + new config fields As discussed in https://github.com/e-mission/e-mission-docs/issues/1062 --- www/js/types/appConfigTypes.ts | 22 ++++++++++++++++++++++ www/js/types/diaryTypes.ts | 11 +++++++++++ 2 files changed, 33 insertions(+) diff --git a/www/js/types/appConfigTypes.ts b/www/js/types/appConfigTypes.ts index 5bfedce03..b97ce2a4d 100644 --- a/www/js/types/appConfigTypes.ts +++ b/www/js/types/appConfigTypes.ts @@ -9,6 +9,10 @@ export type AppConfig = { surveys: EnketoSurveyConfig; buttons?: SurveyButtonsConfig; }; + vehicle_identities?: VehicleIdentity[]; + tracking?: { + bluetooth_only: boolean; + }; reminderSchemes?: ReminderSchemesConfig; [k: string]: any; // TODO fill in all the other fields }; @@ -57,6 +61,24 @@ export type SurveyButtonsConfig = { | SurveyButtonConfig[]; }; +export type VehicleIdentity = { + value: string; + bluetooth_major_minor: string[]; // e.g. ['aaaa:bbbb', 'cccc:dddd'] + text: string; + baseMode: string; + met_equivalent: string; + kgCo2PerKm: number; + vehicle_info: { + type: string; + license: string; + make: string; + model: string; + year: number; + color: string; + engine: 'ICE' | 'HEV' | 'PHEV' | 'BEV' | 'HYDROGENV' | 'BIOV'; + }; +}; + export type ReminderSchemesConfig = { [schemeKey: string]: { title: { [lang: string]: string }; diff --git a/www/js/types/diaryTypes.ts b/www/js/types/diaryTypes.ts index 75c43b2d6..7eb4248e2 100644 --- a/www/js/types/diaryTypes.ts +++ b/www/js/types/diaryTypes.ts @@ -178,6 +178,17 @@ export type UserInputEntry = { key?: string; }; +export type BluetoothBleData = { + ts: number; + eventType: 'REGION_ENTER' | 'REGION_EXIT' | 'RANGE_UPDATE'; + uuid: string; + major: number; // for our use case, missing for REGION_ENTER or REGION_EXIT + minor: number; // for our use case, missing for REGION_ENTER or REGION_EXIT + proximity?: string; // only available for RANGE_UPDATE + rssi?: string; // only available for RANGE_UPDATE + accuracy?: string; // only available for RANGE_UPDATE +}; + export type Location = { speed: number; heading: number; From a586bb71d76d1fa4447336b740f0bcb55940a349 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Fri, 12 Apr 2024 15:33:33 -0400 Subject: [PATCH 07/22] retrieve + match BLE scans to timelineEntries We nearly have the Bluetooth integration done. Now we just need the phone to retrieve BLE scans and implement some matching logic to figure out which beacons go to which trips. in timelineHelper there is a new function to retrieve BLE scans using the unified data loader. in inputMatcher we have new functions that go through each timelineEntry and match it to a beacon. Then it looks up that beacon (by its major:minor pair) in the app config to get the vehicle identity. In LabelTab, we have a new map, timelineBleMap, which holds the mapping of timeline entry IDs to vehicle identities. --- www/js/diary/LabelTab.tsx | 23 ++++++++++- www/js/diary/timelineHelper.ts | 18 ++++++++ www/js/survey/inputMatcher.ts | 75 +++++++++++++++++++++++++++++++++- 3 files changed, 113 insertions(+), 3 deletions(-) diff --git a/www/js/diary/LabelTab.tsx b/www/js/diary/LabelTab.tsx index 0ceaf0505..1ed20c0cc 100644 --- a/www/js/diary/LabelTab.tsx +++ b/www/js/diary/LabelTab.tsx @@ -21,13 +21,19 @@ import { updateLocalUnprocessedInputs, unprocessedLabels, unprocessedNotes, + updateUnprocessedBleScans, + unprocessedBleScans, } from './timelineHelper'; import { fillLocationNamesOfTrip, resetNominatimLimiter } from './addressNamesHelper'; import { getLabelOptions, labelOptionByValue } from '../survey/multilabel/confirmHelper'; import { displayError, displayErrorMsg, logDebug, logWarn } from '../plugin/logger'; import { useTheme } from 'react-native-paper'; import { getPipelineRangeTs } from '../services/commHelper'; -import { getNotDeletedCandidates, mapInputsToTimelineEntries } from '../survey/inputMatcher'; +import { + getNotDeletedCandidates, + mapBleScansToTimelineEntries, + mapInputsToTimelineEntries, +} from '../survey/inputMatcher'; import { configuredFilters as multilabelConfiguredFilters } from '../survey/multilabel/infinite_scroll_filters'; import { configuredFilters as enketoConfiguredFilters } from '../survey/enketo/infinite_scroll_filters'; import LabelTabContext, { @@ -57,6 +63,7 @@ const LabelTab = () => { const [timelineMap, setTimelineMap] = useState(null); const [timelineLabelMap, setTimelineLabelMap] = useState(null); const [timelineNotesMap, setTimelineNotesMap] = useState(null); + const [timelineBleMap, setTimelineBleMap] = useState(null); const [displayedEntries, setDisplayedEntries] = useState(null); const [refreshTime, setRefreshTime] = useState(null); const [isLoading, setIsLoading] = useState('replace'); @@ -102,6 +109,11 @@ const LabelTab = () => { setTimelineLabelMap(newTimelineLabelMap); setTimelineNotesMap(newTimelineNotesMap); + if (appConfig.vehicle_identities?.length) { + const newTimelineBleMap = mapBleScansToTimelineEntries(allEntries, appConfig); + setTimelineBleMap(newTimelineBleMap); + } + applyFilters(timelineMap, newTimelineLabelMap); } catch (e) { displayError(e, t('errors.while-updating-timeline')); @@ -157,6 +169,15 @@ const LabelTab = () => { logDebug(`LabelTab: After updating unprocessedInputs, unprocessedLabels = ${JSON.stringify(unprocessedLabels)}; unprocessedNotes = ${JSON.stringify(unprocessedNotes)}`); + if (appConfig.vehicle_identities?.length) { + await updateUnprocessedBleScans({ + start_ts: pipelineRange.start_ts, + end_ts: Date.now() / 1000, + }); + logDebug(`LabelTab: After updating unprocessedBleScans, + unprocessedBleScans = ${JSON.stringify(unprocessedBleScans)}; + `); + } setPipelineRange(pipelineRange); } catch (e) { displayError(e, t('errors.while-loading-pipeline-range')); diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index f140f1750..f64318731 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -14,6 +14,7 @@ import { TimestampRange, CompositeTrip, UnprocessedTrip, + BluetoothBleData, } from '../types/diaryTypes'; import { getLabelInputDetails, getLabelInputs } from '../survey/multilabel/confirmHelper'; import { LabelOptions } from '../types/labelTypes'; @@ -175,6 +176,23 @@ export async function updateAllUnprocessedInputs( await updateUnprocessedInputs(labelsPromises, notesPromises, appConfig); } +export let unprocessedBleScans: BEMData[] = []; + +export async function updateUnprocessedBleScans(queryRange: TimestampRange) { + const tq = { + key: 'write_ts', + startTs: queryRange.start_ts, + endTs: queryRange.end_ts, + }; + const getMethod = window['cordova'].plugins.BEMUserCache.getSensorDataForInterval; + getUnifiedDataForInterval('background/background_ble', tq, getMethod).then( + (bleScans: BEMData[]) => { + logDebug(`Read ${bleScans.length} BLE scans`); + unprocessedBleScans = bleScans; + }, + ); +} + export function keysForLabelInputs(appConfig: AppConfig) { if (appConfig.survey_info?.['trip-labels'] == 'ENKETO') { return ['manual/trip_user_input']; diff --git a/www/js/survey/inputMatcher.ts b/www/js/survey/inputMatcher.ts index da802d2e8..fdfb25579 100644 --- a/www/js/survey/inputMatcher.ts +++ b/www/js/survey/inputMatcher.ts @@ -1,7 +1,18 @@ import { logDebug, displayErrorMsg } from '../plugin/logger'; import { DateTime } from 'luxon'; -import { CompositeTrip, ConfirmedPlace, TimelineEntry, UserInputEntry } from '../types/diaryTypes'; -import { keysForLabelInputs, unprocessedLabels, unprocessedNotes } from '../diary/timelineHelper'; +import { + BluetoothBleData, + CompositeTrip, + ConfirmedPlace, + TimelineEntry, + UserInputEntry, +} from '../types/diaryTypes'; +import { + keysForLabelInputs, + unprocessedBleScans, + unprocessedLabels, + unprocessedNotes, +} from '../diary/timelineHelper'; import { getLabelInputDetails, inputType2retKey, @@ -11,6 +22,7 @@ import { TimelineLabelMap, TimelineNotesMap } from '../diary/LabelTabContext'; import { MultilabelKey } from '../types/labelTypes'; import { EnketoUserInputEntry } from './enketo/enketoHelper'; import { AppConfig } from '../types/appConfigTypes'; +import { BEMData } from '../types/serverData'; const EPOCH_MAXIMUM = 2 ** 31 - 1; @@ -340,3 +352,62 @@ export function mapInputsToTimelineEntries( return [timelineLabelMap, timelineNotesMap]; } + +function validBleScanForTimelineEntry(tlEntry: TimelineEntry, bleScan: BEMData) { + let entryStart = (tlEntry as CompositeTrip).start_ts || (tlEntry as ConfirmedPlace).enter_ts; + let entryEnd = (tlEntry as CompositeTrip).end_ts || (tlEntry as ConfirmedPlace).exit_ts; + + if (!entryStart && entryEnd) { + /* if a place has no enter time, this is the first start_place of the first composite trip object + so we will set the start time to the start of the day of the end time for the purpose of comparison */ + entryStart = DateTime.fromSeconds(entryEnd).startOf('day').toUnixInteger(); + } + + if (!entryEnd) { + /* if a place has no exit time, the user hasn't left there yet + so we will set the end time as high as possible for the purpose of comparison */ + entryEnd = EPOCH_MAXIMUM; + } + + return bleScan.data.ts >= entryStart && bleScan.data.ts <= entryEnd; +} + +function getBleScansForTimelineEntry( + tlEntry: TimelineEntry, + bleScans: BEMData[], +) { + return bleScans.filter((scan) => validBleScanForTimelineEntry(tlEntry, scan)); +} + +export function mapBleScansToTimelineEntries(allEntries: TimelineEntry[], appConfig: AppConfig) { + const timelineBleMap = {}; + for (const tlEntry of allEntries) { + const matches = getBleScansForTimelineEntry(tlEntry, unprocessedBleScans); + if (!matches.length) { + continue; + } + + // count the number of occurrences of each major:minor pair + const majorMinorCounts = {}; + matches.forEach((match) => { + const majorMinor = match.data.major + ':' + match.data.minor; + majorMinorCounts[majorMinor] = majorMinorCounts[majorMinor] + ? majorMinorCounts[majorMinor] + 1 + : 1; + }); + // determine the major:minor pair with the highest count + const match = Object.keys(majorMinorCounts).reduce((a, b) => + majorMinorCounts[a] > majorMinorCounts[b] ? a : b, + ); + // find the vehicle identity that uses this major:minor pair + const vehicleIdentity = appConfig.vehicle_identities?.find((vi) => + vi.bluetooth_major_minor.includes(match), + ); + if (vehicleIdentity) { + timelineBleMap[tlEntry._id.$oid] = vehicleIdentity; + } else { + displayErrorMsg(`No vehicle identity found for major:minor pair ${match}`); + } + } + return timelineBleMap; +} From bff97ae35d983805572c4270f1e4e954e49d2ceb Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Fri, 12 Apr 2024 15:44:29 -0400 Subject: [PATCH 08/22] use labels OR bluetooth vehicle identity to determine "confirmed mode" With the ability to detect vehicles by Bluetooth, we now have 2 ways to determine the mode of a trip: user labels or Bluetooth. This makes it necessary to refactor and make the code flexible to handle both. "Confirmed mode" is the new term for a mode that has been determined; either by the user-provided MODE label, or by the vehicle identity determined by Bluetooth. "Confirmed mode" is given by the "confirmedModeFor" function. Throughout the codebase, we now use `confirmedModeFor(trip)` instead of `labelFor(trip, 'MODE')`. For existing configurations that use user MODE labels, this does not change the behavior. -- While doing this I refactored useGeojsonForTrip to directly accept the base mode, since that is what it really needs, instead of the labeledMode. As a result, it also doesn't need labelOptions as an argument anymore. --- www/__tests__/timelineHelper.test.ts | 7 ++-- www/js/diary/LabelTab.tsx | 9 ++++++ www/js/diary/LabelTabContext.ts | 2 ++ www/js/diary/cards/ModesIndicator.tsx | 12 +++---- www/js/diary/cards/TripCard.tsx | 4 +-- www/js/diary/details/LabelDetailsScreen.tsx | 32 +++++++++---------- .../details/TripSectionsDescriptives.tsx | 16 +++++----- www/js/diary/timelineHelper.ts | 11 ++----- 8 files changed, 48 insertions(+), 45 deletions(-) diff --git a/www/__tests__/timelineHelper.test.ts b/www/__tests__/timelineHelper.test.ts index bd817f084..aafe13926 100644 --- a/www/__tests__/timelineHelper.test.ts +++ b/www/__tests__/timelineHelper.test.ts @@ -28,7 +28,7 @@ afterAll(() => { describe('useGeojsonForTrip', () => { it('work with an empty input', () => { - const testVal = useGeojsonForTrip({} as any, {} as any); + const testVal = useGeojsonForTrip({} as any); expect(testVal).toBeFalsy; }); @@ -43,10 +43,7 @@ describe('useGeojsonForTrip', () => { }; it('works without labelMode flag', () => { - const testValue = useGeojsonForTrip( - mockTLH.mockCompDataTwo.phone_data[1].data, - mockTLH.mockLabelOptions, - ) as GeoJSONData; + const testValue = useGeojsonForTrip(mockTLH.mockCompDataTwo.phone_data[1].data) as GeoJSONData; expect(testValue).toBeTruthy; checkGeojson(testValue); expect(testValue.data.features.length).toBe(3); diff --git a/www/js/diary/LabelTab.tsx b/www/js/diary/LabelTab.tsx index 1ed20c0cc..938a861f3 100644 --- a/www/js/diary/LabelTab.tsx +++ b/www/js/diary/LabelTab.tsx @@ -331,6 +331,14 @@ const LabelTab = () => { return chosenLabel ? labelOptionByValue(chosenLabel, labelType) : undefined; }; + /** + * @param tlEntry The trip or place object to get the confirmed mode for + * @returns Confirmed mode, which could be a vehicle identity as determined by Bluetooth scans, + * or the label option from a user-given 'MODE' label, or undefined if neither exists. + */ + const confirmedModeFor = (tlEntry: TimelineEntry) => + timelineBleMap?.[tlEntry._id.$oid] || labelFor(tlEntry, 'MODE'); + function addUserInputToEntry(oid: string, userInput: any, inputType: 'label' | 'note') { const tlEntry = timelineMap?.get(oid); if (!pipelineRange || !tlEntry) @@ -372,6 +380,7 @@ const LabelTab = () => { userInputFor, labelFor, notesFor, + confirmedModeFor, addUserInputToEntry, displayedEntries, filterInputs, diff --git a/www/js/diary/LabelTabContext.ts b/www/js/diary/LabelTabContext.ts index 9e80cccae..791cb4cd5 100644 --- a/www/js/diary/LabelTabContext.ts +++ b/www/js/diary/LabelTabContext.ts @@ -2,6 +2,7 @@ import { createContext } from 'react'; import { TimelineEntry, TimestampRange, UserInputEntry } from '../types/diaryTypes'; import { LabelOption, LabelOptions, MultilabelKey } from '../types/labelTypes'; import { EnketoUserInputEntry } from '../survey/enketo/enketoHelper'; +import { VehicleIdentity } from '../types/appConfigTypes'; export type UserInputMap = { /* if the key here is 'SURVEY', we are in the ENKETO configuration, meaning the user input @@ -34,6 +35,7 @@ type ContextProps = { userInputFor: (tlEntry: TimelineEntry) => UserInputMap | undefined; notesFor: (tlEntry: TimelineEntry) => UserInputEntry[] | undefined; labelFor: (tlEntry: TimelineEntry, labelType: MultilabelKey) => LabelOption | undefined; + confirmedModeFor: (tlEntry: TimelineEntry) => VehicleIdentity | LabelOption | undefined; addUserInputToEntry: (oid: string, userInput: any, inputType: 'label' | 'note') => void; displayedEntries: TimelineEntry[] | null; filterInputs: LabelTabFilter[]; diff --git a/www/js/diary/cards/ModesIndicator.tsx b/www/js/diary/cards/ModesIndicator.tsx index bba65c107..4e68da4de 100644 --- a/www/js/diary/cards/ModesIndicator.tsx +++ b/www/js/diary/cards/ModesIndicator.tsx @@ -3,22 +3,22 @@ import { View, StyleSheet } from 'react-native'; import color from 'color'; import LabelTabContext from '../LabelTabContext'; import { logDebug } from '../../plugin/logger'; -import { getBaseModeByValue } from '../diaryHelper'; +import { getBaseModeByKey, getBaseModeByValue } from '../diaryHelper'; import { Text, Icon, useTheme } from 'react-native-paper'; import { useTranslation } from 'react-i18next'; const ModesIndicator = ({ trip, detectedModes }) => { const { t } = useTranslation(); - const { labelOptions, labelFor } = useContext(LabelTabContext); + const { labelOptions, labelFor, confirmedModeFor } = useContext(LabelTabContext); const { colors } = useTheme(); const indicatorBackgroundColor = color(colors.onPrimary).alpha(0.8).rgb().string(); let indicatorBorderColor = color('black').alpha(0.5).rgb().string(); let modeViews; - const labeledModeForTrip = labelFor(trip, 'MODE'); - if (labelOptions && labeledModeForTrip?.value) { - const baseMode = getBaseModeByValue(labeledModeForTrip.value, labelOptions); + const confirmedModeForTrip = confirmedModeFor(trip); + if (labelOptions && confirmedModeForTrip?.value) { + const baseMode = getBaseModeByKey(confirmedModeForTrip.baseMode); indicatorBorderColor = baseMode.color; logDebug(`TripCard: got baseMode = ${JSON.stringify(baseMode)}`); modeViews = ( @@ -32,7 +32,7 @@ const ModesIndicator = ({ trip, detectedModes }) => { fontWeight: '500', textDecorationLine: 'underline', }}> - {labelFor(trip, 'MODE')?.text} + {confirmedModeForTrip.text} ); diff --git a/www/js/diary/cards/TripCard.tsx b/www/js/diary/cards/TripCard.tsx index 3504bde16..f0f8a1284 100644 --- a/www/js/diary/cards/TripCard.tsx +++ b/www/js/diary/cards/TripCard.tsx @@ -42,9 +42,9 @@ const TripCard = ({ trip, isFirstInList }: Props) => { } = useDerivedProperties(trip); let [tripStartDisplayName, tripEndDisplayName] = useAddressNames(trip); const navigation = useNavigation(); - const { labelOptions, labelFor, notesFor } = useContext(LabelTabContext); + const { labelOptions, confirmedModeFor, notesFor } = useContext(LabelTabContext); const tripGeojson = - trip && labelOptions && useGeojsonForTrip(trip, labelOptions, labelFor(trip, 'MODE')?.value); + trip && labelOptions && useGeojsonForTrip(trip, confirmedModeFor(trip)?.baseMode); const isDraft = trip.key.includes('UNPROCESSED'); const flavoredTheme = getTheme(isDraft ? 'draft' : undefined); diff --git a/www/js/diary/details/LabelDetailsScreen.tsx b/www/js/diary/details/LabelDetailsScreen.tsx index 4985300cb..9627ebcaa 100644 --- a/www/js/diary/details/LabelDetailsScreen.tsx +++ b/www/js/diary/details/LabelDetailsScreen.tsx @@ -31,7 +31,7 @@ import { CompositeTrip } from '../../types/diaryTypes'; import NavBar from '../../components/NavBar'; const LabelScreenDetails = ({ route, navigation }) => { - const { timelineMap, labelOptions, labelFor } = useContext(LabelTabContext); + const { timelineMap, labelOptions, confirmedModeFor } = useContext(LabelTabContext); const { t } = useTranslation(); const { height: windowHeight } = useWindowDimensions(); const appConfig = useAppConfig(); @@ -41,17 +41,16 @@ const LabelScreenDetails = ({ route, navigation }) => { const { displayDate, displayStartTime, displayEndTime } = useDerivedProperties(trip); const [tripStartDisplayName, tripEndDisplayName] = useAddressNames(trip); - const [modesShown, setModesShown] = useState<'labeled' | 'detected'>(() => + const [modesShown, setModesShown] = useState<'confirmed' | 'detected'>(() => // if trip has a labeled mode, initial state shows that; otherwise, show detected modes - trip && labelFor(trip, 'MODE')?.value ? 'labeled' : 'detected', + trip && confirmedModeFor(trip)?.value ? 'confirmed' : 'detected', ); const tripGeojson = trip && labelOptions && useGeojsonForTrip( trip, - labelOptions, - modesShown == 'labeled' ? labelFor(trip, 'MODE')?.value : undefined, + modesShown == 'confirmed' ? confirmedModeFor(trip)?.baseMode : undefined, ); const mapOpts = { minZoom: 3, maxZoom: 17 }; @@ -86,23 +85,24 @@ const LabelScreenDetails = ({ route, navigation }) => { )} - - {/* Full-size Leaflet map, with zoom controls */} - + {tripGeojson && ( + // Full-size Leaflet map, with zoom controls + + )} {/* If trip is labeled, show a toggle to switch between "Labeled Mode" and "Detected Modes" otherwise, just show "Detected" */} - {trip && labelFor(trip, 'MODE')?.value ? ( + {trip && confirmedModeFor(trip)?.value ? ( setModesShown(v)} + onValueChange={(v: 'confirmed' | 'detected') => setModesShown(v)} value={modesShown} density="medium" buttons={[ - { label: t('diary.labeled-mode'), value: 'labeled' }, + { label: t('diary.labeled-mode'), value: 'confirmed' }, { label: t('diary.detected-modes'), value: 'detected' }, ]} /> @@ -118,7 +118,7 @@ const LabelScreenDetails = ({ route, navigation }) => { )} {/* section-by-section breakdown of duration, distance, and mode */} - + {/* Overall trip duration, distance, and modes. Only show this when multiple sections are shown, and we are showing detected modes. If we just showed the labeled mode or a single section, this would be redundant. */} diff --git a/www/js/diary/details/TripSectionsDescriptives.tsx b/www/js/diary/details/TripSectionsDescriptives.tsx index fdab61eb3..9e117021c 100644 --- a/www/js/diary/details/TripSectionsDescriptives.tsx +++ b/www/js/diary/details/TripSectionsDescriptives.tsx @@ -5,8 +5,8 @@ import useDerivedProperties from '../useDerivedProperties'; import { getBaseModeByKey, getBaseModeByValue } from '../diaryHelper'; import LabelTabContext from '../LabelTabContext'; -const TripSectionsDescriptives = ({ trip, showLabeledMode = false }) => { - const { labelOptions, labelFor } = useContext(LabelTabContext); +const TripSectionsDescriptives = ({ trip, showConfirmedMode = false }) => { + const { labelOptions, labelFor, confirmedModeFor } = useContext(LabelTabContext); const { displayStartTime, displayTime, @@ -17,14 +17,14 @@ const TripSectionsDescriptives = ({ trip, showLabeledMode = false }) => { const { colors } = useTheme(); - const labeledModeForTrip = labelFor(trip, 'MODE'); + const confirmedModeForTrip = confirmedModeFor(trip); let sections = formattedSectionProperties; /* if we're only showing the labeled mode, or there are no sections (i.e. unprocessed trip), we treat this as unimodal and use trip-level attributes to construct a single section */ - if ((showLabeledMode && labeledModeForTrip) || !trip.sections?.length) { + if ((showConfirmedMode && confirmedModeForTrip) || !trip.sections?.length) { let baseMode; - if (showLabeledMode && labelOptions && labeledModeForTrip) { - baseMode = getBaseModeByValue(labeledModeForTrip.value, labelOptions); + if (showConfirmedMode && labelOptions && confirmedModeForTrip) { + baseMode = getBaseModeByKey(confirmedModeForTrip.baseMode); } else { baseMode = getBaseModeByKey('UNPROCESSED'); } @@ -62,9 +62,9 @@ const TripSectionsDescriptives = ({ trip, showLabeledMode = false }) => { - {showLabeledMode && labeledModeForTrip && ( + {showConfirmedMode && confirmedModeForTrip && ( - {labeledModeForTrip.text} + {confirmedModeForTrip.text} )} diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index f64318731..b0b65b2c8 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -27,19 +27,14 @@ const cachedGeojsons: Map = new Map(); /** * @description Gets a formatted GeoJSON object for a trip, including the start and end places and the trajectory. */ -export function useGeojsonForTrip( - trip: CompositeTrip, - labelOptions: LabelOptions, - labeledMode?: string, -) { +export function useGeojsonForTrip(trip: CompositeTrip, baseMode?: string) { if (!trip?._id?.$oid) return; - const gjKey = `trip-${trip._id.$oid}-${labeledMode || 'detected'}`; + const gjKey = `trip-${trip._id.$oid}-${baseMode || 'detected'}`; if (cachedGeojsons.has(gjKey)) { return cachedGeojsons.get(gjKey); } - const trajectoryColor = - (labeledMode && getBaseModeByValue(labeledMode, labelOptions)?.color) || undefined; + const trajectoryColor = (baseMode && getBaseModeByKey(baseMode)?.color) || undefined; logDebug("Reading trip's " + trip.locations.length + ' location points at ' + new Date()); const features = [ From fbedee41cc0f6933980edf50361c949f6c5dbd9c Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Sun, 14 Apr 2024 00:22:30 -0400 Subject: [PATCH 09/22] during bluetooth_ble matching, convert major and minor to hexadecimal The background/bluetooth_ble data type has "major" and "minor" as decimal integers. (https://github.com/e-mission/e-mission-docs/issues/1062#issuecomment-2052278631) 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.) --- www/js/survey/inputMatcher.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/www/js/survey/inputMatcher.ts b/www/js/survey/inputMatcher.ts index fdfb25579..a02f8ff76 100644 --- a/www/js/survey/inputMatcher.ts +++ b/www/js/survey/inputMatcher.ts @@ -379,6 +379,19 @@ function getBleScansForTimelineEntry( return bleScans.filter((scan) => validBleScanForTimelineEntry(tlEntry, scan)); } +/** + * @description Convert a decimal number to a hexadecimal string, with optional padding + * @example decimalToHex(245) => 'f5' + * @example decimalToHex(245, 4) => '00f5' + */ +function decimalToHex(d: string | number, padding?: number) { + let hex = Number(d).toString(16); + while (hex.length < (padding || 0)) { + hex = '0' + hex; + } + return hex; +} + export function mapBleScansToTimelineEntries(allEntries: TimelineEntry[], appConfig: AppConfig) { const timelineBleMap = {}; for (const tlEntry of allEntries) { @@ -390,7 +403,9 @@ export function mapBleScansToTimelineEntries(allEntries: TimelineEntry[], appCon // count the number of occurrences of each major:minor pair const majorMinorCounts = {}; matches.forEach((match) => { - const majorMinor = match.data.major + ':' + match.data.minor; + const major = decimalToHex(match.data.major, 4); + const minor = decimalToHex(match.data.minor, 4); + const majorMinor = major + ':' + minor; majorMinorCounts[majorMinor] = majorMinorCounts[majorMinor] ? majorMinorCounts[majorMinor] + 1 : 1; From 3fd28632b2907ce5b7ff62c27480e631a84f0e92 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Sun, 14 Apr 2024 17:53:31 -0400 Subject: [PATCH 10/22] add "Refresh App Configuration" row to Profile tab This feature allows the user to refresh the app configuration without having to log out and back in. (This will be handy for our alpha "run-through" of the fermata project, https://github.com/e-mission/nrel-openpath-deploy-configs/pull/89. we will likely need to update beacon + vehicle info periodically. dynamicConfig.ts -- in fetchConfig, pass an option to the fetch API so it does not use a cached copy of the config. We want to make sure it's actually checking for the latest config. -- export function loadNewConfig so it can be used in ProfileSettings.tsx ProfileSettings.tsx --add function refreshConfig, which calls loadNewConfig and triggers a hard refresh if the config has changed. Toast messages to guide the user through the process. --add the new row itself: "Refresh App Configuration" (which also shows the current version of the app config) appConfigTypes.ts --add prop 'version' to config type. Every config has this property. Testing done: On a local dev environment with locally hosted configs, I was signed into an nrel-commute opcode. I updated the local config file, changing "version" from 1 to 2 and changing "use_imperial" from true to false. In the UI Profile tab, the new row showed "Current version: 1". After clicking the row, the app reloads and UI now shows 'km' instead of 'miles'. I went back to the Profile tab and the new row now shows "Current version: 2". Clicking the row a second time triggers a toast message saying "Already up to date!" --- www/i18n/en.json | 6 +++++- www/js/config/dynamicConfig.ts | 9 +++++---- www/js/control/ProfileSettings.tsx | 17 ++++++++++++++++- www/js/types/appConfigTypes.ts | 1 + 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/www/i18n/en.json b/www/i18n/en.json index ffbfb8ea8..9a8b6bb61 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -48,7 +48,11 @@ "reminders-time-of-day": "Time of Day for Reminders ({{time}})", "upcoming-notifications": "Upcoming Notifications", "dummy-notification": "Dummy Notification in 5 Seconds", - "log-out": "Log Out" + "log-out": "Log Out", + "refresh-app-config": "Refresh App Configuration", + "current-version": "Current version: {{version}}", + "refreshing-app-config": "Refreshing app configuration, please wait...", + "already-up-to-date": "Already up to date!" }, "general-settings": { diff --git a/www/js/config/dynamicConfig.ts b/www/js/config/dynamicConfig.ts index 9773a1ead..62e0186fb 100644 --- a/www/js/config/dynamicConfig.ts +++ b/www/js/config/dynamicConfig.ts @@ -134,16 +134,17 @@ async function readConfigFromServer(studyLabel: string) { */ async function fetchConfig(studyLabel: string, alreadyTriedLocal?: boolean) { logDebug('Received request to join ' + studyLabel); - const downloadURL = `https://raw.githubusercontent.com/e-mission/nrel-openpath-deploy-configs/main/configs/${studyLabel}.nrel-op.json`; + let downloadURL = `https://raw.githubusercontent.com/e-mission/nrel-openpath-deploy-configs/main/configs/${studyLabel}.nrel-op.json`; if (!__DEV__ || alreadyTriedLocal) { logDebug('Fetching config from github'); - const r = await fetch(downloadURL); + const r = await fetch(downloadURL, { cache: 'reload' }); if (!r.ok) throw new Error('Unable to fetch config from github'); return r.json(); // TODO: validate, make sure it has required fields } else { logDebug('Running in dev environment, checking for locally hosted config'); try { - const r = await fetch('http://localhost:9090/configs/' + studyLabel + '.nrel-op.json'); + downloadURL = `http://localhost:9090/configs/${studyLabel}.nrel-op.json`; + const r = await fetch(downloadURL, { cache: 'reload' }); if (!r.ok) throw new Error('Local config not found'); return r.json(); } catch (err) { @@ -227,7 +228,7 @@ function extractSubgroup(token: string, config: AppConfig): string | undefined { * @param existingVersion If the new config's version is the same, we won't update * @returns boolean representing whether the config was updated or not */ -function loadNewConfig(newToken: string, existingVersion?: number): Promise { +export function loadNewConfig(newToken: string, existingVersion?: number): Promise { const newStudyLabel = extractStudyName(newToken); return readConfigFromServer(newStudyLabel) .then((downloadedConfig) => { diff --git a/www/js/control/ProfileSettings.tsx b/www/js/control/ProfileSettings.tsx index f851a6651..ab381e594 100644 --- a/www/js/control/ProfileSettings.tsx +++ b/www/js/control/ProfileSettings.tsx @@ -26,7 +26,7 @@ import ControlCollectionHelper, { helperToggleLowAccuracy, forceTransition, } from './ControlCollectionHelper'; -import { resetDataAndRefresh } from '../config/dynamicConfig'; +import { loadNewConfig, resetDataAndRefresh } from '../config/dynamicConfig'; import { AppContext } from '../App'; import { shareQR } from '../components/QrCode'; import { storageClear } from '../plugin/storage'; @@ -307,6 +307,16 @@ const ProfileSettings = () => { }, 1500); } + async function refreshConfig() { + AlertManager.addMessage({ text: t('control.refreshing-app-config') }); + const updated = await loadNewConfig(authSettings.opcode, appConfig?.version); + if (updated) { + window.location.reload(); + } else { + AlertManager.addMessage({ text: t('control.already-up-to-date') }); + } + } + //Platform.OS returns "web" now, but could be used once it's fully a Native app //for now, use window.cordova.platformId @@ -434,6 +444,11 @@ const ProfileSettings = () => { textKey="control.email-log" iconName="email" action={() => sendEmail('loggerDB')}> + Date: Sun, 14 Apr 2024 19:09:48 -0400 Subject: [PATCH 11/22] allow android emulator to download locally hosted configs by using 10.0.2.2 instead of localhost --- www/js/config/dynamicConfig.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/www/js/config/dynamicConfig.ts b/www/js/config/dynamicConfig.ts index 62e0186fb..d9b9f3235 100644 --- a/www/js/config/dynamicConfig.ts +++ b/www/js/config/dynamicConfig.ts @@ -143,7 +143,11 @@ async function fetchConfig(studyLabel: string, alreadyTriedLocal?: boolean) { } else { logDebug('Running in dev environment, checking for locally hosted config'); try { - downloadURL = `http://localhost:9090/configs/${studyLabel}.nrel-op.json`; + if (window['cordova'].platformId == 'android') { + downloadURL = `http://10.0.2.2:9090/configs/${studyLabel}.nrel-op.json`; + } else { + downloadURL = `http://localhost:9090/configs/${studyLabel}.nrel-op.json`; + } const r = await fetch(downloadURL, { cache: 'reload' }); if (!r.ok) throw new Error('Local config not found'); return r.json(); From 933f9a4c2b76b419973b61f76d55f10b227f6086 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Sun, 14 Apr 2024 23:52:40 -0400 Subject: [PATCH 12/22] CSP allow 10.0.2.2 for android emulator --- www/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/index.html b/www/index.html index 696343b7a..6957a38d6 100644 --- a/www/index.html +++ b/www/index.html @@ -3,7 +3,7 @@ - + From fcaec5c88bd03ce8b99455c4b456a90658fa7d56 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Mon, 15 Apr 2024 00:29:04 -0400 Subject: [PATCH 13/22] fix typo: "background_ble" -> "bluetooth_ble" This would explain why I was never getting any entries returned from the unified loader --- www/js/diary/timelineHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index 7e874a595..d82adb4fb 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -182,7 +182,7 @@ export async function updateUnprocessedBleScans(queryRange: TimestampRange) { endTs: queryRange.end_ts, }; const getMethod = window['cordova'].plugins.BEMUserCache.getSensorDataForInterval; - getUnifiedDataForInterval('background/background_ble', tq, getMethod).then( + getUnifiedDataForInterval('background/bluetooth_ble', tq, getMethod).then( (bleScans: BEMData[]) => { logDebug(`Read ${bleScans.length} BLE scans`); unprocessedBleScans = bleScans; From b2d22b769cc3c4d8dbbbe6e7524761134f263c21 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Mon, 15 Apr 2024 13:13:53 -0400 Subject: [PATCH 14/22] revert "CSP allow 10.0.2.2 for android emulator" https://github.com/e-mission/e-mission-phone/pull/1145#discussion_r1565985396 --- www/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/index.html b/www/index.html index 6957a38d6..696343b7a 100644 --- a/www/index.html +++ b/www/index.html @@ -3,7 +3,7 @@ - + From 0b6b844d3942de7f6b2aeb6ea01cb262a0d78e24 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Mon, 15 Apr 2024 15:04:25 -0400 Subject: [PATCH 15/22] add "confirmedMode" to derived properties & use for conditional surveys For the fermata project, the survey shown for a trip should depend on what vehicle was detected from bluetooth. For unprocessed trips, this information is not going to be in the raw trip object. That's why we have "timelineBleMap", which is kept separate from the raw trip objects. But to access this while evaluating the conditional survey "showsIf" expressions, we'll need to include more things in the eval's scope. I decided that it would be appropriate to use our "derived properties" hook for this. we can add a new property to derived properties, then we can include all the derived properties in the scope in which the "showsIf" expressions are evaluated. This will probably be adjusted later. Note: I also simplified the type definition of DerivedProperties. It now just uses the return type of the useDerivedProperties hook. --- www/js/diary/useDerivedProperties.tsx | 7 +++++-- www/js/survey/enketo/UserInputButton.tsx | 4 +++- www/js/survey/enketo/conditionalSurveys.ts | 4 +++- www/js/types/diaryTypes.ts | 14 ++------------ 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/www/js/diary/useDerivedProperties.tsx b/www/js/diary/useDerivedProperties.tsx index fe324ee3f..a6985a8e5 100644 --- a/www/js/diary/useDerivedProperties.tsx +++ b/www/js/diary/useDerivedProperties.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import { useContext, useMemo } from 'react'; import { useImperialConfig } from '../config/useImperialConfig'; import { getFormattedDate, @@ -9,9 +9,11 @@ import { getDetectedModes, isMultiDay, } from './diaryHelper'; +import LabelTabContext from './LabelTabContext'; const useDerivedProperties = (tlEntry) => { const imperialConfig = useImperialConfig(); + const { confirmedModeFor } = useContext(LabelTabContext); return useMemo(() => { const beginFmt = tlEntry.start_fmt_time || tlEntry.enter_fmt_time; @@ -21,6 +23,7 @@ const useDerivedProperties = (tlEntry) => { const tlEntryIsMultiDay = isMultiDay(beginFmt, endFmt); return { + confirmedMode: confirmedModeFor(tlEntry), displayDate: getFormattedDate(beginFmt, endFmt), displayStartTime: getLocalTimeString(beginDt), displayEndTime: getLocalTimeString(endDt), @@ -32,7 +35,7 @@ const useDerivedProperties = (tlEntry) => { distanceSuffix: imperialConfig.distanceSuffix, detectedModes: getDetectedModes(tlEntry), }; - }, [tlEntry, imperialConfig]); + }, [tlEntry, imperialConfig, confirmedModeFor(tlEntry)]); }; export default useDerivedProperties; diff --git a/www/js/survey/enketo/UserInputButton.tsx b/www/js/survey/enketo/UserInputButton.tsx index 5817e7ed3..118b687f8 100644 --- a/www/js/survey/enketo/UserInputButton.tsx +++ b/www/js/survey/enketo/UserInputButton.tsx @@ -17,6 +17,7 @@ import EnketoModal from './EnketoModal'; import LabelTabContext from '../../diary/LabelTabContext'; import useAppConfig from '../../useAppConfig'; import { getSurveyForTimelineEntry } from './conditionalSurveys'; +import useDerivedProperties from '../../diary/useDerivedProperties'; type Props = { timelineEntry: any; @@ -29,6 +30,7 @@ const UserInputButton = ({ timelineEntry }: Props) => { const [prevSurveyResponse, setPrevSurveyResponse] = useState(undefined); const [modalVisible, setModalVisible] = useState(false); const { userInputFor, addUserInputToEntry } = useContext(LabelTabContext); + const derivedTripProps = useDerivedProperties(timelineEntry); // which survey will this button launch? const [surveyName, notFilledInLabel] = useMemo(() => { @@ -38,7 +40,7 @@ const UserInputButton = ({ timelineEntry }: Props) => { return ['TripConfirmSurvey', t('diary.choose-survey')]; } // config lists one or more surveys; find which one to use - const s = getSurveyForTimelineEntry(tripLabelConfig, timelineEntry); + const s = getSurveyForTimelineEntry(tripLabelConfig, timelineEntry, derivedTripProps); const lang = i18n.resolvedLanguage || 'en'; return [s?.surveyName, s?.['not-filled-in-label'][lang]]; }, [appConfig, timelineEntry, i18n.resolvedLanguage]); diff --git a/www/js/survey/enketo/conditionalSurveys.ts b/www/js/survey/enketo/conditionalSurveys.ts index 63f9a9b83..607b49431 100644 --- a/www/js/survey/enketo/conditionalSurveys.ts +++ b/www/js/survey/enketo/conditionalSurveys.ts @@ -1,6 +1,6 @@ import { displayError } from '../../plugin/logger'; import { SurveyButtonConfig } from '../../types/appConfigTypes'; -import { TimelineEntry } from '../../types/diaryTypes'; +import { DerivedProperties, TimelineEntry } from '../../types/diaryTypes'; import { Position } from 'geojson'; const conditionalSurveyFunctions = { @@ -31,6 +31,7 @@ const scopedEval = (script: string, scope: { [k: string]: any }) => export function getSurveyForTimelineEntry( tripLabelConfig: SurveyButtonConfig | SurveyButtonConfig[], tlEntry: TimelineEntry, + derivedProperties: DerivedProperties, ) { // if only one survey is given, just return it if (!(tripLabelConfig instanceof Array)) return tripLabelConfig; @@ -40,6 +41,7 @@ export function getSurveyForTimelineEntry( if (!surveyConfig.showsIf) return surveyConfig; // survey shows unconditionally const scope = { ...tlEntry, + ...derivedProperties, ...conditionalSurveyFunctions, }; try { diff --git a/www/js/types/diaryTypes.ts b/www/js/types/diaryTypes.ts index 1caaf18df..5280e7232 100644 --- a/www/js/types/diaryTypes.ts +++ b/www/js/types/diaryTypes.ts @@ -3,6 +3,7 @@ As much as possible, these types parallel the types used in the server code. */ import { BaseModeKey, MotionTypeKey } from '../diary/diaryHelper'; +import useDerivedProperties from '../diary/useDerivedProperties'; import { MultilabelKey } from './labelTypes'; import { BEMData, LocalDt } from './serverData'; import { FeatureCollection, Feature, Geometry, Point, Position } from 'geojson'; @@ -129,18 +130,7 @@ export type TimestampRange = { start_ts: number; end_ts: number }; /* These properties aren't received from the server, but are derived from the above properties. They are used in the UI to display trip/place details and are computed by the useDerivedProperties hook. */ -export type DerivedProperties = { - displayDate: string; - displayStartTime: string; - displayEndTime: string; - displayTime: string; - displayStartDateAbbr: string; - displayEndDateAbbr: string; - formattedDistance: string; - formattedSectionProperties: any[]; // TODO - distanceSuffix: string; - detectedModes: { mode: string; icon: string; color: string; pct: number | string }[]; -}; +export type DerivedProperties = ReturnType; export type SectionSummary = { count: { [k: MotionTypeKey | BaseModeKey]: number }; From 37fbb851ec9f7e9a989071855790b13a1907405b Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Mon, 15 Apr 2024 15:26:02 -0400 Subject: [PATCH 16/22] only use "ranging" scans for BLE matching When matching we basically are trying to find the major:minor pair that was sensed the most frequently during the trip. But should only consider RANGE_UPDATE events for this. The REGION_ENTER and REGION_EXIT events do not have major and minor defined, so we should exclude them for the purpose of matching. Add a filter condition for this. Also updated function + variable names to make it clearer that we are only considering 'ranging' scans. Note that before processing, the value of eventType is a string ('RANGE_UPDATE'). But the server changes this to an enum value where 'RANGE_UPDATE' -> 2. So we have to check for both. --- www/js/survey/inputMatcher.ts | 22 +++++++++++++++------- www/js/types/diaryTypes.ts | 2 +- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/www/js/survey/inputMatcher.ts b/www/js/survey/inputMatcher.ts index a02f8ff76..b1460194e 100644 --- a/www/js/survey/inputMatcher.ts +++ b/www/js/survey/inputMatcher.ts @@ -372,11 +372,19 @@ function validBleScanForTimelineEntry(tlEntry: TimelineEntry, bleScan: BEMData= entryStart && bleScan.data.ts <= entryEnd; } -function getBleScansForTimelineEntry( +/** + * @description Get BLE scans that are of type RANGE_UPDATE and are within the time range of the timeline entry + */ +function getBleRangingScansForTimelineEntry( tlEntry: TimelineEntry, bleScans: BEMData[], ) { - return bleScans.filter((scan) => validBleScanForTimelineEntry(tlEntry, scan)); + return bleScans.filter( + (scan) => + /* RANGE_UPDATE is the string value, but the server uses an enum, so once processed it becomes 2 */ + (scan.data.eventType == 'RANGE_UPDATE' || scan.data.eventType == 2) && + validBleScanForTimelineEntry(tlEntry, scan), + ); } /** @@ -395,16 +403,16 @@ function decimalToHex(d: string | number, padding?: number) { export function mapBleScansToTimelineEntries(allEntries: TimelineEntry[], appConfig: AppConfig) { const timelineBleMap = {}; for (const tlEntry of allEntries) { - const matches = getBleScansForTimelineEntry(tlEntry, unprocessedBleScans); - if (!matches.length) { + const rangingScans = getBleRangingScansForTimelineEntry(tlEntry, unprocessedBleScans); + if (!rangingScans.length) { continue; } // count the number of occurrences of each major:minor pair const majorMinorCounts = {}; - matches.forEach((match) => { - const major = decimalToHex(match.data.major, 4); - const minor = decimalToHex(match.data.minor, 4); + rangingScans.forEach((scan) => { + const major = decimalToHex(scan.data.major, 4); + const minor = decimalToHex(scan.data.minor, 4); const majorMinor = major + ':' + minor; majorMinorCounts[majorMinor] = majorMinorCounts[majorMinor] ? majorMinorCounts[majorMinor] + 1 diff --git a/www/js/types/diaryTypes.ts b/www/js/types/diaryTypes.ts index 5280e7232..9757e95cf 100644 --- a/www/js/types/diaryTypes.ts +++ b/www/js/types/diaryTypes.ts @@ -168,7 +168,7 @@ export type UserInputEntry = { export type BluetoothBleData = { ts: number; - eventType: 'REGION_ENTER' | 'REGION_EXIT' | 'RANGE_UPDATE'; + eventType: 'REGION_ENTER' | 'REGION_EXIT' | 'RANGE_UPDATE' | number; uuid: string; major: number; // for our use case, missing for REGION_ENTER or REGION_EXIT minor: number; // for our use case, missing for REGION_ENTER or REGION_EXIT From 8cc6a7e3debf882e4a6861087c7568eb84097ff6 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 17 Apr 2024 00:58:06 -0400 Subject: [PATCH 17/22] UserInputButton: before appConfig is defined, don't show default survey --- www/js/survey/enketo/UserInputButton.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/www/js/survey/enketo/UserInputButton.tsx b/www/js/survey/enketo/UserInputButton.tsx index 118b687f8..e3e629bda 100644 --- a/www/js/survey/enketo/UserInputButton.tsx +++ b/www/js/survey/enketo/UserInputButton.tsx @@ -34,6 +34,7 @@ const UserInputButton = ({ timelineEntry }: Props) => { // which survey will this button launch? const [surveyName, notFilledInLabel] = useMemo(() => { + if (!appConfig) return []; // no config loaded yet; show blank for now const tripLabelConfig = appConfig?.survey_info?.buttons?.['trip-label']; if (!tripLabelConfig) { // config doesn't specify; use default From 03673a40a511136cc07d8092c701804f21bb0905 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 17 Apr 2024 01:02:10 -0400 Subject: [PATCH 18/22] in updateUnprocessedBleScans, await call to getUnifiedDataForInterval Without the 'await' keyword here, execution would continue without the request being completed. This would cause the BLE scans to sometimes not be considered while rendering and evaluating conditional surveys. If the BLE scans took longer to load than the trips took to render, there would be no surveys prompted. Thus, we must 'await' to make sure the BLE scans are loaded first. --- www/js/diary/timelineHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index d82adb4fb..6e82c0fbf 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -182,7 +182,7 @@ export async function updateUnprocessedBleScans(queryRange: TimestampRange) { endTs: queryRange.end_ts, }; const getMethod = window['cordova'].plugins.BEMUserCache.getSensorDataForInterval; - getUnifiedDataForInterval('background/bluetooth_ble', tq, getMethod).then( + await getUnifiedDataForInterval('background/bluetooth_ble', tq, getMethod).then( (bleScans: BEMData[]) => { logDebug(`Read ${bleScans.length} BLE scans`); unprocessedBleScans = bleScans; From d83f4d4a28744dc33a71f86b94014e32cd2c1219 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 17 Apr 2024 01:21:17 -0400 Subject: [PATCH 19/22] cacheResourcesFromConfig: reload the cache when config is downloaded We recently added the ability to refresh the config from inside the app. But whenever the config is downloaded for the first time, we cache the resources referenced in it by URL. Subsequent config downloads would still use the resources from the first time. fetchUrlCached now accepts options to pass through to the fetch API; if cache is 'reload', we will skip checking our localStorage cache for a previously stored value. This option will also cause the fetch API will also skip its own internal cache The result of this is that when we refresh the config, URL-referenced resources inside it will also be refreshed. --- www/js/config/dynamicConfig.ts | 4 ++-- www/js/services/commHelper.ts | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/www/js/config/dynamicConfig.ts b/www/js/config/dynamicConfig.ts index d9b9f3235..5843af3d2 100644 --- a/www/js/config/dynamicConfig.ts +++ b/www/js/config/dynamicConfig.ts @@ -92,11 +92,11 @@ function cacheResourcesFromConfig(config: AppConfig) { if (config.survey_info?.surveys) { Object.values(config.survey_info.surveys).forEach((survey) => { if (!survey?.['formPath']) throw new Error(i18next.t('config.survey-missing-formpath')); - fetchUrlCached(survey['formPath']); + fetchUrlCached(survey['formPath'], { cache: 'reload' }); }); } if (config.label_options) { - fetchUrlCached(config.label_options); + fetchUrlCached(config.label_options, { cache: 'reload' }); } } diff --git a/www/js/services/commHelper.ts b/www/js/services/commHelper.ts index 26dce8056..ec2ee9d97 100644 --- a/www/js/services/commHelper.ts +++ b/www/js/services/commHelper.ts @@ -5,17 +5,18 @@ import { TimestampRange } from '../types/diaryTypes'; /** * @param url URL endpoint for the request + * @param fetchOpts (optional) options for the fetch request. If 'cache' is set to 'reload', the cache will be ignored * @returns Promise of the fetched response (as text) or cached text from local storage */ -export async function fetchUrlCached(url) { +export async function fetchUrlCached(url: string, fetchOpts?: RequestInit) { const stored = localStorage.getItem(url); - if (stored) { + if (stored && fetchOpts?.cache != 'reload') { logDebug(`fetchUrlCached: found cached data for url ${url}, returning`); return Promise.resolve(stored); } try { - logDebug(`fetchUrlCached: found no cached data for url ${url}, fetching`); - const response = await fetch(url); + logDebug(`fetchUrlCached: cache had ${stored} for url ${url}, not using; fetching`); + const response = await fetch(url, fetchOpts); const text = await response.text(); localStorage.setItem(url, text); logDebug(`fetchUrlCached: fetched data for url ${url}, returning`); From f0c85f2a393df3af5abec54c411cff179fd95997 Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Mon, 22 Apr 2024 13:02:20 -0700 Subject: [PATCH 20/22] codecov-action version update v3 -> v4 --- .github/workflows/code-coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index b0e94db22..caf0fc7bc 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -27,7 +27,7 @@ jobs: npx jest - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: files: ./coverage/coverage-final.json flags: unit From 22cbf2f506af10deaae9cc9ece4381a71416de6a Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Mon, 22 Apr 2024 13:13:41 -0700 Subject: [PATCH 21/22] add CODECOV_TOKEN as required for v4 --- .github/workflows/code-coverage.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index caf0fc7bc..dc1af47ac 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -29,6 +29,7 @@ jobs: - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v4 with: + token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage/coverage-final.json flags: unit fail_ci_if_error: ${{ github.repository == 'e-mission/e-mission-phone' }} From 0f6aabcc18255302a1c2bcf40b2a4cde15fb67d4 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Sun, 28 Apr 2024 13:35:30 -0400 Subject: [PATCH 22/22] Lock React version to ~18.2.0 (#1148) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * lock React version to ~18.2.0 React 19 and React 18.3 are out (https://react.dev/blog/2024/04/25/react-19-upgrade-guide#react-18-3), but the React Native ecosystem is still on React 18.2. We need to lock the versions down until the React Native packages update. * 💩 Temporarily pin the version to 14.1 so that the CI is successful Consisent with https://github.com/e-mission/e-mission-phone/pull/1148#issuecomment-2079688203 * 💩 Revert back even further to xcode 14 Consistent with https://github.com/e-mission/e-mission-phone/pull/1148#issuecomment-2080155690 * 📌 Ensure that we are using the correct version of java Before this, we set only the `JAVA_HOME` to pin the java version This was working until a couple of days ago, when, even after setting the version, the java version was not changed. This may be due to the location for the java executable changing, so let's try to set the PATH as well Also print out several environment variables to help debug further Related info: https://github.com/e-mission/e-mission-phone/pull/1148#issuecomment-2079702274 https://github.com/e-mission/e-mission-phone/pull/1148#issuecomment-2080155690 * 🔧 Change the environment variable to the one used in the new github actions runner Related context: https://github.com/e-mission/e-mission-phone/pull/1148#issuecomment-2081318853 * 📌 Pin the mac version to 13 To be consistent with https://github.com/e-mission/e-mission-phone/pull/1148#issuecomment-2081318853 Before we fix https://github.com/e-mission/e-mission-docs/issues/1060 * 📌 Pin the mac version to 13 Consistent with https://github.com/e-mission/e-mission-phone/pull/1148#issuecomment-2081323518 and https://github.com/e-mission/e-mission-phone/pull/1148/commits/b5072f351e7954d0ffef9d7d55a5751b225813e2 (for iOS) * ⏪️ Revert previous attempts to fix the issue since pinning to OSX-13 fixed it https://github.com/e-mission/e-mission-phone/pull/1148#issuecomment-2081331221 * 📌 Pin the xcode version again Because apparently the default version, even on the OSX 13 runner, is now xcode 15 https://github.com/e-mission/e-mission-phone/pull/1148#issuecomment-2081536981 * Ensure that the switch command is run as root --------- Co-authored-by: K. Shankari --- .github/workflows/android-build.yml | 6 +++++- .github/workflows/ios-build.yml | 8 +++++++- package.cordovabuild.json | 6 +++--- package.serve.json | 7 ++++--- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 25eb65317..90c75d147 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -22,13 +22,17 @@ jobs: # This workflow contains a single job called "build" build: # The type of runner that the job will run on - runs-on: macos-latest + runs-on: macos-13 # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v2 + # Runs a single command using the runners shell + - name: Prints related environment variables so that we can know what to set + run: env | egrep "JAVA|PATH|ANDROID" + # Runs a single command using the runners shell - name: Print the java and gradle versions run: | diff --git a/.github/workflows/ios-build.yml b/.github/workflows/ios-build.yml index ad0ce2f01..ac6bb76f3 100644 --- a/.github/workflows/ios-build.yml +++ b/.github/workflows/ios-build.yml @@ -22,7 +22,7 @@ jobs: # This workflow contains a single job called "build" build: # The type of runner that the job will run on - runs-on: macos-latest + runs-on: macos-13 # Steps represent a sequence of tasks that will be executed as part of the job steps: @@ -33,6 +33,12 @@ jobs: - name: Print the xcode path run: xcode-select --print-path + - name: Set xcode to 14.1 + run: | + sudo xcode-select --switch /Applications/Xcode_14.1.app + echo "After setting xcode version "`xcode-select --print-path` + + - name: Print the xcode setup run: xcodebuild -version -sdk diff --git a/package.cordovabuild.json b/package.cordovabuild.json index c3f7ec8d3..85ebd6eb9 100644 --- a/package.cordovabuild.json +++ b/package.cordovabuild.json @@ -26,7 +26,7 @@ "@babel/preset-typescript": "^7.21.4", "@ionic/cli": "6.20.8", "@types/luxon": "^3.3.0", - "@types/react": "^18.2.20", + "@types/react": "~18.2.0", "babel-loader": "^9.1.2", "babel-plugin-optional-require": "^0.3.1", "concurrently": "^8.0.1", @@ -152,9 +152,9 @@ "npm": "^9.6.3", "phonegap-plugin-barcodescanner": "git+https://github.com/phonegap/phonegap-plugin-barcodescanner#v8.1.0", "prop-types": "^15.8.1", - "react": "^18.2.*", + "react": "~18.2.0", "react-chartjs-2": "^5.2.0", - "react-dom": "^18.2.*", + "react-dom": "~18.2.0", "react-i18next": "^13.5.0", "react-native-paper": "^5.11.0", "react-native-paper-dates": "^0.18.12", diff --git a/package.serve.json b/package.serve.json index dceeb2267..f6f5c2ae3 100644 --- a/package.serve.json +++ b/package.serve.json @@ -26,7 +26,7 @@ "@ionic/cli": "6.20.8", "@testing-library/react-native": "^12.3.0", "@types/luxon": "^3.3.0", - "@types/react": "^18.2.20", + "@types/react": "~18.2.0", "babel-jest": "^29.7.0", "babel-loader": "^9.1.2", "babel-plugin-optional-require": "^0.3.1", @@ -40,6 +40,7 @@ "jest-environment-jsdom": "^29.7.0", "phonegap": "9.0.0+cordova.9.0.0", "process": "^0.11.10", + "react-test-renderer": "~18.2.0", "sass": "^1.62.1", "sass-loader": "^13.3.1", "style-loader": "^3.3.3", @@ -76,9 +77,9 @@ "luxon": "^3.3.0", "npm": "^9.6.3", "prop-types": "^15.8.1", - "react": "^18.2.*", + "react": "~18.2.0", "react-chartjs-2": "^5.2.0", - "react-dom": "^18.2.*", + "react-dom": "~18.2.0", "react-i18next": "^13.5.0", "react-native-paper": "^5.11.0", "react-native-paper-dates": "^0.18.12",