diff --git a/.gitignore b/.gitignore index 4c2bc9c..afd16d0 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,4 @@ crowdinstats.csv package-lock.json .DS_Store build.sh +local_scripts/ diff --git a/app/pxt.json b/app/pxt.json index e8ca1c3..09983c4 100644 --- a/app/pxt.json +++ b/app/pxt.json @@ -1,6 +1,6 @@ { "name": "MicroData App", - "version": "1.7.5", + "version": "1.9.3", "dependencies": { "core": "*", "radio": "*", diff --git a/assets.ts b/assets.ts index 28e2f3c..293417e 100644 --- a/assets.ts +++ b/assets.ts @@ -1,25 +1,103 @@ namespace microdata { - let extraImage: Bitmap = null + let extraImage: Bitmap | undefined; //% shim=TD_NOOP function extraSamples(name: string) { } - export class Icons { - public static get(name: string | number, nullIfMissing = false): Bitmap { - if (name == "microdataLogo") return microdataLogo - if (typeof name === "string") - return user_interface_base.icons.get(name, nullIfMissing) - return MISSING + export class AppAssets implements ui.UiAssetResolver { + private icons: ui.UiMapAssets + + constructor() { + this.icons = new ui.UiMapAssets({ + microdataLogo: microdataLogo, + microbitLogo: ui.microbitLogo, + sensor_logging_setup_screen_background: sensor_logging_setup_screen_background, + btn_plus: btn_plus + }, bmp`.`) + } + + public getBitmap( + id: string | number, + nullIfMissing?: boolean, + ): Bitmap | undefined { + if (id == "start") { + return bmp` + . 7 . + 7 7 7 + . 7 . + ` + } + return this.icons.getBitmap(id, nullIfMissing) + } + + public getText(id: string): string { + if (id == "startLabel") return "Start" + return "" } } + /** + * NOTE: Need to add the correct Jacdac logo + */ + export function sensorNameToBitmap(name: string, isJacdacSensor?: boolean): Bitmap | undefined { + if (isJacdacSensor) return ui.microbitLogoWhiteBackground + + switch (name) { + case "Accel. X": { + return ui.accelerometer; + } + case "Accel. Y": { + return ui.accelerometer; + } + case "Accel. Z": { + return ui.accelerometer; + } + case "Pitch": { + return ui.car_right_turn; + } + case "Roll": { + return ui.car_right_spin; + } + case "A. Pin 0": { + return ui.pin_0; + } + case "A. Pin 1": { + return ui.pin_1; + } + case "A. Pin 2": { + return ui.pin_2; + } + case "Light": { + return ui.led_light_sensor; + } + case "Temperature": { + return ui.thermometer; + } + case "Magnetometer": { + return ui.magnet; + } + case "Logo Press": { + return ui.finger_press; + } + case "Microphone": { + return ui.microphone; + } + case "Compass": { + return ui.compass; + } + } + + return undefined; + } + export const microdataLogo = bmp` ....111111.......111111...1111................................................11111111111.............................................................. ...11bbbbbb.....11bbbbbb.11bbbb...............................................1bbbbbbbbbbff.......................111.................................. ...1bbbbbbbb...11bbbbbbbf1bbbbbf..............................................1bbbbbbbbbbbff.....................1bbbb................................. + ...1bbbbbbbbb.11bbbbbbbbf1bbbbbf..............................................1bbbbbbbbbbbbff...................1bbbbbf................................ ...1bbbbbbbbbb1bbbbbbbbbf1bbbbbf..............................................1bbbbfffbbbbbbf...................1bbbbbf................................ ...1bbbbbbbbbbbbbbbbbbbbf.bbbbff..............................................1bbbf....bbbbbff..................1bbbbbf................................ @@ -39,25 +117,147 @@ namespace microdata { ...1bbbbbbf......1bbbbbbf1bbbbbf..bbbbbbbbbbbff.1bbbbbf.........bbbbbbbbbbbff.bbbbbbbbbbbbbff..fbbbbbbbbbbbbbbb..fbbbbbbbbbbbbf..fbbbbbbbbbbbbbbbb..... ....bbbbbff.......bbbbbff.bbbbff...fbbbbbbbfff...bbbbff..........fbbbbbbbfff...fbbbbbbbbbff.....fbbbbbbbbbbbbff...fbbbbbbbbbbf....fbbbbbbbbbbbbbff..... .....fffff.........fffff...ffff......fffffff......ffff.............fffffff......fffffffff........fffffffffffff.....ffffffffff......ffffffffffffff...... -` - - //TODO: Move into user_interface_base/coreAssets.ts - export const MISSING = bmp` - . . . . . . . . . . . . . . . . - . . . . . . . . . . . . . . . . - . . . . . . . . . . . . . . . . - . . . 2 2 2 2 2 2 2 2 2 2 . . . - . . . 2 2 . . . . . . 2 2 . . . - . . . 2 . 2 . . . . 2 . 2 . . . - . . . 2 . . 2 . . 2 . . 2 . . . - . . . 2 . . . 2 2 . . . 2 . . . - . . . 2 . . . 2 2 . . . 2 . . . - . . . 2 . . 2 . . 2 . . 2 . . . - . . . 2 . 2 . . . . 2 . 2 . . . - . . . 2 2 . . . . . . 2 2 . . . - . . . 2 2 2 2 2 2 2 2 2 2 . . . - . . . . . . . . . . . . . . . . - . . . . . . . . . . . . . . . . - . . . . . . . . . . . . . . . . -` -} \ No newline at end of file + ` + + export const btn_plus = bmp` + . . . . . . . . . . . . . . . . + . . . . . . . . . . . . . . . . + . . . . . . . . . . . . . . . . + . . . . . . f f f f . . . . . . + . . . . . . f 5 5 f . . . . . . + . . . . . . f 5 5 f . . . . . . + . . . f f f f 5 5 f f f f . . . + . . . f 5 5 5 5 5 5 5 5 f . . . + . . . f 5 5 5 5 5 5 5 5 f . . . + . . . f f f f 5 5 f f f f . . . + . . . . . . f 5 5 f . . . . . . + . . . . . . f 5 5 f . . . . . . + . . . . . . f f f f . . . . . . + . . . . . . . . . . . . . . . . + . . . . . . . . . . . . . . . . + . . . . . . . . . . . . . . . . + ` + + export const sensor_logging_setup_screen_background = bmp` + ffff88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888ffff + ff888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888ff + f88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888f + f88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888f + 8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888866666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666688888 + 8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 + 8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 + f88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888f + f88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888f + ff888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888ff + ffff88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888ffff + ` +} diff --git a/dataRecorder.ts b/dataRecorder.ts index cf492cf..63ae05e 100644 --- a/dataRecorder.ts +++ b/dataRecorder.ts @@ -1,322 +1,512 @@ namespace microdata { - import AppInterface = user_interface_base.AppInterface - import Screen = user_interface_base.Screen - import Scene = user_interface_base.Scene - import Sprite = user_interface_base.Sprite - import Affine = user_interface_base.Affine - import font = user_interface_base.font - - /** Number of sensor information boxes that can fit onto the screen at once*/ - const MAX_SENSORS_ON_SCREEN: number = 5 - /** The colours that will be used for the lines & sensor information boxes */ - const SENSOR_BOX_COLORS: number[] = [2, 3, 4, 6, 7, 9] - /** The colours that will be used for writing the information about the sensor. */ - const SENSOR_BOX_TEXT_COLORS: number[] = [1, 1, 1, 1, 15, 15] - - /** - * Responsible for invoking the logging commands for each sensor, - * Presents information about each sensor's state via colourful collapsing boxes - * - * Sensors are logged via a scheduler - */ - export class DataRecorder extends Scene { - /** */ - private scheduler: SensorScheduler; - /** For displaying their status on the screen and passing to the scheduler. */ - private sensors: Sensor[] - /** For faster looping, modulo calculation when pressing UP or DOWN */ - private numberOfSensors: number; - /** Sensor to be shown */ - private currentSensorIndex: number; - /** Last sensor on the screen */ - private sensorIndexOffset: number; - /** For the currentSensorIndex */ - private sensorBoxColor: number; - - private showCancelRecordingScreen: boolean; - private currentlyCancelling: boolean - private yesBtn: Sprite // currentBtn = 0 - private noBtn: Sprite // currentBtn = 1 - - constructor(app: AppInterface, sensors: Sensor[]) { - super(app, "dataRecorder") - - this.scheduler = new SensorScheduler(sensors) - this.sensors = sensors - this.numberOfSensors = sensors.length - - this.sensorIndexOffset = 0 - this.currentSensorIndex = 0 - this.sensorBoxColor = 15 - this.showCancelRecordingScreen = false; - this.currentlyCancelling = false; - - //--------------- - // User Controls: - //--------------- - - // Go Back: - context.onEvent( - ControllerButtonEvent.Pressed, - controller.B.id, - () => { - if (this.scheduler.loggingComplete()) { - this.app.popScene() - this.app.pushScene(new Home(this.app)) - } - - else { - this.showCancelRecordingScreen = !this.showCancelRecordingScreen - } - } - ) - - // Clear whatever A was previously bound to - context.onEvent( - ControllerButtonEvent.Pressed, - controller.A.id, - () => { - if (this.showCancelRecordingScreen) { - this.currentlyCancelling = true - this.scheduler.stop() - - basic.pause(1000) - this.app.popScene() - this.app.pushScene(new Home(this.app)) - } - } - ) - - // Scroll Up - context.onEvent( - ControllerButtonEvent.Pressed, - controller.up.id, - () => { - this.currentSensorIndex = Math.max(0, this.currentSensorIndex - 1) - - if (this.sensorIndexOffset > 0) - this.sensorIndexOffset = Math.max(0, this.sensorIndexOffset - 1) - - this.update() - } - ) - - // Scroll Down - context.onEvent( - ControllerButtonEvent.Pressed, - controller.down.id, - () => { - this.currentSensorIndex = Math.min(this.currentSensorIndex + 1, this.numberOfSensors - 1) - - if (this.currentSensorIndex > 4) - this.sensorIndexOffset = Math.min(this.sensorIndexOffset + 1, this.numberOfSensors - 5) - - this.update() - } - ) - - - // For cancelling the current recording: - - this.yesBtn = new Sprite({ img: Icons.get("tile_button_a") }) - this.yesBtn.bindXfrm(new Affine()) - this.yesBtn.xfrm.parent = new Affine() - this.yesBtn.xfrm.worldPos.x = Screen.HALF_WIDTH - this.yesBtn.xfrm.worldPos.y = Screen.HALF_HEIGHT - this.yesBtn.xfrm.localPos.x = -40 - this.yesBtn.xfrm.localPos.y = 12 - - this.noBtn = new Sprite({ img: Icons.get("tile_button_b") }) - this.noBtn.bindXfrm(new Affine()) - this.noBtn.xfrm.parent = new Affine() - this.noBtn.xfrm.worldPos.x = Screen.HALF_WIDTH - this.noBtn.xfrm.worldPos.y = Screen.HALF_HEIGHT - this.noBtn.xfrm.localPos.x = 40 - this.noBtn.xfrm.localPos.y = 12 - - this.log() + /** Number of sensor information boxes that can fit onto the screen at once*/ + const MAX_SENSORS_ON_SCREEN: number = 5 + /** The colours that will be used for the lines & sensor information boxes */ + const SENSOR_BOX_COLORS: number[] = [2, 3, 4, 6, 7, 9] + /** The colours that will be used for writing the information about the sensor. */ + const SENSOR_BOX_TEXT_COLORS: number[] = [1, 1, 1, 1, 15, 15] + + const halfWidth = ui.STANDARD_DISPLAY_WIDTH >> 1 + const halfHeight = ui.STANDARD_DISPLAY_HEIGHT >> 1 + + /** + * Responsible for invoking the logging commands for each sensor, + * Presents information about each sensor's state via colourful collapsing boxes + * + * Sensors are logged via a scheduler + */ + export class DataRecorder extends ui.UiScreen { + /** */ + private scheduler: sensors.SensorScheduler; + /** For displaying their status on the screen and passing to the scheduler. */ + private sensors: sensors.Sensor[] + /** For faster looping, modulo calculation when pressing UP or DOWN */ + private numberOfSensors: number; + /** Sensor to be shown */ + private currentSensorIndex: number; + /** Last sensor on the screen */ + private sensorIndexOffset: number; + /** For the currentSensorIndex */ + private sensorBoxColor: number; + + private showCancelRecordingScreen: boolean; + private currentlyCancelling: boolean + // private yesBtn: Sprite // currentBtn = 0 + // private noBtn: Sprite // currentBtn = 1 + + constructor(runtime: ui.UiRuntime, s: sensors.Sensor[]) { + super(runtime) + + this.scheduler = new sensors.SensorScheduler(s) + this.sensors = s + this.numberOfSensors = s.length + + this.sensorIndexOffset = 0 + this.currentSensorIndex = 0 + this.sensorBoxColor = 15 + this.showCancelRecordingScreen = false; + this.currentlyCancelling = false; + + //--------------- + // User Controls: + //--------------- + + // // Go Back: + // context.onEvent( + // ControllerButtonEvent.Pressed, + // controller.B.id, + // () => { + // if (this.scheduler.loggingComplete()) { + // this.app.popScene() + // this.app.pushScene(new Home(this.app)) + // } + // + // else { + // this.showCancelRecordingScreen = !this.showCancelRecordingScreen + // } + // } + // ) + // + // // Clear whatever A was previously bound to + // context.onEvent( + // ControllerButtonEvent.Pressed, + // controller.A.id, + // () => { + // if (this.showCancelRecordingScreen) { + // this.currentlyCancelling = true + // this.scheduler.stop() + // + // basic.pause(1000) + // this.app.popScene() + // this.app.pushScene(new Home(this.app)) + // } + // } + // ) + // + // // Scroll Up + // context.onEvent( + // ControllerButtonEvent.Pressed, + // controller.up.id, + // () => { + // this.currentSensorIndex = Math.max(0, this.currentSensorIndex - 1) + // + // if (this.sensorIndexOffset > 0) + // this.sensorIndexOffset = Math.max(0, this.sensorIndexOffset - 1) + // + // this.update() + // } + // ) + // + // // Scroll Down + // context.onEvent( + // ControllerButtonEvent.Pressed, + // controller.down.id, + // () => { + // this.currentSensorIndex = Math.min(this.currentSensorIndex + 1, this.numberOfSensors - 1) + // + // if (this.currentSensorIndex > 4) + // this.sensorIndexOffset = Math.min(this.sensorIndexOffset + 1, this.numberOfSensors - 5) + // + // this.update() + // } + // ) + // + // + // // For cancelling the current recording: + // + // this.yesBtn = new Sprite({ img: Icons.get("tile_button_a") }) + // this.yesBtn.bindXfrm(new Affine()) + // this.yesBtn.xfrm.parent = new Affine() + // this.yesBtn.xfrm.worldPos.x = Screen.HALF_WIDTH + // this.yesBtn.xfrm.worldPos.y = Screen.HALF_HEIGHT + // this.yesBtn.xfrm.localPos.x = -40 + // this.yesBtn.xfrm.localPos.y = 12 + // + // this.noBtn = new Sprite({ img: Icons.get("tile_button_b") }) + // this.noBtn.bindXfrm(new Affine()) + // this.noBtn.xfrm.parent = new Affine() + // this.noBtn.xfrm.worldPos.x = Screen.HALF_WIDTH + // this.noBtn.xfrm.worldPos.y = Screen.HALF_HEIGHT + // this.noBtn.xfrm.localPos.x = 40 + // this.noBtn.xfrm.localPos.y = 12 + + this.log() + } + + log() { this.scheduler.start() } + + private drawCenteredText(surface: ui.DrawSurface, text: string, y: number, color: number): void { + const size = surface.measureText(text) + const x = Math.max(0, Math.idiv(ui.STANDARD_DISPLAY_WIDTH - size.width, 2)) + surface.drawText(text, x, y, { color }) + } + + private rect(x: number, y: number, width: number, height: number): ui.Rect { + return new ui.Rect(x, y, width, height) // if this constructor exists + } + + public render(surface: ui.DrawSurface): void { + surface.clear(0xC); + + const halfWidth = ui.STANDARD_DISPLAY_WIDTH >> 1 + const halfHeight = screen().height >> 1 + + if (this.scheduler.loggingComplete()) { + this.drawCenteredText(surface, "Data Logging Complete.", halfHeight - 10, 15); + this.drawCenteredText(surface, "Press B to back out.", halfHeight, 15); + } + + else { + this.drawCenteredText(surface, "Recording data...", 4, 15); + let y = 16 + + for (let i = this.sensorIndexOffset; i < this.numberOfSensors; i++) { + if (i - this.sensorIndexOffset > MAX_SENSORS_ON_SCREEN) + break + + this.sensorBoxColor = SENSOR_BOX_COLORS[i % SENSOR_BOX_COLORS.length] + const boxWidth: number = 142 + + if (i != this.currentSensorIndex) { + surface.fillRect(new ui.Rect(5, y, boxWidth, 16), 16) + surface.fillRect(new ui.Rect(7, y, boxWidth + 3, 14), this.sensorBoxColor) + surface.drawText(this.sensors[i].name, 12, y + 2, { color: 15 }) + } + + else { + surface.fillRect(new ui.Rect(5, y, boxWidth, 62), 15) + surface.fillRect(new ui.Rect(7, y, boxWidth + 3, 60), this.sensorBoxColor) + + const sensor = this.sensors[i] + surface.drawText(sensor.name, 12, y + 2, { color: 15 }) + + const sensorInfo: string[] = sensor.isInEventMode + ? sensor.getEventInformation() + : sensor.getRecordingInformation(); + + sensorInfo.forEach((info, idx) => { + y += 12 + surface.drawText(info, 24, y, { color: SENSOR_BOX_TEXT_COLORS[idx] }) + }); + } + + y += 14 } - log() { this.scheduler.start() } - - update(): void { - Screen.fillRect( - Screen.LEFT_EDGE, - Screen.TOP_EDGE, - Screen.WIDTH, - Screen.HEIGHT, - 0xc - ) - - // Check if all sensors have finished their work: - if (this.scheduler.loggingComplete()) { - screen().printCenter("Data Logging Complete.", (screen().height >> 1) - 10); - screen().printCenter("Press B to back out.", screen().height >> 1); - } - - else { - screen().printCenter("Recording data...", 4); - let y = 16 - - for (let i = this.sensorIndexOffset; i < this.numberOfSensors; i++) { - if (i - this.sensorIndexOffset > MAX_SENSORS_ON_SCREEN) - break - - // Get the colour for this box - this.sensorBoxColor = SENSOR_BOX_COLORS[i % SENSOR_BOX_COLORS.length] - - const boxWidth: number = 142 - - // Draw box as collapsed: - if (i != this.currentSensorIndex) { - screen().fillRect( - 5, - y, - boxWidth, - 16, - 16 - ) - - screen().fillRect( - 7, - y, - boxWidth + 3, - 14, - this.sensorBoxColor - ) - - screen().print( - this.sensors[i].getName(), - 12, - y + 2, - 15 - ) - } - - // Box is selected: Draw all information: - else { - screen().fillRect( - 5, - y, - boxWidth, - 62, - 15 - ) - - screen().fillRect( - 7, - y, - boxWidth + 3, - 60, - this.sensorBoxColor - ) - - //------------------------------- - // Information inside sensor box: - //------------------------------- - - const sensor = this.sensors[i] - screen().print( - sensor.getName(), - 12, - y + 2, - 15 - ) - - //------------------------------ - // Write the sensor information: - //------------------------------ - const sensorInfo: string[] = (sensor.isInEventMode) ? sensor.getEventInformation() : sensor.getRecordingInformation(); - sensorInfo.forEach((info, idx) => { - y += 12 - screen().print( - info, - 24, - y, - SENSOR_BOX_TEXT_COLORS[idx] - ) - }); - } - - y += 14 - } - - if (this.showCancelRecordingScreen) { - const headerX = Screen.HALF_WIDTH; // Log has data in it - - // Outline: - screen().fillRect( - Screen.HALF_WIDTH - 65, - Screen.HALF_HEIGHT - 30, - 130 + 2, - 60 + 2, - 15 // Black - ) - - screen().fillRect( - Screen.HALF_WIDTH - 65, - Screen.HALF_HEIGHT - 30, - 130, - 60, - 4 // Orange - ) - - const tutorialTextLength = ("Cancel recording?".length * font.charWidth) - screen().print( - "Cancel recording?", - headerX - (tutorialTextLength >> 1), - Screen.HALF_HEIGHT - 30 + 7, - 15 // Black - ) - - // Underline the title: - screen().fillRect( - headerX - (tutorialTextLength >> 1) - 1, - Screen.HALF_HEIGHT - 30 + 16, - tutorialTextLength, - 2, - 15 // Black - ) - - if (this.currentlyCancelling) - screen().printCenter("Cancelling...", Screen.HALF_HEIGHT - 9, 15) - - // Draw button prompts: - screen().print( - "Yes", - Screen.HALF_WIDTH - 48, - Screen.HALF_HEIGHT + 20, - 15 - ) - - screen().print( - "No", - Screen.HALF_WIDTH + 33, - Screen.HALF_HEIGHT + 20, - 15 - ) - - // White boxes behind yes & no btns: - screen().fillRect( - Screen.HALF_WIDTH - 47, - Screen.HALF_HEIGHT + 6, - 12, - 12, - 1 - ) - - screen().fillRect( - Screen.HALF_WIDTH + 34, - Screen.HALF_HEIGHT + 6, - 12, - 12, - 1 - ) - - this.yesBtn.draw() - this.noBtn.draw() - } - } + if (this.showCancelRecordingScreen) { + const headerX = halfWidth; + + surface.fillRect(new ui.Rect(halfWidth - 65, halfHeight - 30, 132, 62), 15) + surface.fillRect(new ui.Rect(halfWidth - 65, halfHeight - 30, 130, 60), 4) + + const titleText = "Cancel recording?" + const titleSize = surface.measureText(titleText) + const titleX = headerX - (titleSize.width >> 1) + const titleY = halfHeight - 30 + 7 + surface.drawText(titleText, titleX, titleY, { color: 15 }) + + surface.fillRect(new ui.Rect(titleX - 1, halfHeight - 30 + 16, titleSize.width, 2), 15) + + if (this.currentlyCancelling) + this.drawCenteredText(surface, "Cancelling...", halfHeight - 9, 15) + + surface.drawText("Yes", halfWidth - 48, halfHeight + 20, { color: 15 }) + surface.drawText("No", halfWidth + 33, halfHeight + 20, { color: 15 }) + + surface.fillRect(new ui.Rect(halfWidth - 47, halfHeight + 6, 12, 12), 1) + surface.fillRect(new ui.Rect(halfWidth + 34, halfHeight + 6, 12, 12), 1) } + } + } + } +} + +namespace microdata { + const SENSOR_VALUE_WIDTH = 30 + + const SENSOR_ACTION_SCOPE = "live-graph/actions" + const MAX_SENSORS = 3 + + interface SensorChoice { + sensor: sensors.Sensor; + } + + interface SensorLoggingInfo { + sensor: sensors.Sensor | undefined, + number_of_measurements: number | undefined, + measurement_interval_ms: number | undefined + } + + function getDefaultSensorLoggingInfoObj(): SensorLoggingInfo { + return { sensor: undefined, number_of_measurements: undefined, measurement_interval_ms: undefined } + } + + type SensorAction = "sensors" + type TimeAction = "time" + type DoneAction = "done" + type AddAction = "add" + type RemoveAction = "remove" + + type GridActions = SensorAction | TimeAction | AddAction | RemoveAction | DoneAction + + const SENSOR_PICKER_SCOPE = "select-sensor" + const SENSOR_PICKER_COLUMNS = 5 + const SENSOR_PICKER_ITEM_WIDTH = 28 + const SENSOR_PICKER_ITEM_HEIGHT = 28 + const SENSOR_PICKER_GAP = 3 + + const SENSOR_ACTION_BAND_HEIGHT = 16 + const SENSOR_ACTION_WIDTH = 40 + const SENSOR_ACTION_HEIGHT = 22 + const SENSOR_ACTION_GAP_H = 8 + const SENSOR_ACTION_GAP_V = 5 + const SENSOR_ACTION_CENTER_Y = (SENSOR_PICKER_ITEM_HEIGHT >> 1) + 3 + + // Selected sensors get a thick yellow rounded border. + const SENSOR_SELECTED_STYLE: ui.UiButtonStyle = { + backgroundColor: 1, + frame: "roundedRect", + borderColor: 5, + borderThickness: 3, + } + + export class RecordData extends ui.UiScreen { + private sensorLoggingInfos: SensorLoggingInfo[] + private actions: ui.UiGrid; + + constructor(runtime: ui.UiRuntime) { + super(runtime) + + this.backgroundColor = 6 + this.sensorLoggingInfos = [getDefaultSensorLoggingInfoObj()]; + + this.actions = new ui.UiGrid({ + scopeId: SENSOR_ACTION_SCOPE, + controls: this.getRowOfLoggingActions(0).concat(this.getRowOfAddRemoveDeleteActions()), + controlSize: { width: SENSOR_ACTION_WIDTH, height: SENSOR_ACTION_HEIGHT }, + columnCount: 3, + controlStyle: ui.UiButtonStyles.LightShadowedWhite, + rowGap: SENSOR_ACTION_GAP_V, + columnGap: SENSOR_ACTION_GAP_H, + }); + + this.addCentered( + this.actions, + SENSOR_ACTION_CENTER_Y, + ui.STANDARD_DISPLAY_WIDTH, + SENSOR_ACTION_BAND_HEIGHT + ); + } + + public handleInput(event: ui.UiInputEvent): undefined | boolean { + if (event.action === "cancel" && event.phase === "pressed") { + this.runtime.pop(); + return true; + } + return undefined; + } + + // Sensors are keyed by name for ownership checks (Sensor instances may be + // rebuilt between calls, so don't rely on reference equality). + private rowIndexForSensor(sensor: sensors.Sensor): number { + for (let i = 0; i < this.sensorLoggingInfos.length; i++) { + if (this.sensorLoggingInfos[i].sensor !== undefined && + this.sensorLoggingInfos[i].sensor.name === sensor.name) return i; + } + return -1; + } + + private openSelectSensorsPicker(rowIdx: number): void { + const availableSensors: sensors.Sensor[] = + sensors.getAllMicrobitSensors().concat(sensors.getAllConnectedJacdacSimpleSensors()); + + const sensorControls: ui.UiControl[] = + availableSensors.map((sensor: sensors.Sensor, i: number) => { + const owner = this.rowIndexForSensor(sensor); + const isMine = owner === rowIdx; + const takenByOther = owner >= 0 && !isMine; + + return { + id: `sensor-${i}`, + value: { sensor }, + focusLabel: takenByOther ? `${sensor.name} (in use)` : sensor.name, + bitmap: sensorNameToBitmap(sensor.name, sensor.isJacdacSensor), + style: (isMine || takenByOther) ? SENSOR_SELECTED_STYLE : undefined, + focusable: !takenByOther, + }; + }); + + const modal = new ui.UiPicker({ + modalScopeId: SENSOR_PICKER_SCOPE, + title: "Sensors", + controls: sensorControls, + columnCount: SENSOR_PICKER_COLUMNS, + controlSize: { width: SENSOR_PICKER_ITEM_WIDTH, height: SENSOR_PICKER_ITEM_HEIGHT }, + columnGap: SENSOR_PICKER_GAP, + rowGap: SENSOR_PICKER_GAP, + controlStyle: ui.UiButtonStyles.LightShadowedWhite, + closeOnActivate: true, + onActivate: ( + choice: SensorChoice, + control: ui.UiControl + ) => { + this.sensorLoggingInfos[rowIdx].sensor = choice.sensor; + const sensorButton = this.actions.controls.find(c => c.id === `sensors-${rowIdx}`); + sensorButton.bitmap = sensorNameToBitmap(choice.sensor.name, choice.sensor.isJacdacSensor) + sensorButton.focusLabel = choice.sensor.name + + this.rebuildGrid(); + }, + }); + this.openModal(modal) + } + + private openSelectNumMeasurementsPicker(rowIdx: number): void { + this.openModal( + new ui.UiNumericEntryModal("num-measurements-editor", this.sensorLoggingInfos[0].number_of_measurements, value => { + this.sensorLoggingInfos[rowIdx].number_of_measurements = value + const sensorButton = this.actions.controls.find(c => c.id === `num_measurements-${rowIdx}`); + sensorButton.text = "" + value + sensorButton.bitmap = undefined + + this.rebuildGrid() + }), + ) + } + + private openSelectMeasurementIntervalPicker(rowIdx: number): void { + this.openModal( + new ui.UiNumericEntryModal("interval-measurements-editor", this.sensorLoggingInfos[0].measurement_interval_ms, value => { + this.sensorLoggingInfos[rowIdx].measurement_interval_ms = value + const sensorButton = this.actions.controls.find(c => c.id === `interval_measurements-${rowIdx}`); + sensorButton.text = "" + value + "ms" + sensorButton.bitmap = undefined + + this.rebuildGrid() + }), + ) + } + + private getRowOfLoggingActions(idx: number): ui.UiControl[] { + const info = this.sensorLoggingInfos[idx]; + return [ + { + id: `sensors-${idx}`, + value: "sensors", + focusLabel: info.sensor !== undefined + ? info.sensor.name + : "Choose sensor", + bitmap: info.sensor !== undefined + ? sensorNameToBitmap(info.sensor.name, info.sensor.isJacdacSensor) + : this.assets.getBitmap("btn_plus"), + onActivate: () => this.openSelectSensorsPicker(idx), + }, + { + id: `num_measurements-${idx}`, + value: "time", + focusLabel: "Number of measurements", + text: info.number_of_measurements !== undefined + ? "" + info.number_of_measurements + : undefined, + bitmap: info.number_of_measurements === undefined + ? this.assets.getBitmap("btn_plus") + : undefined, + onActivate: () => this.openSelectNumMeasurementsPicker(idx), + }, + { + id: `interval_measurements-${idx}`, + value: "time", + focusLabel: "Measurement interval", + text: info.measurement_interval_ms === undefined + ? undefined + : `${info.measurement_interval_ms}ms`, + bitmap: info.measurement_interval_ms === undefined + ? this.assets.getBitmap("btn_plus") + : undefined, + onActivate: () => this.openSelectMeasurementIntervalPicker(idx), + } + ]; + } + + private getRowOfAddRemoveDeleteActions(): ui.UiControl[] { + let actions: ui.UiControl[] = [] + + if (this.sensorLoggingInfos.length < MAX_SENSORS) { + actions.push( + { + id: "add", + value: "add", + text: "Add", + onActivate: () => { + this.sensorLoggingInfos.push(getDefaultSensorLoggingInfoObj()); + + const controls = this.sensorLoggingInfos + .reduce[]>((acc, _, i) => acc.concat(this.getRowOfLoggingActions(i)), []) + .concat(this.getRowOfAddRemoveDeleteActions()); + + const oldActions = this.actions; + this.actions = new ui.UiGrid({ + scopeId: SENSOR_ACTION_SCOPE, + controls, + controlSize: { width: SENSOR_ACTION_WIDTH, height: SENSOR_ACTION_HEIGHT }, + columnCount: 3, + controlStyle: ui.UiButtonStyles.LightShadowedWhite, + rowGap: SENSOR_ACTION_GAP_V, + columnGap: SENSOR_ACTION_GAP_H, + }); + + this.remove(oldActions); + this.addCentered(this.actions, SENSOR_ACTION_CENTER_Y, ui.STANDARD_DISPLAY_WIDTH, SENSOR_ACTION_BAND_HEIGHT); + this.rebuildGrid(); + }, + visible: false, + focusable: false, + }, + ) + } + + actions.push( + { + id: "done", + value: "done", + text: "Done", + onActivate: () => { + const s: sensors.Sensor[] = this.sensorLoggingInfos.map(info => { + info.sensor.setConfig({ measurements: info.number_of_measurements, period: info.measurement_interval_ms }) + return info.sensor; + }) + + this.runtime.push(new DataRecorder(this.runtime, s)); + }, + visible: false, + focusable: false, + }, + ); + return actions; + } + + private isSensorLoggingInfoFilledIn(info: SensorLoggingInfo): boolean { + return info.sensor !== undefined && + info.number_of_measurements !== undefined && + info.measurement_interval_ms !== undefined; + } + + private rebuildGrid() { + const complete = this.sensorLoggingInfos.every(info => this.isSensorLoggingInfoFilledIn(info)); + for (const id of ["add", "done"]) { + const control = this.actions.controls.find(c => c.id === id); + if (!control) continue; + control.visible = complete; + control.focusable = complete; + } + + this.actions.invalidateLayout() + this.actions.registerFocusTargets(this.focus) + this.actions.registerNavigation(this.focusInput); + } + + public render(surface: ui.DrawSurface): void { + super.render(surface) } + } } diff --git a/home.ts b/home.ts index 93c4d75..a7def20 100644 --- a/home.ts +++ b/home.ts @@ -1,146 +1,113 @@ -namespace microdata { - import Screen = user_interface_base.Screen - import CursorScene = user_interface_base.CursorScene - import Button = user_interface_base.Button - import ButtonStyles = user_interface_base.ButtonStyles - import AppInterface = user_interface_base.AppInterface - - import font = user_interface_base.font - - export class Home extends CursorScene { - constructor(app: AppInterface) { - super(app) - } - - /* override */ startup() { - super.startup() - basic.pause(50); - - const y = 25 - - this.navigator.setBtns([[ - new Button({ - parent: null, - style: ButtonStyles.Transparent, - icon: "linear_graph_1", - ariaId: "Real-time Data", - x: -58, - y, - onClick: () => { - this.app.popScene() - this.app.pushScene(new SensorSelect(this.app, MicroDataSceneEnum.LiveDataViewer)) - }, - }), - - new Button({ - parent: null, - style: ButtonStyles.Transparent, - icon: "edit_program", - ariaId: "Log Data", - x: -20, - y, - onClick: () => { - this.app.popScene() - this.app.pushScene(new SensorSelect(this.app, MicroDataSceneEnum.RecordingConfigSelect)) - }, - }), - - new Button({ - parent: null, - style: ButtonStyles.Transparent, - icon: "radio_set_group", - ariaId: "Command Mode", - x: 20, - y, - onClick: () => { - this.app.popScene() - this.app.pushScene(new DistributedLoggingScreen(this.app)) - }, - }), - - new Button({ - parent: null, - style: ButtonStyles.Transparent, - icon: "largeDisk", - ariaId: "View Data & Settings", - x: 58, - y, - onClick: () => { - this.app.popScene() - this.app.pushScene(new DataViewSelect(this.app)) - }, - }) - ]]) - } - - private drawVersion() { - const font = bitmaps.font5 - const text = "v1.7.5" - Screen.print( - text, - Screen.RIGHT_EDGE - (font.charWidth * text.length), - Screen.BOTTOM_EDGE - font.charHeight - 2, - 0xb, - font - ) - } - - private yOffset = -Screen.HEIGHT >> 1 - draw() { - Screen.fillRect( - Screen.LEFT_EDGE, - Screen.TOP_EDGE, - Screen.WIDTH, - Screen.HEIGHT, - 0xc - ) - - const microbitLogo = Icons.get("microbitLogo") - const microdataLogo = Icons.get("microdataLogo") - - this.yOffset = Math.min(0, this.yOffset + 2) - const t = control.millis() - const dy = this.yOffset == 0 ? (Math.idiv(t, 800) & 1) - 1 : 0 - const margin = 2 - const OFFSET = (Screen.HEIGHT >> 1) - microdataLogo.height - margin - 9 - const y = Screen.TOP_EDGE + OFFSET //+ dy - Screen.drawTransparentImage( - microdataLogo, - Screen.LEFT_EDGE + ((Screen.WIDTH - microdataLogo.width) >> 1)// + dy - , - y + this.yOffset - ) - - Screen.drawTransparentImage( - microbitLogo, - Screen.LEFT_EDGE + - ((Screen.WIDTH - microbitLogo.width) >> 1) + dy - , - y - microdataLogo.height + this.yOffset + margin - ) - - if (!this.yOffset) { - Screen.print( - "Mini-measurer", - Screen.LEFT_EDGE + - ((Screen.WIDTH + microdataLogo.width) >> 1) - + dy - - - font.charWidth * "Mini-measurer".length, - Screen.TOP_EDGE + - OFFSET + - microdataLogo.height + - dy + - this.yOffset + - 3, - 0xb, - font - ) - } - - this.navigator.drawComponents(); - this.drawVersion() - super.draw() - } - } -} +namespace microdata { + type HomeAction = "realtime" | "log" | "command" | "view" + + const HOME_ACTION_SCOPE = "home/actions" + const HOME_ACTION_SIZE = 30 + const HOME_ACTION_GAP = 10 + const HOME_ACTION_CENTER_Y = ui.STANDARD_DISPLAY_HEIGHT - 32 + + export class Home extends ui.UiScreen { + constructor(runtime: ui.UiRuntime) { + super(runtime); + + const actions = new ui.UiRow({ + scopeId: HOME_ACTION_SCOPE, + controls: this.createActions(), + controlSize: { width: HOME_ACTION_SIZE, height: HOME_ACTION_SIZE }, + gap: HOME_ACTION_GAP, + controlStyle: ui.UiButtonStyles.Transparent, + labelBounds: new ui.Rect( + 0, + 0, + ui.STANDARD_DISPLAY_WIDTH, + ui.STANDARD_DISPLAY_HEIGHT + ), + wrap: true, + }); + + this.addCentered( + actions, + HOME_ACTION_CENTER_Y, + ui.STANDARD_DISPLAY_WIDTH, + HOME_ACTION_SIZE + ); + } + + private createActions(): ui.UiControl[] { + return [ + this.action("realtime", ui.linearGraph1, "Real-time Data", () => + this.runtime.push(new LiveSensorGraph(this.runtime)) + ), + this.action("log", ui.largeEditIcon, "Log Data", () => this.runtime.push(new SensorLoggingSetup(this.runtime))), + this.action("command", ui.radio_set_group, "Command Mode", () => + this.runtime.pop() + ), + this.action("view", ui.largeDisk, "View Data & Settings", () => + this.runtime.pop() + ), + ]; + } + + // The row owns a single focus scope and arranges its controls; left/right + // navigation and activation are handled by the focus runtime, so no + // handleInput override is needed here. + private action( + id: HomeAction, + bitmap: Bitmap, + focusLabel: string, + onActivate: () => void + ): ui.UiControl { + const control = ui.button(id, { bitmap }, onActivate); + control.focusLabel = focusLabel; + return control; + } + + private yOffset = -ui.STANDARD_DISPLAY_HEIGHT >> 1 + render(surface: ui.DrawSurface): void { + this.yOffset = Math.min(0, this.yOffset + 2) + const t = control.millis() + const dy = this.yOffset == 0 ? (Math.idiv(t, 800) & 1) - 1 : 0 + const margin = 2 + const OFFSET = (ui.STANDARD_DISPLAY_HEIGHT >> 1) - microdataLogo.height - margin - 9 + const y = OFFSET + + surface.clear(0xC); + + surface.drawBitmap( + microdataLogo, + ((ui.STANDARD_DISPLAY_WIDTH - microdataLogo.width) >> 1) + dy, + y + this.yOffset + ); + + surface.drawBitmap( + ui.microbitLogo, + ((ui.STANDARD_DISPLAY_WIDTH - ui.microbitLogo.width) >> 1) + dy, + y - microdataLogo.height + this.yOffset + margin + ); + + if (!this.yOffset) { + const size: ui.Size = surface.measureText("direct", bitmaps.font8) + surface.drawText( + "Mini-measurer", + ((ui.STANDARD_DISPLAY_WIDTH + microdataLogo.width) >> 1) + dy - (size.width << 1), + OFFSET + microdataLogo.height + dy + this.yOffset + 3, + { color: 1, font: bitmaps.font8 } + ) + } + + // draw version: + const font = bitmaps.font5 + const text = "v1.9.0" + const size: ui.Size = surface.measureText("direct", font) + + surface.drawText( + text, + ui.STANDARD_DISPLAY_WIDTH - size.width, + ui.STANDARD_DISPLAY_HEIGHT - size.height - 2, + { color: 0xb, font } + ) + + super.render(surface) // row of action buttons + } + } +} diff --git a/liveSensorGraph.ts b/liveSensorGraph.ts new file mode 100644 index 0000000..16a676a --- /dev/null +++ b/liveSensorGraph.ts @@ -0,0 +1,215 @@ +namespace microdata { + type SensorInfo = { + sensor: sensors.Sensor, + nameLabel: ui.UiLabel + valueLabel: ui.UiLabel + unitLabel: ui.UiLabel + } + + type SensorAction = "sensors" + + const MAX_SELECTABLE_SENSORS = 3 + const SENSOR_ACTION_SCOPE = "live-graph/actions" + const SENSOR_ACTION_GAP = 8 + const SENSOR_ACTION_BAND_HEIGHT = 24 + const SENSOR_ACTION_CENTER_Y = 110 + + const SENSOR_READOUT_X = 4 + const SENSOR_READOUT_Y = 1 + const SENSOR_ROW_GAP = 3 + const SENSOR_VALUE_WIDTH = 30 + + export class LiveSensorGraph extends ui.UiScreen { + private tick: number + private graphRect: ui.Rect + private sensorInfos: SensorInfo[] + private readout: ui.UiStack + private nameColumn: ui.UiStack + private valueColumn: ui.UiStack + private unitColumn: ui.UiStack + + private onJacdacChange: () => void; + + constructor(runtime: ui.UiRuntime) { + super(runtime) + + this.backgroundColor = 6 + this.tick = 0 + this.graphRect = new ui.Rect(8, 26, 144, 72) + + this.sensorInfos = [] + + this.nameColumn = new ui.UiStack({ orientation: "column", gap: 0 }) + this.valueColumn = new ui.UiStack({ orientation: "column", gap: 0 }) + this.unitColumn = new ui.UiStack({ orientation: "column", gap: 0 }) + this.readout = new ui.UiStack({ + orientation: "row", + children: [ + { view: this.nameColumn }, + { view: this.valueColumn }, + { view: this.unitColumn }, + ], + gap: SENSOR_ROW_GAP, + }); + this.add(this.readout, { x: SENSOR_READOUT_X, y: SENSOR_READOUT_Y }) + + const actions = new ui.UiRow({ + scopeId: SENSOR_ACTION_SCOPE, + controls: this.createActions(), + controlStyle: ui.UiButtonStyles.LightShadowedWhite, + gap: SENSOR_ACTION_GAP, + wrap: true, + }); + + this.addCentered( + actions, + SENSOR_ACTION_CENTER_Y, + ui.STANDARD_DISPLAY_WIDTH, + SENSOR_ACTION_BAND_HEIGHT + ); + + this.onJacdacChange = () => this.pruneDisconnectedSensors(getAvailableMicrobitAndJacdacSensors()) + } + + public activate(): void { + jacdac.bus.on(jacdac.DEVICE_CONNECT, this.onJacdacChange) + jacdac.bus.on(jacdac.DEVICE_DISCONNECT, this.onJacdacChange) + } + + public deactivate(): void { + jacdac.bus.off(jacdac.DEVICE_CONNECT, this.onJacdacChange) + jacdac.bus.off(jacdac.DEVICE_DISCONNECT, this.onJacdacChange) + } + + private createActions(): ui.UiControl[] { + return [ + ui.button("sensors", "Sensors", () => openSensorPicker( + getAvailableMicrobitAndJacdacSensors, + { + screen: this, + selected: () => this.sensorInfos.map(info => info.sensor), + maxSelectableSensorNum: MAX_SELECTABLE_SENSORS, + onPick: sensor => this.toggleSensor(sensor), + onUnpick: index => { + this.sensorInfos.splice(index, 1) + this.rebuildReadout() + }, + } + ) + ) + ]; + } + + private pruneDisconnectedSensors(connectedSensors: sensors.Sensor[]): void { + const connectedNames = connectedSensors.map(s => s.name); + const stillConnected = (info: SensorInfo) => connectedNames.indexOf(info.sensor.name) >= 0; + const removed = this.sensorInfos.some(info => !stillConnected(info)); + this.sensorInfos = this.sensorInfos.filter(stillConnected); + if (removed) this.rebuildReadout(); + } + + private activeIndexForName(name: string): number { + for (let i = 0; i < this.sensorInfos.length; i++) { + if (this.sensorInfos[i].sensor.name == name) + return i + } + return -1 + } + + // Sets each readout column from the active sensors and assigns each value + // label's color to match its graph line. + private rebuildReadout(): void { + const names: ui.UiStackChild[] = [] + const values: ui.UiStackChild[] = [] + const units: ui.UiStackChild[] = [] + for (let i = 0; i < this.sensorInfos.length; i++) { + const info = this.sensorInfos[i] + info.valueLabel.setColor(2 + i) + names.push({ view: info.nameLabel }) + values.push({ view: info.valueLabel }) + units.push({ view: info.unitLabel }) + } + this.nameColumn.setChildren(names) + this.valueColumn.setChildren(values) + this.unitColumn.setChildren(units) + this.remove(this.readout) + this.add(this.readout, { x: SENSOR_READOUT_X, y: SENSOR_READOUT_Y }) + } + + public handleInput(event: ui.UiInputEvent): undefined | boolean { + if (event.action === "cancel" && event.phase === "pressed") { + this.runtime.pop(); + return true; + } + + return undefined; + } + + // Toggles a sensor channel on or off. Selection is keyed by sensor name. + private toggleSensor(sensor: sensors.Sensor): void { + const idx = this.activeIndexForName(sensor.name) + if (idx >= 0) { + this.sensorInfos.splice(idx, 1) + } else { + // The picker locks these out, but guard the array regardless. + if (this.sensorInfos.length >= MAX_SELECTABLE_SENSORS) return + const nameLabel = new ui.UiLabel(sensor.name, 1) + const valueLabel = new ui.UiLabel({ + text: "--", + color: 1, + size: { width: SENSOR_VALUE_WIDTH }, + }) + const unitLabel = new ui.UiLabel(`${sensor.unitSymbol}`, 1) + nameLabel.setColor(15) + unitLabel.setColor(15) + this.sensorInfos.push({ sensor, nameLabel, valueLabel, unitLabel }) + } + this.rebuildReadout() + } + + public update(): void { + this.tick += 1 + if (this.tick % 6 != 0) return; + + this.sensorInfos.forEach((info: SensorInfo) => { + info.sensor.readIntoBufferOnce() + info.valueLabel.setText(`${info.sensor.reading.toString().slice(0, 5)}`) + }) + } + + public render(surface: ui.DrawSurface): void { + // Graph area: + surface.fillRect(this.graphRect, 0) + surface.drawRect(this.graphRect, 1) + + // Center reference line, dotted so it doesn't read as a data trace. + const midY = this.graphRect.y + Math.idiv(this.graphRect.height, 2) + const dotEnd = this.graphRect.x + this.graphRect.width - 2 + for (let x = this.graphRect.x + 1; x <= dotEnd; x += 2) { + surface.drawLine(x, midY, x, midY, 13) + } + + this.sensorInfos.forEach((sensorInfo: SensorInfo, i: number) => { + const sensor = sensorInfo.sensor + const values = sensor.dataBuffer + const range = sensor.max - sensor.min; + const color = 2 + i; + + let previousX = 0 + let previousY = 0 + for (let i = 0; i < values.length; i++) { + const x = this.graphRect.x + 2 + Math.idiv(i * (this.graphRect.width - 4), values.length - 1) + const y = this.graphRect.y + this.graphRect.height - 3 - Math.idiv((values[i] - sensor.min) * (this.graphRect.height - 6), range); + + if (i > 0) + surface.drawLine(previousX, previousY, x, y, color) + previousX = x + previousY = y + } + }); + // end of graph area + + super.render(surface) + } + } +} diff --git a/main.ts b/main.ts index 5cd25f2..1790162 100644 --- a/main.ts +++ b/main.ts @@ -1 +1,5 @@ -new microdata.App(); \ No newline at end of file +namespace microdata { + export const runtime = new ui.UiRuntime(new ui.DisplayShieldFrameAdapter(), new AppAssets()); + runtime.push(new Home(runtime)); + runtime.start(); +} diff --git a/pxt.d.ts b/pxt.d.ts new file mode 100644 index 0000000..f3568c9 --- /dev/null +++ b/pxt.d.ts @@ -0,0 +1,15 @@ +interface Math { + idiv(x: number, y: number): number +} + +interface Array { + removeAt(index: number): T +} + +interface Screen { + (): Bitmap +} + +interface String { + replaceAll(search: string, replacement: string): string +} diff --git a/pxt.json b/pxt.json index 856924c..0bef09d 100644 --- a/pxt.json +++ b/pxt.json @@ -1,42 +1,23 @@ { "name": "MicroData", - "version": "1.7.5", + "version": "1.9.3", "description": "Data science with micro:bit v2", "dependencies": { "core": "*", "radio": "*", "microphone": "*", "datalogger": "*", - "user-interface-base": "github:microbit-apps/user-interface-base#v0.0.33", - "jacdac": "github:jacdac/pxt-jacdac#v1.9.40", - "jacdac-light-level": "github:jacdac/pxt-jacdac/light-level#v1.9.40", - "jacdac-soil-moisture": "github:jacdac/pxt-jacdac/soil-moisture#v1.9.40", - "jacdac-distance": "github:jacdac/pxt-jacdac/distance#v1.9.40", - "jacdac-temperature": "github:jacdac/pxt-jacdac/temperature#v1.9.40", - "jacdac-flex": "github:jacdac/pxt-jacdac/flex#v1.9.40", - "jacdac-led": "github:jacdac/pxt-jacdac/led#v1.9.40", - "jacdac-led-strip": "github:jacdac/pxt-jacdac/led-strip#v1.9.40", - "jacdac-color": "github:jacdac/pxt-jacdac/color#v1.9.40" + "ui-controls": "github:microbit-apps/ui-controls#v0.0.10", + "sensors": "github:microbit-apps/sensors#v0.0.52" }, "files": [ - "analytics.ts", - "config.ts", "main.ts", "assets.ts", - "app.ts", "home.ts", - "version.ts", - "sensors.ts", - "liveDataViewer.ts", - "dataViewSelect.ts", - "loggingConfig.ts", - "sensorSelect.ts", + "liveSensorGraph.ts", "dataRecorder.ts", - "tabularDataViewer.ts", - "generateGraph.ts", - "distributedLogging.ts", - "headlessMode.ts", - "experiments.ts" + "sensorLoggingSetup.ts", + "sensorPicker.ts" ], "testFiles": [ "test.ts" diff --git a/sensorLoggingSetup.ts b/sensorLoggingSetup.ts new file mode 100644 index 0000000..2769b21 --- /dev/null +++ b/sensorLoggingSetup.ts @@ -0,0 +1,226 @@ +namespace microdata { + const MAX_SENSORS = 3 + + const ACTION_SCOPE = "logging-sensor-setup/actions" + const LENGTH_UNIT_SCOPE = "select-length-unit" + const INTERVAL_UNIT_SCOPE = "select-interval-unit" + const LENGTH_ENTRY_SCOPE = "length-editor" + const INTERVAL_ENTRY_SCOPE = "interval-editor" + + const CONFIG_IDS = ["length", "length_unit", "interval", "interval_unit"] + const CONFIG_LABELS = ["Total recording time", "Units", "Recording interval time", "Units"] + + const ACTION_COLUMNS = 4 + const ACTION_WIDTH = 34 + const ACTION_HEIGHT = 22 + const ACTION_GAP_H = 4 + const ACTION_GAP_V = 5 + const ACTION_ROWS: number[] = [CONFIG_IDS.length, MAX_SENSORS, 1] + const ACTION_BAND_HEIGHT = ACTION_ROWS.length * ACTION_HEIGHT + (ACTION_ROWS.length - 1) * ACTION_GAP_V + const ACTION_CENTER_Y = ui.STANDARD_DISPLAY_HEIGHT >> 1 + + const TIME_UNITS: string[] = ["ms", "sec", "min", "hr"] + const TIME_UNIT_MS: number[] = [1, 1000, 60000, 3600000] + + interface RecordingConfig { + length: number | undefined + lengthUnit: string | undefined + interval: number | undefined + intervalUnit: string | undefined + } + + type GridActions = "length" | "length_unit" | "interval" | "interval_unit" | "sensor" | "done" + + export class SensorLoggingSetup extends ui.UiScreen { + private recordingConfig: RecordingConfig + private selectedSensors: sensors.Sensor[] + private actions: ui.UiGrid + + constructor(runtime: ui.UiRuntime) { + super(runtime) + + this.backgroundColor = 6 + this.recordingConfig = { + length: undefined, + lengthUnit: undefined, + interval: undefined, + intervalUnit: undefined, + } + + this.selectedSensors = [] + + this.actions = new ui.UiGrid({ + scopeId: ACTION_SCOPE, + controls: this.createActions(), + controlSize: { width: ACTION_WIDTH, height: ACTION_HEIGHT }, + columnCount: ACTION_COLUMNS, + rows: ACTION_ROWS, + controlStyle: ui.UiButtonStyles.LightShadowedWhite, + rowGap: ACTION_GAP_V, + columnGap: ACTION_GAP_H, + }) + + this.add(new ui.UiLabel({ text: "Data logging setup", color: 15 }), { x: 24, y: 4 }); + this.addCentered( + this.actions, + ACTION_CENTER_Y, + ui.STANDARD_DISPLAY_WIDTH, + ACTION_BAND_HEIGHT + ) + this.syncActionsWithSelectedSensors() + + sensors.onSimpleSensorChange(() => { + const sensorNames = getAvailableMicrobitAndJacdacSensors().map(s => s.name) + this.selectedSensors = this.selectedSensors.filter(s => sensorNames.indexOf(s.name) >= 0) + this.syncActionsWithSelectedSensors() + }) + } + + public handleInput(event: ui.UiInputEvent): undefined | boolean { + if (event.action === "cancel" && event.phase === "pressed") { + this.runtime.pop() + return true + } + return undefined + } + + private createActions(): ui.UiControl[] { + const cfg = this.recordingConfig + const controls: ui.UiControl[] = [ + { + id: "length", value: "length", + onActivate: () => this.editNumber(LENGTH_ENTRY_SCOPE, cfg.length, v => { cfg.length = v }), + }, + { + id: "length_unit", value: "length_unit", + onActivate: () => this.editUnit(LENGTH_UNIT_SCOPE, "Total recording time units", u => { cfg.lengthUnit = u }), + }, + { + id: "interval", value: "interval", + onActivate: () => this.editNumber(INTERVAL_ENTRY_SCOPE, cfg.interval, v => { cfg.interval = v }), + }, + { + id: "interval_unit", value: "interval_unit", + onActivate: () => this.editUnit(INTERVAL_UNIT_SCOPE, "Interval units", u => { cfg.intervalUnit = u }), + }, + ] + + for (let i = 0; i < MAX_SENSORS; i++) + controls.push(this.createSensorAction(i)) + + controls.push({ id: "done", value: "done", onActivate: () => { } }) + return controls + } + + private createSensorAction(slot: number): ui.UiControl { + return { + id: `sensor-${slot}`, + value: "sensor", + onActivate: () => this.chooseSensorForSlot(slot), + } + } + + private action(id: string): ui.UiControl { + return this.actions.controls.find(c => c.id === id) + } + + // Update the text, focusLabel, bitmap, etc for the config and sensors buttons. + // So that this.recordingConfig and this.selectedSensors are in sync with the GUI: + private syncActionsWithSelectedSensors(): void { + const cfg = this.recordingConfig + const values: string[] = [ + (cfg.length === undefined) ? undefined : "" + cfg.length, + cfg.lengthUnit, + (cfg.interval === undefined) ? undefined : "" + cfg.interval, + cfg.intervalUnit, + ] + + let unlocked = true + for (let i = 0; i < CONFIG_IDS.length; i += 2) { + this.syncAction(CONFIG_IDS[i], unlocked, CONFIG_LABELS[i], values[i]) + this.syncAction(CONFIG_IDS[i + 1], unlocked, CONFIG_LABELS[i + 1], values[i + 1]) + unlocked = unlocked && (values[i] !== undefined) && (values[i + 1] !== undefined) + } + + const shown = unlocked ? Math.min(this.selectedSensors.length + 1, MAX_SENSORS) : 0 + for (let i = 0; i < MAX_SENSORS; i++) { + const sensor = this.selectedSensors[i] + const control = this.syncAction( + `sensor-${i}`, + i < shown, + sensor === undefined ? "Choose sensor" : sensor.name, + undefined + ); + + if (sensor !== undefined) + control.bitmap = sensorNameToBitmap(sensor.name, sensor.isJacdacSensor) + } + + this.syncAction("done", this.selectedSensors.length > 0, "Done", "Done") + + this.actions.invalidateLayout() + this.actions.registerFocusTargets(this.focus) + this.actions.registerNavigation(this.focusInput) + } + + private syncAction(id: string, visible: boolean, label: string, text: string): ui.UiControl { + const control = this.action(id) + control.visible = visible + control.focusable = visible + control.focusLabel = label + control.text = text + control.bitmap = (text === undefined) ? this.assets.getBitmap("btn_plus") : undefined + return control + } + + private editNumber(scopeId: string, value: number, apply: (value: number) => void): void { + this.openModal(new ui.UiNumericEntryModal(scopeId, value, v => { + apply(v) + this.syncActionsWithSelectedSensors() + })) + } + + private editUnit(scopeId: string, title: string, apply: (unit: string) => void): void { + this.openModal(new ui.UiPicker(scopeId, title, TIME_UNITS, u => { + apply(u) + this.syncActionsWithSelectedSensors() + })) + } + + private slotForSensor(sensor: sensors.Sensor): number { + return this.selectedSensors.map(s => s.name).indexOf(sensor.name) + } + + private chooseSensorForSlot(slot: number): void { + // This slot's own sensor stays pickable, others are locked: + const own = this.selectedSensors[slot] + openSensorPicker(getAvailableMicrobitAndJacdacSensors, { + screen: this, + selected: () => this.selectedSensors, + pickedSensors: this.selectedSensors.filter(s => own === undefined || s.name !== own.name), + onPick: sensor => this.selectSensor(slot, sensor), + onUnpick: index => { + this.selectedSensors.splice(index, 1) + this.syncActionsWithSelectedSensors() + }, + }) + } + + private selectSensor(slot: number, sensor: sensors.Sensor): void { + const existing = this.slotForSensor(sensor) + if (existing >= 0) { + // Re-picking a slot's own sensor clears it: + if (existing === slot) this.selectedSensors.splice(slot, 1) + this.syncActionsWithSelectedSensors() + return + } + if (slot < this.selectedSensors.length) this.selectedSensors[slot] = sensor + else this.selectedSensors.push(sensor) + this.syncActionsWithSelectedSensors() + } + + private toMs(value: number, unit: string): number { + return value * TIME_UNIT_MS[TIME_UNITS.indexOf(unit)] + } + } +} diff --git a/sensorPicker.ts b/sensorPicker.ts new file mode 100644 index 0000000..7b3b2e8 --- /dev/null +++ b/sensorPicker.ts @@ -0,0 +1,77 @@ +namespace microdata { + const SENSOR_SELECT_PICKER_SCOPE = "select-sensor" + const SENSOR_SELECT_PICKER_COLUMNS = 5 + const SENSOR_SELECT_PICKER_ITEM = 28 + const SENSOR_SELECT_PICKER_GAP = 4 + + // Selected sensors get a thick yellow rounded border. + const SENSOR_SELECTED_STYLE: ui.UiButtonStyle = { + backgroundColor: 1, + frame: "roundedRect", + borderColor: 5, + borderThickness: 3, + } + + interface SensorChoice { + sensor: sensors.Sensor + } + + export interface SensorPickerOptions { + screen: ui.UiScreen + // Re-read after pruning, so it must reflect live caller state. + selected: () => sensors.Sensor[] + pickedSensors?: sensors.Sensor[] + maxSelectableSensorNum?: number + onPick: (sensor: sensors.Sensor) => void + // One call per selected sensor that has left the bus, highest index first + onUnpick?: (index: number) => void + } + + export function getAvailableMicrobitAndJacdacSensors(): sensors.Sensor[] { + return sensors.getAllMicrobitSensors().concat(sensors.getAllConnectedJacdacSimpleSensors()) + } + + // Prunes disconnected selections + // uses sensorNameToBitmap from assets + export function openSensorPicker(getAvailableSensorsFn: () => sensors.Sensor[], options: SensorPickerOptions): void { + const availableSensors = getAvailableSensorsFn() + + // Check if a sensor was unselected: + const stale = options.selected() + for (let i = stale.length - 1; i >= 0; i--) + if (!availableSensors.some(s => s.name === stale[i].name) && options.onUnpick) + options.onUnpick(i) + + const selectedSensors: sensors.Sensor[] = options.selected() + const pickedSensors: sensors.Sensor[] = options.pickedSensors || [] + const full = (options.maxSelectableSensorNum !== undefined) && (selectedSensors.length >= options.maxSelectableSensorNum) + + const controls: ui.UiControl[] = + availableSensors.map((sensor: sensors.Sensor, i: number) => { + const isSelected = selectedSensors.some(s => s.name === sensor.name) + const pickable = !pickedSensors.some(s => s.name === sensor.name) && (isSelected || !full) + + return { + id: `sensor-${i}`, + value: { sensor }, + focusLabel: pickable ? sensor.name : `${sensor.name} (in use)`, + bitmap: sensorNameToBitmap(sensor.name, sensor.isJacdacSensor), + style: isSelected ? SENSOR_SELECTED_STYLE : undefined, + focusable: pickable, + } + }) + + options.screen.openModal(new ui.UiPicker({ + modalScopeId: SENSOR_SELECT_PICKER_SCOPE, + title: "Sensors", + controls, + columnCount: SENSOR_SELECT_PICKER_COLUMNS, + controlSize: { width: SENSOR_SELECT_PICKER_ITEM, height: SENSOR_SELECT_PICKER_ITEM }, + columnGap: SENSOR_SELECT_PICKER_GAP, + rowGap: SENSOR_SELECT_PICKER_GAP, + controlStyle: ui.UiButtonStyles.LightShadowedWhite, + closeOnActivate: true, + onActivate: (choice: SensorChoice) => options.onPick(choice.sensor), + })) + } +} diff --git a/simpleSensorSelect.ts b/simpleSensorSelect.ts new file mode 100644 index 0000000..bd85cfa --- /dev/null +++ b/simpleSensorSelect.ts @@ -0,0 +1,52 @@ +namespace microdata { + export class SimpleSensorSelect extends ui.UiScreen { + private selectedSensors: sensors.Sensor[]; + + constructor(runtime: ui.UiRuntime) { + super(runtime) + this.backgroundColor = 6 + this.selectedSensors = [] + + this.add( + new ui.UiButton( + "sensors", + "Sensors", + () => { + openSensorPicker( + getAvailableMicrobitAndJacdacSensors, { + screen: this, + selected: () => this.selectedSensors, + // Example of how to do more complex behaviours, in this case preventing deselction only of the last picked: + // pickedSensors: this.selectedSensors.length == 0 ? [] : this.selectedSensors.slice(-1), + onPick: sensor => this.selectSensor(sensor), + onUnpick: index => { + this.selectedSensors.splice(index, 1) + }, + }) + } + ), + {centerX: 80, centerY: 60} + ) + } + + private selectSensor(sensor: sensors.Sensor): void { + const selectedSensorIdx = this.selectedSensors.map(s => s.name).indexOf(sensor.name) + if (selectedSensorIdx === -1) + this.selectedSensors.push(sensor) + else + this.selectedSensors.splice(selectedSensorIdx, 1) + } + + handleInput(event: ui.UiInputEvent) { + if (event.action == "cancel" && event.phase == "pressed") { + this.runtime.pop() + return true; + } + return undefined; + } + + render(surface: ui.DrawSurface) { + super.render(surface) + } + } +}