diff --git a/app.ts b/app.ts index 3479372..7c33697 100644 --- a/app.ts +++ b/app.ts @@ -1,57 +1,84 @@ namespace microdata { - import AppInterface = user_interface_base.AppInterface - import Scene = user_interface_base.Scene - import SceneManager = user_interface_base.SceneManager + import AppInterface = user_interface_base.AppInterface + import Scene = user_interface_base.Scene + import SceneManager = user_interface_base.SceneManager - // Auto-save slot - export const SAVESLOT_AUTO = "sa" - export interface SavedState { - progdef: any - version?: string + /** + * Used to control the flow between scenes, + * The SensorSelect scene is used to set the sensors before the RecordData, DistributedLogging and LiveDataViewer scenes + * This enum may be passed to the constructors of these scenes so that they can dynamically control this flow. + * + */ + export enum MicroDataSceneEnum { + LiveDataViewer, + SensorSelect, + RecordingConfigSelect, + RecordData, + DistributedLogging + } + + // Auto-save slot + export const SAVESLOT_AUTO = "sa" + + export interface SavedState { + progdef: any + version?: string + } + + // application configuration + // user_interface_base.getIcon = (id) => Icons..get(id) + user_interface_base.getIcon = (id) => microdata.Icons.get(id) + user_interface_base.resolveTooltip = (ariaId: string) => ariaId + + /** + * If an Arcade Shield is not present when starting MicroData that Microbit will enter DistributedLoggingProtocol. + * It will show a :) on its LEDs and try to become a Target - where it will receive radio commands from a Commander Microbit (one with an Arcade Shield) + */ + export class App implements AppInterface { + sceneManager: SceneManager + + constructor() { + // One interval delay to ensure all static constructors have executed. + basic.pause(10) + reportEvent("app.start") + + this.sceneManager = new SceneManager() + datalogger.includeTimestamp(FlashLogTimeStampFormat.None) + + // datalogger.deleteLog(datalogger.DeleteType.Fast) + // for (let i = 0; i < 400; i++) { + // datalogger.log( + // datalogger.createCV("Sensor", "testtest"), + // datalogger.createCV("Time (ms)", i * 1000), + // datalogger.createCV("Reading", (i * 43) % 5000), + // datalogger.createCV("Event", "N/A") + // ) + // basic.pause(1) + // } + // this.pushScene(new microdata.TabularDataViewer(this, () => {})); + + const arcadeShieldConnected = shieldhelpers.shieldPresent(); + if (arcadeShieldConnected) + this.pushScene(new microdata.Home(this)); + else + new HeadlessMode(); + } + + public pushScene(scene: Scene) { + this.sceneManager.pushScene(scene) + } + + public popScene() { + this.sceneManager.popScene() + } + + public save(slot: string, buffer: Buffer): boolean { + return true; } - // application configuration - // user_interface_base.getIcon = (id) => icons.get(id) - user_interface_base.getIcon = (id) => user_interface_base.icons.get(id) - user_interface_base.resolveTooltip = (ariaId: string) => ariaId - - /** - * If an Arcade Shield is not present when starting MicroData that Microbit will enter DistributedLoggingProtocol. - * It will show a :) on its LEDs and try to become a Target - where it will receive radio commands from a Commander Microbit (one with an Arcade Shield) - */ - export class App implements AppInterface { - sceneManager: SceneManager - - constructor() { - // One interval delay to ensure all static constructors have executed. - basic.pause(10) - reportEvent("app.start") - - this.sceneManager = new SceneManager() - datalogger.includeTimestamp(FlashLogTimeStampFormat.None) - - const arcadeShieldConnected = shieldhelpers.shieldPresent(); - if (arcadeShieldConnected) - this.pushScene(new microdata.Home(this)); - else - new HeadlessMode(this); - } - - public pushScene(scene: Scene) { - this.sceneManager.pushScene(scene) - } - - public popScene() { - this.sceneManager.popScene() - } - - public save(slot: string, buffer: Buffer): boolean { - return true; - } - - public load(slot: string): Buffer { - return Buffer.create(0) - } + public load(slot: string): Buffer { + return Buffer.create(0) } + } } diff --git a/assets.ts b/assets.ts index 9c601f2..28e2f3c 100644 --- a/assets.ts +++ b/assets.ts @@ -6,11 +6,13 @@ namespace microdata { } - export class icons { - public static get(name: string, nullIfMissing = false): Bitmap { + export class Icons { + public static get(name: string | number, nullIfMissing = false): Bitmap { if (name == "microdataLogo") return microdataLogo - return user_interface_base.icons.get(name, nullIfMissing); + if (typeof name === "string") + return user_interface_base.icons.get(name, nullIfMissing) + return MISSING } } @@ -38,4 +40,24 @@ namespace microdata { ....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 diff --git a/clearDataloggerScreen.ts b/clearDataloggerScreen.ts index 15c20de..632deaa 100644 --- a/clearDataloggerScreen.ts +++ b/clearDataloggerScreen.ts @@ -15,10 +15,10 @@ namespace microdata { // Data logger already empty: if (datalogger.getNumberOfRows(0) <= 1) { this.app.popScene() - this.app.pushScene(new SensorSelect(this.app, CursorSceneEnum.RecordingConfigSelect)) + this.app.pushScene(new SensorSelect(this.app, MicroDataSceneEnum.RecordingConfigSelect)) } - this.yesBtn = new Sprite({ img: icons.get("tile_button_a") }) + 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 @@ -26,7 +26,7 @@ namespace microdata { this.yesBtn.xfrm.localPos.x = -39 this.yesBtn.xfrm.localPos.y = 20 - this.noBtn = new Sprite({ img: icons.get("tile_button_b") }) + 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 @@ -41,7 +41,7 @@ namespace microdata { this.unbindButtons() - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.A.id, () => { @@ -51,16 +51,16 @@ namespace microdata { datalogger.deleteLog(datalogger.DeleteType.Fast) this.app.popScene() - this.app.pushScene(new SensorSelect(this.app, CursorSceneEnum.RecordingConfigSelect)) + this.app.pushScene(new SensorSelect(this.app, MicroDataSceneEnum.RecordingConfigSelect)) } ) - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.B.id, () => { this.app.popScene() - this.app.pushScene(new SensorSelect(this.app, CursorSceneEnum.RecordingConfigSelect)) + this.app.pushScene(new SensorSelect(this.app, MicroDataSceneEnum.RecordingConfigSelect)) } ) } @@ -71,12 +71,12 @@ namespace microdata { * Invocations to other functions can be particularly prone to crashing if during datalogger.deleteLog() */ private unbindButtons() { - control.onEvent(ControllerButtonEvent.Pressed, controller.A.id, () => { }) - control.onEvent(ControllerButtonEvent.Pressed, controller.B.id, () => { }) - control.onEvent(ControllerButtonEvent.Pressed, controller.left.id, () => { }) - control.onEvent(ControllerButtonEvent.Pressed, controller.right.id, () => { }) - control.onEvent(ControllerButtonEvent.Pressed, controller.up.id, () => { }) - control.onEvent(ControllerButtonEvent.Pressed, controller.down.id, () => { }) + context.onEvent(ControllerButtonEvent.Pressed, controller.A.id, () => { }) + context.onEvent(ControllerButtonEvent.Pressed, controller.B.id, () => { }) + context.onEvent(ControllerButtonEvent.Pressed, controller.left.id, () => { }) + context.onEvent(ControllerButtonEvent.Pressed, controller.right.id, () => { }) + context.onEvent(ControllerButtonEvent.Pressed, controller.up.id, () => { }) + context.onEvent(ControllerButtonEvent.Pressed, controller.down.id, () => { }) } draw() { diff --git a/dataRecorder.ts b/dataRecorder.ts index b00488b..cf492cf 100644 --- a/dataRecorder.ts +++ b/dataRecorder.ts @@ -56,7 +56,7 @@ namespace microdata { //--------------- // Go Back: - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.B.id, () => { @@ -72,7 +72,7 @@ namespace microdata { ) // Clear whatever A was previously bound to - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.A.id, () => { @@ -80,6 +80,7 @@ namespace microdata { this.currentlyCancelling = true this.scheduler.stop() + basic.pause(1000) this.app.popScene() this.app.pushScene(new Home(this.app)) } @@ -87,7 +88,7 @@ namespace microdata { ) // Scroll Up - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.up.id, () => { @@ -101,7 +102,7 @@ namespace microdata { ) // Scroll Down - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.down.id, () => { @@ -117,7 +118,7 @@ namespace microdata { // For cancelling the current recording: - this.yesBtn = new Sprite({ img: icons.get("tile_button_a") }) + 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 @@ -125,7 +126,7 @@ namespace microdata { this.yesBtn.xfrm.localPos.x = -40 this.yesBtn.xfrm.localPos.y = 12 - this.noBtn = new Sprite({ img: icons.get("tile_button_b") }) + 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 diff --git a/dataViewSelect.ts b/dataViewSelect.ts index 2f01dd4..27f5228 100644 --- a/dataViewSelect.ts +++ b/dataViewSelect.ts @@ -1,125 +1,132 @@ namespace microdata { - import Screen = user_interface_base.Screen - import CursorSceneWithPriorPage = user_interface_base.CursorSceneWithPriorPage - import CursorSceneEnum = user_interface_base.CursorSceneEnum - import Button = user_interface_base.Button - import ButtonStyles = user_interface_base.ButtonStyles - import AppInterface = user_interface_base.AppInterface - - /** - * Choose between: - * Resetting Datalogger - * A tabular view of the recorded data - * A graph of the recorded data - */ - export class DataViewSelect extends CursorSceneWithPriorPage { - private dataloggerEmpty: boolean - - constructor(app: AppInterface) { - super(app, - function () { - this.app.popScene(); - this.app.pushScene(new Home(this.app)) - }) - } + 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 - /* override */ startup() { - super.startup() - basic.pause(50); - - // Includes the header: - this.dataloggerEmpty = datalogger.getNumberOfRows() <= 1 - - //--------- - // Control: - //--------- - - // No data in log (first row are headers) - if (this.dataloggerEmpty) { - control.onEvent( - ControllerButtonEvent.Pressed, - controller.A.id, - () => { - this.app.popScene() - this.app.pushScene(new SensorSelect(this.app, CursorSceneEnum.RecordingConfigSelect)) - } - ) - } + /** + * Choose between: + * Resetting Datalogger + * A tabular view of the recorded data + * Jacdac light experiment + */ + export class DataViewSelect extends CursorScene { + private dataloggerEmpty: boolean - const y = Screen.HEIGHT * 0.234 // y = 30 on an Arcade Shield of height 128 pixels - - this.navigator.setBtns([[ - new Button({ - parent: null, - style: ButtonStyles.Transparent, - icon: "largeDisk", - ariaId: "View Data", - x: -50, - y, - onClick: () => { - this.app.popScene() - this.app.pushScene(new TabularDataViewer(this.app, function () {this.app.popScene(); this.app.pushScene(new DataViewSelect(this.app))})) - }, - }), - - new Button({ - parent: null, - style: ButtonStyles.Transparent, - icon: "linear_graph_1", - ariaId: "View Graph", - x: 0, - y, - onClick: () => { - this.app.popScene() - this.app.pushScene(new GraphGenerator(this.app)) - }, - }), - - new Button({ - parent: null, - style: ButtonStyles.Transparent, - icon: "largeSettingsGear", - ariaId: "Reset Datalogger", - x: 50, - y, - onClick: () => { - datalogger.deleteLog() - this.dataloggerEmpty = true - - control.onEvent( - ControllerButtonEvent.Pressed, - controller.A.id, - () => { - this.app.popScene() - this.app.pushScene(new SensorSelect(this.app, CursorSceneEnum.RecordingConfigSelect)) - } - ) - }, - }) - ]]) - } + constructor(app: AppInterface) { + super(app); + } - draw() { - Screen.fillRect( - Screen.LEFT_EDGE, - Screen.TOP_EDGE, - Screen.WIDTH, - Screen.HEIGHT, - 0xC - ) - - if (this.dataloggerEmpty) { - screen().printCenter("No data has been recorded", 5) - screen().printCenter("Press A to Record some!", Screen.HALF_HEIGHT) - return; - } + /* override */ startup() { + super.startup() + basic.pause(50); + + // Includes the header: + this.dataloggerEmpty = datalogger.getNumberOfRows() <= 1 + + const y = Screen.HEIGHT * 0.234 // y = 30 on an Arcade Shield of height 128 pixels + + let btns: Button[][] = [[]]; + + if (this.dataloggerEmpty) { + btns[0].push(new Button({ + parent: null, + style: ButtonStyles.Transparent, + icon: "edit_program", + ariaId: "Log Data", + x: -50, + y, + onClick: () => { + this.app.popScene() + this.app.pushScene(new SensorSelect(this.app, MicroDataSceneEnum.RecordingConfigSelect)) + }, + })) + } else { + btns[0].push(new Button({ + parent: null, + style: ButtonStyles.Transparent, + icon: "largeDisk", + ariaId: "View Data", + x: -50, + y, + onClick: () => { + this.app.popScene() + this.app.pushScene(new TabularDataViewer(this.app, () => { this.app.popScene(); this.app.pushScene(new DataViewSelect(this.app)) })) + } + })) + } - else { - screen().printCenter("Recorded Data Options", 5) - this.navigator.drawComponents(); + btns[0].push(new Button({ + parent: null, + style: ButtonStyles.Transparent, + icon: "linear_graph_1", + ariaId: "Jacdac Light Experiment", + x: 0, + y, + onClick: () => { + this.app.popScene() + this.app.pushScene(new JacdacLightExperiment(this.app)) + }, + })) + + btns[0].push(new Button({ + parent: null, + style: ButtonStyles.Transparent, + icon: "largeSettingsGear", + ariaId: "Reset Datalogger", + x: 50, + y, + onClick: () => { + datalogger.deleteLog() + this.dataloggerEmpty = true + + context.onEvent( + ControllerButtonEvent.Pressed, + controller.A.id, + () => { + this.app.popScene() + this.app.pushScene(new SensorSelect(this.app, MicroDataSceneEnum.RecordingConfigSelect)) } + ) + }, + })) + this.navigator.setBtns(btns) - super.draw() + //--------- + // Control: + //--------- + + context.onEvent( + ControllerButtonEvent.Pressed, + controller.B.id, + () => { + this.app.popScene() + this.app.pushScene(new Home(this.app)) } + ) + } + + draw() { + Screen.fillRect( + Screen.LEFT_EDGE, + Screen.TOP_EDGE, + Screen.WIDTH, + Screen.HEIGHT, + 0xC + ) + + if (this.dataloggerEmpty) { + screen().printCenter("No data has been recorded", 5) + screen().printCenter("Log Data to collect some!", Screen.HALF_HEIGHT - 30) + } + + else { + screen().printCenter("View Data, Experiment or Clear Data", 5) + } + + this.navigator.drawComponents(); + super.draw() } + } } diff --git a/distributedLogging.ts b/distributedLogging.ts index 185306d..238a8d9 100644 --- a/distributedLogging.ts +++ b/distributedLogging.ts @@ -1,7 +1,7 @@ namespace microdata { import Screen = user_interface_base.Screen import CursorScene = user_interface_base.CursorScene - import CursorSceneEnum = user_interface_base.CursorSceneEnum + import Button = user_interface_base.Button import ButtonStyles = user_interface_base.ButtonStyles import AppInterface = user_interface_base.AppInterface @@ -565,7 +565,7 @@ namespace microdata { if (DistributedLoggingScreen.showTabularData) { this.app.popScene() - this.app.pushScene(new TabularDataViewer(this.app, function () { this.app.popScene(); this.app.pushScene(new DistributedLoggingScreen(this.app)) })) + this.app.pushScene(new TabularDataViewer(this.app, () => { this.app.popScene(); this.app.pushScene(new DistributedLoggingScreen(this.app)) })) } } } @@ -578,7 +578,7 @@ namespace microdata { super.startup() basic.pause(50); - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.B.id, () => { @@ -637,7 +637,7 @@ namespace microdata { DistributedLoggingScreen.streamDataBack = false this.app.popScene() - this.app.pushScene(new SensorSelect(this.app, CursorSceneEnum.DistributedLogging)) + this.app.pushScene(new SensorSelect(this.app, MicroDataSceneEnum.DistributedLogging)) } } }), @@ -654,7 +654,7 @@ namespace microdata { DistributedLoggingScreen.streamDataBack = true this.app.popScene() - this.app.pushScene(new SensorSelect(this.app, CursorSceneEnum.DistributedLogging)) + this.app.pushScene(new SensorSelect(this.app, MicroDataSceneEnum.DistributedLogging)) } }, flipIcon: true @@ -670,7 +670,7 @@ namespace microdata { onClick: () => { if (DistributedLoggingScreen.showTabularData) { this.app.popScene(); - this.app.pushScene(new TabularDataViewer(this.app, function () { this.app.popScene(); this.app.pushScene(new DistributedLoggingScreen(this.app)) })); + this.app.pushScene(new TabularDataViewer(this.app, () => { this.app.popScene(); this.app.pushScene(new DistributedLoggingScreen(this.app)) })); } }, }) diff --git a/experiments.ts b/experiments.ts new file mode 100644 index 0000000..0cc049f --- /dev/null +++ b/experiments.ts @@ -0,0 +1,42 @@ +namespace microdata { + import AppInterface = user_interface_base.AppInterface + import Scene = user_interface_base.Scene + + export class JacdacLightExperiment extends Scene { + constructor(app: AppInterface) { + super(app); + } + + private setupJacdacSensors() { + modules.led1.start(); + modules.color1.start(); + modules.ledStrip1.start(); + modules.ledStrip1.setBrightness(75) + + // A: + input.onButtonPressed(1, () => { + const red = 0xFF0000 + for (let i = 0; i < modules.led1.numPixels(); i++) { + modules.led1.setPixelColor(i, red) + } + modules.ledStrip1.setAll(red) + modules.color1.red() + }) + + // B: + input.onButtonPressed(2, () => { + const blue = 0x0000FF + for (let i = 0; i < modules.led1.numPixels(); i++) { + modules.led1.setPixelColor(i, blue) + } + modules.ledStrip1.setAll(blue) + modules.color1.blue() + }) + } + + /* overide*/ startup() { + this.setupJacdacSensors(); + this.app.pushScene(new LiveDataViewer(this.app, [Sensor.getFromName("Light")])) + } + } +} diff --git a/generateGraph.ts b/generateGraph.ts index ef5c591..0fe222f 100644 --- a/generateGraph.ts +++ b/generateGraph.ts @@ -128,12 +128,12 @@ namespace microdata { // Unbind all controls - since .processReadings() may take some time if there are an immense amount of readings: // Pressing a button during this early stage of processing may crash: - control.onEvent(ControllerButtonEvent.Pressed, controller.up.id, () => {}); - control.onEvent(ControllerButtonEvent.Pressed, controller.down.id,() => {}); - control.onEvent(ControllerButtonEvent.Pressed, controller.left.id,() => {}); - control.onEvent(ControllerButtonEvent.Pressed, controller.right.id,() => {}); - control.onEvent(ControllerButtonEvent.Pressed, controller.A.id,() => {}); - control.onEvent(ControllerButtonEvent.Pressed, controller.B.id,() => {}); + context.onEvent(ControllerButtonEvent.Pressed, controller.up.id, () => {}); + context.onEvent(ControllerButtonEvent.Pressed, controller.down.id,() => {}); + context.onEvent(ControllerButtonEvent.Pressed, controller.left.id,() => {}); + context.onEvent(ControllerButtonEvent.Pressed, controller.right.id,() => {}); + context.onEvent(ControllerButtonEvent.Pressed, controller.A.id,() => {}); + context.onEvent(ControllerButtonEvent.Pressed, controller.B.id,() => {}); this.lowestPeriod = 0; this.greatestPeriod = 0; @@ -145,7 +145,7 @@ namespace microdata { // Bind Controls: //--------------- - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.up.id, () => { @@ -162,7 +162,7 @@ namespace microdata { } ) - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.down.id, () => { @@ -180,7 +180,7 @@ namespace microdata { } ) - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.left.id, () => { @@ -193,7 +193,7 @@ namespace microdata { } ) - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.right.id, () => { @@ -207,7 +207,7 @@ namespace microdata { ) // Select/Deselect a sensor to be drawn: - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.A.id, () => { @@ -219,7 +219,7 @@ namespace microdata { } ) - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.B.id, () => { @@ -675,4 +675,4 @@ namespace microdata { ) } } -} \ No newline at end of file +} diff --git a/headlessMode.ts b/headlessMode.ts index eac82e3..ea65bef 100644 --- a/headlessMode.ts +++ b/headlessMode.ts @@ -1,379 +1,301 @@ namespace microdata { - /** - * This is presently unused on this /main branch due to memory constraints. The distributedLoggingProtocol is used in its stead. - * The no-jacdac-mode uses this feature fully, but with no Jacdac support. - * - * This version supports the Radio logging in addition to its other logging modes. - * - * In the future there shouldn't be a trade-off between these features. - */ - - - /** - * Sensor Selection cycles between 'animations' of the options; animations are an LED loop specific to that sensor. - * Mutated by the A & B button - */ - const enum UI_MODE { - SENSOR_SELECTION, - LOGGING - }; - - - /** - * Represents the internal state of the UI diplay when in SENSOR_SELECTION UI_MODE; - * - * Mutated by the A & B button & .dynamicSensorSelectionLoop() - * - * Which LED should be shown and what Sensor object should it be converted to when complete. - * see .uiSelectionToSensor() - * - * Notice the RADIO element; which is not a sensor; see .uiSelectionToSensor() since it is handled differently. - */ - const enum UI_SENSOR_SELECT_STATE { - ACCELERATION, - TEMPERATURE, - LIGHT, - MAGNET, - RADIO - }; - - /** For module inside of B button. */ - const UI_SENSOR_SELECT_STATE_LEN = 5; - /** How long should each LED picture be shown for? Series of pictures divide this by how many there are. */ - const SHOW_EACH_SENSOR_FOR_MS: number = 1000; - - /** - * Simple class to enable the use of MicroData w/o an Arcade Shield for recording data for the sensors listed in UI_SENSOR_SELECT_STATE. - * Invoked if an arcade shield is not detected from app.ts - * The LED is used to represent sensor options, the user can press A to select one; which starts logging. - * Or press B to move onto the next one. - * - * Logging happens every second and is indefinite. The user may cancel the logging via the B button. - * - * Whilst the sensors are cycled between the sensor being displayed may dynamically update if the readings from that sensor are in excess. - * See .dynamicSensorSelectionLoop() - * It checks all sensors inside UI_SENSOR_SELECT_STATE; if there is one that has a reading beyond the threshold then it will switch this.uiSensorSelectState to that sensor. - * This allows the user to cycle between UI elements phsyically - by shining light on or shaking the microbit. - * - * Fibers and special waiting functions .waitUntilSensorSelectStateChange & .waitUntilUIModeChanges are required to maintain low-latency and the dynamic behaviour described above. - */ - export class HeadlessMode { - private app: App; - /** Mutated by the A & B button */ - private uiMode: UI_MODE; - /** Mutated by the B button & .dynamicSensorSelectionLoop() */ - private uiSensorSelectState: UI_SENSOR_SELECT_STATE; - - constructor(app: App) { - this.app = app; - this.uiMode = UI_MODE.SENSOR_SELECTION; - this.uiSensorSelectState = UI_SENSOR_SELECT_STATE.ACCELERATION; - - // A Button - input.onButtonPressed(1, () => { - if (this.uiMode == UI_MODE.SENSOR_SELECTION) { - this.uiMode = UI_MODE.LOGGING; - this.log(); - } - }) - - // B Button - input.onButtonPressed(2, () => { - if (this.uiMode == UI_MODE.SENSOR_SELECTION) - this.uiSensorSelectState = (this.uiSensorSelectState + 1) % UI_SENSOR_SELECT_STATE_LEN - else if (this.uiMode == UI_MODE.LOGGING) { - this.uiMode = UI_MODE.SENSOR_SELECTION; - this.dynamicSensorSelectionLoop(); - this.showSensorIcon(); - } - }) - - this.dynamicSensorSelectionLoop(); - this.showSensorIcon(); + /** + * Sensor Selection cycles between 'animations' of the options; animations are an LED loop specific to that sensor. + * Mutated by the A & B button + */ + const enum UI_MODE { + SENSOR_SELECTION, + LOGGING + }; + + + /** + * Represents the internal state of the UI diplay when in SENSOR_SELECTION UI_MODE; + * + * Mutated by the A & B button & .dynamicSensorSelectionLoop() + * + * Which LED should be shown and what Sensor object should it be converted to when complete. + * see .uiSelectionToSensor() + * + * Notice the RADIO element; which is not a sensor; see .uiSelectionToSensor() since it is handled differently. + */ + const enum UI_SENSOR_SELECT_STATE { + ACCELERATION, + TEMPERATURE, + LIGHT, + MAGNET + }; + + const sensorEventThresholds: { [id: number]: number } = { + [UI_SENSOR_SELECT_STATE.ACCELERATION]: 300, // in milli-g for 2g (-2048 to 2047) + [UI_SENSOR_SELECT_STATE.TEMPERATURE]: 1, + [UI_SENSOR_SELECT_STATE.LIGHT]: 25, + [UI_SENSOR_SELECT_STATE.MAGNET]: 100, + } + + /** For module inside of B button. */ + const UI_SENSOR_SELECT_STATE_LEN = 4; + /** How long should each LED picture be shown for? Series of pictures divide this by how many there are. */ + const SHOW_EACH_SENSOR_FOR_MS: number = 1000; + + /** + * Simple class to enable the use of MicroData w/o an Arcade Shield for recording data for the sensors listed in UI_SENSOR_SELECT_STATE. + * Invoked if an arcade shield is not detected from app.ts + * The LED is used to represent sensor options, the user can press A to select one; which starts logging. + * Or press B to move onto the next one. + * + * Logging happens every second and is indefinite. The user may cancel the logging via the B button. + * + * Whilst the sensors are cycled between the sensor being displayed may dynamically update if the readings from that sensor are in excess. + * See .dynamicSensorSelectionLoop() + * It checks all sensors inside UI_SENSOR_SELECT_STATE; if there is one that has a reading beyond the threshold then it will switch this.uiSensorSelectState to that sensor. + * This allows the user to cycle between UI elements phsyically - by shining light on or shaking the microbit. + * + * Fibers and special waiting functions .waitUntilSensorSelectStateChange & .waitUntilUIModeChanges are required to maintain low-latency and the dynamic behaviour described above. + */ + export class HeadlessMode { + /** Mutated by the A & B button */ + private uiMode: UI_MODE; + /** Mutated by the B button & .dynamicSensorSelectionLoop() */ + private uiSensorSelectState: UI_SENSOR_SELECT_STATE; + + constructor() { + this.uiMode = UI_MODE.SENSOR_SELECTION; + this.uiSensorSelectState = UI_SENSOR_SELECT_STATE.ACCELERATION; + datalogger.deleteLog(datalogger.DeleteType.Fast) + + + // A Button + input.onButtonPressed(1, () => { + if (this.uiMode == UI_MODE.SENSOR_SELECTION) { + this.uiMode = UI_MODE.LOGGING; + } else if (this.uiMode == UI_MODE.LOGGING) { + this.uiMode = UI_MODE.SENSOR_SELECTION; } - - - /** - * Runs in background fiber. - * Polls all UI_SENSOR_SELECT_STATE except RADIO for abormally high readings. - * If the reading is beyond the threshold then this.uiSensorSelectState is mutated. - * - * Turned off if not in UI_MODE.SENSOR_SELECTION - * - * Invoked at start and when moving back from logging via pressing the B button. - */ - private dynamicSensorSelectionLoop() { - const dynamicInfo = [ - { sensor: Sensor.getFromName("Accel. X"), uiState: UI_SENSOR_SELECT_STATE.ACCELERATION, threshold: 0.25 }, - { sensor: Sensor.getFromName("Accel. Y"), uiState: UI_SENSOR_SELECT_STATE.ACCELERATION, threshold: 0.25 }, - { sensor: Sensor.getFromName("Accel. Z"), uiState: UI_SENSOR_SELECT_STATE.ACCELERATION, threshold: 0.25 }, - { sensor: Sensor.getFromName("Light"), uiState: UI_SENSOR_SELECT_STATE.LIGHT, threshold: 0.85 }, - { sensor: Sensor.getFromName("Magnet"), uiState: UI_SENSOR_SELECT_STATE.MAGNET, threshold: 0.80 }, - ]; - - // Don't trigger the same sensor selection twice in a row: - let ignore: boolean[] = dynamicInfo.map(_ => false); - control.inBackground(() => { - while (this.uiMode == UI_MODE.SENSOR_SELECTION) { - dynamicInfo.forEach((info, idx) => { - if (!ignore[idx] && info.sensor.getNormalisedReading() > info.threshold) { - this.uiSensorSelectState = info.uiState; - - ignore = dynamicInfo.map(_ => false); - ignore[idx] = true; - basic.pause(1000) - } - basic.pause(100) - }) - basic.pause(100) - } - return; - }) + }) + + // B Button + input.onButtonPressed(2, () => { + if (this.uiMode == UI_MODE.SENSOR_SELECTION) + this.uiSensorSelectState = (this.uiSensorSelectState + 1) % UI_SENSOR_SELECT_STATE_LEN + else if (this.uiMode == UI_MODE.LOGGING) { + this.uiMode = UI_MODE.SENSOR_SELECTION; } + }) + this.loop(); + } - //------------------------- - // Special Waiting Methods: - //------------------------- - - /** - * Wait time number of milliseconds but in increments of check_n_times. Exit if initialState changes. - * To show led animations you need to wait inbetween each frame. But you need to switch to another state if a button is pressed immediately. - * used by .showSensorIcon() - * - * @param time milliseconds - * @param check_n_times period = time / check_n_times - * @param initialState this.uiSensorSelectState != causes pre-mature exit; returning false. - * @returns true if neither this.uiSensorSelectState nor this.uiMode changed; meaning that the full time was waited. - */ - private waitUntilSensorSelectStateChange(time: number, check_n_times: number, initialState: UI_SENSOR_SELECT_STATE): boolean { - const period = time / check_n_times; - - for (let n = 0; n < check_n_times; n++) { - if (this.uiSensorSelectState != initialState || this.uiMode != UI_MODE.SENSOR_SELECTION) - return false; - - basic.pause(period) + private loop() { + while (1) { + if (this.uiMode == UI_MODE.SENSOR_SELECTION) { + switch (this.uiSensorSelectState) { + case UI_SENSOR_SELECT_STATE.ACCELERATION: { + // basic.showLeds() requires a '' literal; thus the following is un-loopable: + + basic.showLeds(` + # # # . . + # # . . . + # . # . . + . . . # . + . . . . . + `); + if (!this.waitUntilSensorSelectStateChange((SHOW_EACH_SENSOR_FOR_MS / 3), 10, UI_SENSOR_SELECT_STATE.ACCELERATION)) break; + + basic.showLeds(` + . . # . . + . . # . . + # # # # # + . # # # . + . . # . . + `); + if (!this.waitUntilSensorSelectStateChange((SHOW_EACH_SENSOR_FOR_MS / 3), 10, UI_SENSOR_SELECT_STATE.ACCELERATION)) break; + + basic.showLeds(` + . . # . . + . . # # . + # # # # # + . . # # . + . . # . . + `); + if (!this.waitUntilSensorSelectStateChange((SHOW_EACH_SENSOR_FOR_MS / 3), 10, UI_SENSOR_SELECT_STATE.ACCELERATION)) break; + + break; } - return true; - } - /** - * Wait time number of milliseconds but in increments of check_n_times. Exit if initialState changes. - * To show led animations you need to wait inbetween each frame. But you need to switch to another state if a button is pressed immediately. - * used by .log() - * - * @param time milliseconds - * @param check_n_times period = time / check_n_times - * @param initialState this.uiMode != causes pre-mature exit; returning false. - * @returns true if this.uiMode did not change; meaning that the full time was waited. - */ - private waitUntilUIModeChanges(time: number, check_n_times: number, initialState: UI_MODE): boolean { - const period = time / check_n_times; - - for (let n = 0; n < check_n_times; n++) { - if (this.uiMode != initialState) { - return false; - } - basic.pause(period) + case UI_SENSOR_SELECT_STATE.TEMPERATURE: { + basic.showLeds(` + # . . . . + . . # # . + . # . . . + . # . . . + . . # # . + `); + if (!this.waitUntilSensorSelectStateChange((SHOW_EACH_SENSOR_FOR_MS), 50, UI_SENSOR_SELECT_STATE.TEMPERATURE)) break; + + break; } - return true; - } - - - //----------------- - // Display Methods: - //----------------- - - - /** - * Starts a fiber that loops through UI_SENSOR_SELECT_STATE: - * Showing each as an animation, checking for this.uiMode & this.uiSensorSelectState changes whilst waiting. - * Invoked at start & by the B button if re-entering SENSOR_SELECTION UI_MODE from the LOGGING UI_MODE - */ - private showSensorIcon() { - control.inBackground(() => { - while (this.uiMode == UI_MODE.SENSOR_SELECTION) { - switch (this.uiSensorSelectState) { - case UI_SENSOR_SELECT_STATE.ACCELERATION: { - // basic.showLeds() requires a '' literal; thus the following is un-loopable: - - basic.showLeds(` - # # # . . - # # . . . - # . # . . - . . . # . - . . . . . - `); - if (!this.waitUntilSensorSelectStateChange((SHOW_EACH_SENSOR_FOR_MS / 3), 10, UI_SENSOR_SELECT_STATE.ACCELERATION)) break; - - basic.showLeds(` - . . # . . - . . # . . - # # # # # - . # # # . - . . # . . - `); - if (!this.waitUntilSensorSelectStateChange((SHOW_EACH_SENSOR_FOR_MS / 3), 10, UI_SENSOR_SELECT_STATE.ACCELERATION)) break; - - basic.showLeds(` - . . # . . - . . # # . - # # # # # - . . # # . - . . # . . - `); - if (!this.waitUntilSensorSelectStateChange((SHOW_EACH_SENSOR_FOR_MS / 3), 10, UI_SENSOR_SELECT_STATE.ACCELERATION)) break; - - break; - } - - case UI_SENSOR_SELECT_STATE.TEMPERATURE: { - basic.showLeds(` - # . . . . - . . # # . - . # . . . - . # . . . - . . # # . - `); - if (!this.waitUntilSensorSelectStateChange((SHOW_EACH_SENSOR_FOR_MS), 50, UI_SENSOR_SELECT_STATE.TEMPERATURE)) break; - - break; - } - - case UI_SENSOR_SELECT_STATE.LIGHT: { - basic.showLeds(` - . . . . . - . # # # . - . . # . . - . . . . . - . . # . . - `); - if (!this.waitUntilSensorSelectStateChange((SHOW_EACH_SENSOR_FOR_MS >> 1), 50, UI_SENSOR_SELECT_STATE.LIGHT)) break; - - basic.showLeds(` - . # # # . - . # # # . - . # # # . - . . . . . - . . # . . - `); - if (!this.waitUntilSensorSelectStateChange((SHOW_EACH_SENSOR_FOR_MS >> 1), 50, UI_SENSOR_SELECT_STATE.LIGHT)) break; - break; - } + case UI_SENSOR_SELECT_STATE.LIGHT: { + basic.showLeds(` + . . . . . + . # # # . + . . # . . + . . . . . + . . # . . + `); + if (!this.waitUntilSensorSelectStateChange((SHOW_EACH_SENSOR_FOR_MS >> 1), 50, UI_SENSOR_SELECT_STATE.LIGHT)) break; + + basic.showLeds(` + . # # # . + . # # # . + . # # # . + . . . . . + . . # . . + `); + if (!this.waitUntilSensorSelectStateChange((SHOW_EACH_SENSOR_FOR_MS >> 1), 50, UI_SENSOR_SELECT_STATE.LIGHT)) break; + + break; + } - case UI_SENSOR_SELECT_STATE.MAGNET: { - basic.showLeds(` + case UI_SENSOR_SELECT_STATE.MAGNET: { + basic.showLeds(` . # # # . # # # # # # # . # # . . . . . . . . . . `) - if (!this.waitUntilSensorSelectStateChange((SHOW_EACH_SENSOR_FOR_MS >> 1), 50, UI_SENSOR_SELECT_STATE.MAGNET)) break; + if (!this.waitUntilSensorSelectStateChange((SHOW_EACH_SENSOR_FOR_MS >> 1), 50, UI_SENSOR_SELECT_STATE.MAGNET)) break; - basic.showLeds(` + basic.showLeds(` . # # # . # # # # # # # . # # . . . . . # # . # # `) - if (!this.waitUntilSensorSelectStateChange((SHOW_EACH_SENSOR_FOR_MS >> 1), 50, UI_SENSOR_SELECT_STATE.MAGNET)) break; - - break; - } + if (!this.waitUntilSensorSelectStateChange((SHOW_EACH_SENSOR_FOR_MS >> 1), 50, UI_SENSOR_SELECT_STATE.MAGNET)) break; - case UI_SENSOR_SELECT_STATE.RADIO: { - basic.showLeds(` - . . . . . - . . . . . - . # # # . - # . . . # - . . # . . - `); - if (!this.waitUntilSensorSelectStateChange((SHOW_EACH_SENSOR_FOR_MS >> 1), 50, UI_SENSOR_SELECT_STATE.RADIO)) break; + break; + } - basic.showLeds(` - . # # # . - # . . . # - . # # # . - # . . . # - . . # . . - `); - if (!this.waitUntilSensorSelectStateChange((SHOW_EACH_SENSOR_FOR_MS >> 1), 50, UI_SENSOR_SELECT_STATE.RADIO)) break; - - break; - } - - default: - break; - } - } + default: + break; + } + } else if (this.uiMode == UI_MODE.LOGGING) { + const sensors = this.uiSelectionToSensors(); + let time = 0; + + basic.showLeds(` + . . . . . + . . . . . + . . . . . + . . . . . + . # # # . + `) + + // control.inBackground(() => { + const WAIT_TIME_MS = 30; + let start = input.runningTime(); + + const threshold = this.uiSelectionToSensorEventThresholds(); + let priorReadings: number[] = sensors.map(sensor => sensor.getReading()); + while (this.uiMode == UI_MODE.LOGGING) { + sensors.forEach((sensor, index) => { + // datalogger.log( + // datalogger.createCV("Sensor", sensor.getName()), + // datalogger.createCV("Time (ms)", time), + // datalogger.createCV("Reading", sensor.getReading()), + // datalogger.createCV("Event", "N/A") + // ); + + const reading = sensor.getReading(); + if (Math.abs(reading - priorReadings[index]) > threshold) { + datalogger.log( + datalogger.createCV("Sensor", sensor.getName()), + datalogger.createCV("Time (ms)", time), + datalogger.createCV("Reading", reading), + datalogger.createCV("Event", "delta") + ); + } + priorReadings[index] = reading; }); + time += WAIT_TIME_MS; + + const loop = input.runningTime(); + basic.pause(WAIT_TIME_MS - (loop - start)); + start = loop; + } + + basic.showLeds(` + . . . . . + . # . # . + . . . . . + # . . . # + . # # # . + `) + basic.pause(1000) } + } + } + //------------------------- + // Special Waiting Methods: + //------------------------- - /** - * Get the sensor(s) that the user selected and start logging them. - * Exit if the UI_MODE changes back to SENSOR_SELECTION (upon the user pressing B) - */ - private log() { - const sensors = this.uiSelectionToSensors(); - let time = 0; - - control.inBackground(() => { - while (this.uiMode == UI_MODE.LOGGING) { - let start = input.runningTime(); - sensors.forEach(sensor => { - datalogger.log( - datalogger.createCV("Sensor", sensor.getName()), - datalogger.createCV("Time (ms)", time), - datalogger.createCV("Reading", sensor.getReading()), - datalogger.createCV("Event", "N/A") - ); - }); - - if (this.uiMode == UI_MODE.LOGGING) - basic.showNumber((time / 1000)); - if (!this.waitUntilUIModeChanges(Math.max(0, 1000 - (input.runningTime() - start)), 80, UI_MODE.LOGGING)) break; - time += 1000; - } - return; - }); - } + /** + * Wait time number of milliseconds but in increments of check_n_times. Exit if initialState changes. + * To show led animations you need to wait inbetween each frame. But you need to switch to another state if a button is pressed immediately. + * used by .showSensorIcon() + * + * @param time milliseconds + * @param check_n_times period = time / check_n_times + * @param initialState this.uiSensorSelectState != causes pre-mature exit; returning false. + * @returns true if neither this.uiSensorSelectState nor this.uiMode changed; meaning that the full time was waited. + */ + private waitUntilSensorSelectStateChange(time: number, check_n_times: number, initialState: UI_SENSOR_SELECT_STATE): boolean { + const period = time / check_n_times; + + for (let n = 0; n < check_n_times; n++) { + if (this.uiSensorSelectState != initialState || this.uiMode != UI_MODE.SENSOR_SELECTION) + return false; + + basic.pause(period) + } + return true; + } - /** - * this.uiSensorSelectState -> relevant sensors - * Most are only 1 sensor, but UI_SENSOR_SELECT_STATE.ACCELERATION gives all X,Y,Z sensors. - * - * Special note to UI_SENSOR_SELECT_STATE.RADIO which leaves NoArcadeShieldMode & starts the DistributedLoggingProtocol(). - * - * @returns sensors used by .log() - */ - private uiSelectionToSensors(): Sensor[] { - switch (this.uiSensorSelectState) { - case UI_SENSOR_SELECT_STATE.ACCELERATION: - return [Sensor.getFromName("Accel. X"), Sensor.getFromName("Accel. Y"), Sensor.getFromName("Accel. Z")] + /** + * this.uiSensorSelectState -> relevant sensors + * Most are only 1 sensor, but UI_SENSOR_SELECT_STATE.ACCELERATION gives all X,Y,Z sensors. + * + * Special note to UI_SENSOR_SELECT_STATE.RADIO which leaves NoArcadeShieldMode & starts the DistributedLoggingProtocol(). + * + * @returns sensors used by .log() + */ + private uiSelectionToSensors(): Sensor[] { + switch (this.uiSensorSelectState) { + case UI_SENSOR_SELECT_STATE.ACCELERATION: + return [Sensor.getFromName("Accel. X"), Sensor.getFromName("Accel. Y"), Sensor.getFromName("Accel. Z")] - case UI_SENSOR_SELECT_STATE.TEMPERATURE: - return [Sensor.getFromName("Temp.")] + case UI_SENSOR_SELECT_STATE.TEMPERATURE: + return [Sensor.getFromName("Temp.")] - case UI_SENSOR_SELECT_STATE.LIGHT: - return [Sensor.getFromName("Light")] + case UI_SENSOR_SELECT_STATE.LIGHT: + return [Sensor.getFromName("Light")] - case UI_SENSOR_SELECT_STATE.MAGNET: - return [Sensor.getFromName("Magnet")] + case UI_SENSOR_SELECT_STATE.MAGNET: + return [Sensor.getFromName("Magnet")] - case UI_SENSOR_SELECT_STATE.RADIO: - new DistributedLoggingProtocol(this.app, false); - return [] + default: + return [] + } + } - default: - return [] - } - } + private uiSelectionToSensorEventThresholds(): number { + return sensorEventThresholds[this.uiSensorSelectState]; } + } } diff --git a/home.ts b/home.ts index 487980f..93c4d75 100644 --- a/home.ts +++ b/home.ts @@ -4,7 +4,7 @@ namespace microdata { import Button = user_interface_base.Button import ButtonStyles = user_interface_base.ButtonStyles import AppInterface = user_interface_base.AppInterface - import CursorSceneEnum = user_interface_base.CursorSceneEnum + import font = user_interface_base.font export class Home extends CursorScene { @@ -28,7 +28,7 @@ namespace microdata { y, onClick: () => { this.app.popScene() - this.app.pushScene(new SensorSelect(this.app, CursorSceneEnum.LiveDataViewer)) + this.app.pushScene(new SensorSelect(this.app, MicroDataSceneEnum.LiveDataViewer)) }, }), @@ -41,7 +41,7 @@ namespace microdata { y, onClick: () => { this.app.popScene() - this.app.pushScene(new SensorSelect(this.app, CursorSceneEnum.RecordingConfigSelect)) + this.app.pushScene(new SensorSelect(this.app, MicroDataSceneEnum.RecordingConfigSelect)) }, }), @@ -62,7 +62,7 @@ namespace microdata { parent: null, style: ButtonStyles.Transparent, icon: "largeDisk", - ariaId: "View Data", + ariaId: "View Data & Settings", x: 58, y, onClick: () => { @@ -75,7 +75,7 @@ namespace microdata { private drawVersion() { const font = bitmaps.font5 - const text = "v1.7.4" + const text = "v1.7.5" Screen.print( text, Screen.RIGHT_EDGE - (font.charWidth * text.length), @@ -95,8 +95,8 @@ namespace microdata { 0xc ) - const microbitLogo = icons.get("microbitLogo") - const microdataLogo = icons.get("microdataLogo") + const microbitLogo = Icons.get("microbitLogo") + const microdataLogo = Icons.get("microdataLogo") this.yOffset = Math.min(0, this.yOffset + 2) const t = control.millis() diff --git a/liveDataViewer.ts b/liveDataViewer.ts index f772c08..5ac5b11 100644 --- a/liveDataViewer.ts +++ b/liveDataViewer.ts @@ -44,7 +44,6 @@ namespace microdata { * Multiple sensors may be plotted at once * Display modes may be toggled per sensor * - * * UI elements have been scaled to allow for Arcade Shields of different dimensions. * Where this is the case the raw value for an Arcade Shield of Height 128 is commented alongside it. */ @@ -98,7 +97,6 @@ namespace microdata { /** Greatest of sensor.maximum for all sensors: required to write at the top of the y-axis */ private globalSensorMaximum: number; - constructor(app: AppInterface, sensors: Sensor[]) { super(app, "liveDataViewer") this.backgroundColor = 3 @@ -132,7 +130,7 @@ namespace microdata { this.setGlobalMinAndMax() } - /* override */ startup() { + /* override */ startup() { super.startup() basic.pause(50); @@ -141,7 +139,7 @@ namespace microdata { //-------------------------------- // Zoom in: - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.A.id, () => { @@ -155,7 +153,11 @@ namespace microdata { const sensor = this.sensors[this.oscSensorIndex]; this.oscXCoordinate = Math.round(sensor.getHeightNormalisedBufferLength() >> 1); - this.oscReading = sensor.getNthHeightNormalisedReading(this.oscXCoordinate); + + // Silly: + const yScaledToBufHeight = sensor.getNthHeightNormalisedReading(this.oscXCoordinate) + const yScaledToFullheight = (yScaledToBufHeight / BUFFERED_SCREEN_HEIGHT) * (Screen.HEIGHT) + this.oscReading = yScaledToFullheight; this.windowLeftBuffer = 0; this.windowRightBuffer = 0; @@ -168,7 +170,7 @@ namespace microdata { ) // Zoom out, if not ZOOMED_IN then go back to home - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.B.id, () => { @@ -194,7 +196,7 @@ namespace microdata { } ) - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.up.id, () => { @@ -221,7 +223,7 @@ namespace microdata { } ) - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.down.id, () => { @@ -248,7 +250,7 @@ namespace microdata { } ) - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.left.id, () => { @@ -258,7 +260,7 @@ namespace microdata { // this.update() // For fast response to the above change let tick = true; - control.onEvent( + context.onEvent( ControllerButtonEvent.Released, controller.left.id, () => tick = false @@ -271,18 +273,18 @@ namespace microdata { basic.pause(isFirstTick ? 100 : 33) isFirstTick = false } - control.onEvent(ControllerButtonEvent.Released, controller.left.id, () => { }) + context.onEvent(ControllerButtonEvent.Released, controller.left.id, () => { }) } } ) - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.right.id, () => { if (this.guiState == GUI_STATE.ZOOMED_IN) { let tick = true; - control.onEvent( + context.onEvent( ControllerButtonEvent.Released, controller.right.id, () => tick = false @@ -294,7 +296,7 @@ namespace microdata { basic.pause(isFirstTick ? 100 : 33) isFirstTick = false } - control.onEvent(ControllerButtonEvent.Released, controller.right.id, () => { }) + context.onEvent(ControllerButtonEvent.Released, controller.right.id, () => { }) } } ) diff --git a/loggingConfig.ts b/loggingConfig.ts index 2055fc5..eeb3562 100644 --- a/loggingConfig.ts +++ b/loggingConfig.ts @@ -1,10 +1,11 @@ namespace microdata { import Screen = user_interface_base.Screen import Scene = user_interface_base.Scene - import CursorSceneEnum = user_interface_base.CursorSceneEnum + import AppInterface = user_interface_base.AppInterface import font = user_interface_base.font + /** * Generated at recordingConfigSelection * Passed to and owned by a sensor @@ -90,9 +91,9 @@ namespace microdata { private currentConfigMode: CONFIG_MODE private sensorConfigIsSet: boolean[] - private nextSceneEnum: CursorSceneEnum + private nextSceneEnum: MicroDataSceneEnum - constructor(app: AppInterface, sensors: Sensor[], nextSceneEnum?: CursorSceneEnum) { + constructor(app: AppInterface, sensors: Sensor[], nextSceneEnum?: MicroDataSceneEnum) { super(app, "measurementConfigSelect") this.guiState = GUI_STATE.SENSOR_SELECT @@ -120,7 +121,7 @@ namespace microdata { super.startup() basic.pause(50); - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.A.id, () => { @@ -148,7 +149,7 @@ namespace microdata { this.app.popScene() - if (this.nextSceneEnum == CursorSceneEnum.DistributedLogging) { + if (this.nextSceneEnum == MicroDataSceneEnum.DistributedLogging) { this.app.pushScene(new DistributedLoggingScreen(this.app, this.sensors, this.sensorConfigs)); // Temp disabled with Distributedlogging (no mem) } else { @@ -210,7 +211,7 @@ namespace microdata { } ) - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.B.id, () => { @@ -235,7 +236,7 @@ namespace microdata { } ) - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.up.id, () => { @@ -276,7 +277,7 @@ namespace microdata { } ) - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.down.id, () => { @@ -320,13 +321,22 @@ namespace microdata { } ) - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.left.id, () => { - if (this.guiState == GUI_STATE.SENSOR_SELECT_CONFIG_ROW && this.configurationIndex == CONFIG_ROW.PERIOD_OR_EVENT) + if (this.guiState == GUI_STATE.SENSOR_SELECT_CONFIG_ROW && this.configurationIndex == CONFIG_ROW.PERIOD_OR_EVENT) { this.currentConfigMode = (this.currentConfigMode == CONFIG_MODE.PERIOD) ? CONFIG_MODE.EVENT : CONFIG_MODE.PERIOD + // Reset period values if going back to it from Event. + // This unncessary in the other direction. + if (this.currentConfigMode == CONFIG_MODE.PERIOD) { + this.sensorConfigs[this.sensorIndex].period = 1000 + this.sensorConfigs[this.sensorIndex].inequality = null + this.sensorConfigs[this.sensorIndex].comparator = null + } + } + else if (this.guiState == GUI_STATE.SENSOR_MODIFY_CONFIG_ROW) { switch (this.configurationIndex) { case CONFIG_ROW.MEASUREMENT_QTY: { @@ -347,7 +357,7 @@ namespace microdata { } ) - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.right.id, () => { diff --git a/package.json b/package.json index 678d033..b4f4a53 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { "dependencies": { - "pxt-microbit": "^7.1.13" + "pxt-microbit": "^7.1.56" } } diff --git a/pxt.json b/pxt.json index 8aca04e..856924c 100644 --- a/pxt.json +++ b/pxt.json @@ -7,13 +7,16 @@ "radio": "*", "microphone": "*", "datalogger": "*", - "user-interface-base": "github:microbit-apps/user-interface-base#v0.0.17", + "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-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" }, "files": [ "analytics.ts", @@ -32,7 +35,8 @@ "tabularDataViewer.ts", "generateGraph.ts", "distributedLogging.ts", - "headlessMode.ts" + "headlessMode.ts", + "experiments.ts" ], "testFiles": [ "test.ts" diff --git a/sensorSelect.ts b/sensorSelect.ts index a14a1c9..93deea7 100644 --- a/sensorSelect.ts +++ b/sensorSelect.ts @@ -1,218 +1,409 @@ namespace microdata { import Screen = user_interface_base.Screen import GridNavigator = user_interface_base.GridNavigator - import CursorSceneEnum = user_interface_base.CursorSceneEnum - import CursorSceneWithPriorPage = user_interface_base.CursorSceneWithPriorPage + import CursorScene = user_interface_base.CursorScene import Button = user_interface_base.Button import ButtonStyles = user_interface_base.ButtonStyles import AppInterface = user_interface_base.AppInterface - /** - * Limit to how many sensors you may record from & read from at once. Neccessary to prevent egregious lag in live-data-viewer. - * Inclusively, only one Jacdac sensor may be selected at once. - */ - export const MAX_NUMBER_OF_SENSORS: number = 3 - - /** - * Starting index of contigious row of Jacdac sensors. - * Used to ensure that Jacdac sensors are appropriately enabled/disabled. - */ - const START_OF_JACDAC_BUTTONS_INDEX: number = 14 - - /** - * Responsible for allowing the user to select sensors to record or view live readings from. - * The user may select up to 5 sensors to read from simultaneously including 1 Jacdac sensor. - * These sensors are passed to either the measurement screen or the live data view - */ - export class SensorSelect extends CursorSceneWithPriorPage { - private btns: Button[][] - private selectedSensorAriaIDs: string[] - private nextSceneEnum: CursorSceneEnum - private jacdacSensorSelected: boolean - - constructor(app: AppInterface, nextSceneEnum: CursorSceneEnum) { - super(app, function () { - this.app.popScene(); - this.app.pushScene(new Home(this.app)) - }, new GridNavigator()); - - this.btns = [[], [], [], []]; // For our 4x5 grid - this.selectedSensorAriaIDs = []; - this.nextSceneEnum = nextSceneEnum; - this.jacdacSensorSelected = false; - } + + /** + * Limit to how many sensors you may record from & read from at once. Neccessary to prevent egregious lag in live-data-viewer. + * Inclusively, only one Jacdac sensor may be selected at once. + */ + export const MAX_NUMBER_OF_SENSORS: number = 3 + + /** + * Starting index of contigious row of Jacdac sensors. + * Used to ensure that Jacdac sensors are appropriately enabled/disabled. + */ + const START_OF_JACDAC_BUTTONS_INDEX: number = 14 + const TUTORIAL_MODE: boolean = true; + const NUMBER_OF_TUTORIAL_HINTS: number = 4; + const TUTORIAL_TEXT_BOT_ALIGNMENT = Screen.BOTTOM_EDGE - 34; + const TUTORIAL_TEXT_TOP_ALIGNMENT = -54; + + /** + * Responsible for allowing the user to select sensors to record or view live readings from. + * The user may select up to 5 sensors to read from simultaneously including 1 Jacdac sensor. + * These sensors are passed to either the measurement screen or the live data view + */ + export class SensorSelect extends CursorScene { + private btns: Button[][]; + private selectedSensorAriaIDs: string[]; + private nextSceneEnum: MicroDataSceneEnum; + private jacdacSensorSelected: boolean + private tutorialHintIndex: number; + private userHasPressedABtn: boolean; + private tutorialTextYOffset: number = TUTORIAL_TEXT_BOT_ALIGNMENT; + + constructor(app: AppInterface, nextSceneEnum: MicroDataSceneEnum) { + super(app, new GridNavigator()); + + this.btns = [[], [], [], []]; // For our 4x5 grid + this.selectedSensorAriaIDs = []; + this.nextSceneEnum = nextSceneEnum; + this.jacdacSensorSelected = false; + this.tutorialHintIndex = 0; + this.userHasPressedABtn = false; + } /* override */ startup() { - super.startup() - basic.pause(50); - - this.cursor.resetOutlineColourOnMove = true - const icons: string[] = [ - "accelerometer", "accelerometer", "accelerometer", "right_turn", "right_spin", "pin_0", "pin_1", "pin_2", - "led_light_sensor", "thermometer", "magnet", "finger_press", "microphone", "compass", "microbitLogoWhiteBackground", - "microbitLogoWhiteBackground", "microbitLogoWhiteBackground", "microbitLogoWhiteBackground", "microbitLogoWhiteBackground" - ] - - const ariaIDs: string[] = [ - "Accelerometer X", "Accelerometer Y", "Accelerometer Z", "Pitch", "Roll", "Analog Pin 0", "Analog Pin 1", "Analog Pin 2", "Light", - "Temperature", "Magnet", "Logo Press", "Microphone", "Compass", "Jacdac Flex", "Jacdac Temperature", "Jacdac Light", - "Jacdac Moisture", "Jacdac Distance" - ] - - //----------------------------------------------------- - // Organise buttons in 4x5 grid: same as GridNavigator: - //----------------------------------------------------- - - let x: number = -60; - let y: number = -41 - let iconIndex: number = 0; - - const rowLengths = [5, 5, 5, 4] // Last row has 'Done' button added after this loop: - for (let i = 0; i < 4; i++) { - for (let j = 0; j < rowLengths[i]; j++) { - this.btns[i][j] = new Button({ - parent: null, - style: ButtonStyles.Transparent, - icon: icons[iconIndex], - ariaId: ariaIDs[iconIndex], - x: x, - y: y, - onClick: (button: Button) => { - // Deletion: - const index = this.selectedSensorAriaIDs.indexOf(button.ariaId) - if (index != -1) { - this.cursor.setOutlineColour() - this.selectedSensorAriaIDs.splice(index, 1) - - if (Sensor.getFromName(button.ariaId).isJacdac()) { - this.jacdacSensorSelected = false - this.setOtherJacdacButtonsTo(true) + super.startup(); + this.overrideControllerButtonBindings(); + + this.cursor.resetOutlineColourOnMove = true + const icons: string[] = [ + "accelerometer", "accelerometer", "accelerometer", "right_turn", "right_spin", "pin_0", "pin_1", "pin_2", + "led_light_sensor", "thermometer", "magnet", "finger_press", "microphone", "compass", "microbitLogoWhiteBackground", + "microbitLogoWhiteBackground", "microbitLogoWhiteBackground", "microbitLogoWhiteBackground", "microbitLogoWhiteBackground" + ] + + const ariaIDs: string[] = [ + "Accelerometer X", "Accelerometer Y", "Accelerometer Z", "Pitch", "Roll", "Analog Pin 0", "Analog Pin 1", "Analog Pin 2", "Light", + "Temperature", "Magnet", "Logo Press", "Microphone", "Compass", "Jacdac Flex", "Jacdac Temperature", "Jacdac Light", + "Jacdac Moisture", "Jacdac Distance" + ] + + //----------------------------------------------------- + // Organise buttons in 4x5 grid: same as GridNavigator: + //----------------------------------------------------- + + let x: number = -60; + let y: number = -41 + let iconIndex: number = 0; + + const rowLengths = [5, 5, 5, 4] // Last row has 'Done' button added after this loop: + for (let i = 0; i < 4; i++) { + for (let j = 0; j < rowLengths[i]; j++) { + this.btns[i][j] = new Button({ + parent: null, + style: ButtonStyles.Transparent, + icon: icons[iconIndex], + ariaId: ariaIDs[iconIndex], + x: x, + y: y, + onClick: (button: Button) => { + // Deletion: + const index = this.selectedSensorAriaIDs.indexOf(button.ariaId) + if (index != -1) { + this.cursor.setOutlineColour() + this.selectedSensorAriaIDs.splice(index, 1) + + if (Sensor.getFromName(button.ariaId).isJacdac()) { + this.jacdacSensorSelected = false + this.setOtherJacdacButtonsTo(true) + } + + // Renable all except the Jacdac buttons: + let currentIndex = 0; + for (let i = 0; i < this.btns.length; i++) { + for (let j = 0; j < rowLengths[i]; j++) { + if (currentIndex >= START_OF_JACDAC_BUTTONS_INDEX) + break + this.btns[i][j].pressable = true + currentIndex++; + } + } + } + + // Addition: + else if (this.selectedSensorAriaIDs.length < MAX_NUMBER_OF_SENSORS) { + this.cursor.setOutlineColour(7) + + if (Sensor.getFromName(button.ariaId).isJacdac()) { + if (!this.jacdacSensorSelected) { + this.selectedSensorAriaIDs.push(button.ariaId) + this.jacdacSensorSelected = true + + this.setOtherJacdacButtonsTo(false, button) + } + } + + else { + this.selectedSensorAriaIDs.push(button.ariaId) + button.pressable = true + } + } + + // Prevention: + if (this.selectedSensorAriaIDs.length >= MAX_NUMBER_OF_SENSORS) { + for (let i = 0; i < this.btns.length; i++) { + for (let j = 0; j < rowLengths[i]; j++) { + let buttonInUse = false + for (let k = 0; k < this.selectedSensorAriaIDs.length; k++) { + if (this.btns[i][j].ariaId == this.selectedSensorAriaIDs[k]) { + buttonInUse = true + break + } + } + + if (!buttonInUse) + this.btns[i][j].pressable = false + } + } + } + }, + dynamicBoundaryColorsOn: true, + }) + + x += 30 + if (x > 60) { + x = -60 + y += Screen.HEIGHT * 0.21875 // 28 on 128 pixel high Arcade Shield + } + + iconIndex++; + } + } + + this.btns[3].push(new Button({ + parent: null, + style: ButtonStyles.Transparent, + icon: "green_tick", + ariaId: "Done", + x, + y, + onClick: () => { + if (this.selectedSensorAriaIDs.length === 0) { + return + } + const sensors = this.selectedSensorAriaIDs.map((ariaID) => Sensor.getFromName(ariaID)) + + this.app.popScene() + if (this.nextSceneEnum === MicroDataSceneEnum.LiveDataViewer) { + this.app.pushScene(new LiveDataViewer(this.app, sensors)) + } + + else if (this.nextSceneEnum === MicroDataSceneEnum.RecordingConfigSelect) + this.app.pushScene(new RecordingConfigSelection(this.app, sensors)) + + else if (this.nextSceneEnum === MicroDataSceneEnum.DistributedLogging) + this.app.pushScene(new RecordingConfigSelection(this.app, sensors, MicroDataSceneEnum.DistributedLogging)) } + })) + + + this.navigator.setBtns(this.btns); + } - // Renable all except the Jacdac buttons: - let currentIndex = 0; - for (let i = 0; i < this.btns.length; i++) { - for (let j = 0; j < rowLengths[i]; j++) { - if (currentIndex >= START_OF_JACDAC_BUTTONS_INDEX) - break - this.btns[i][j].pressable = true + /** + * Modify the mutability of all of the Jacdac buttons at once. + * Neccessary since only one Jacdac sensor should be selected at once. + * @param pressableStatus to set all Jacdac buttons to. + * @param buttonToIgnore Optional case that ignores the pressableStatus + */ + private setOtherJacdacButtonsTo(pressableStatus: boolean, buttonToIgnore?: Button) { + let currentIndex = 0; + + for (let i = 0; i < this.btns.length; i++) { + for (let j = 0; j < this.btns[0].length; j++) { + if (currentIndex >= START_OF_JACDAC_BUTTONS_INDEX && currentIndex != (4 * 5) - 1) // Don't touch the last button ('Done') + this.btns[i][j].pressable = pressableStatus currentIndex++; - } } - } + } - // Addition: - else if (this.selectedSensorAriaIDs.length < MAX_NUMBER_OF_SENSORS) { - this.cursor.setOutlineColour(7) + if (buttonToIgnore) + buttonToIgnore.pressable = !pressableStatus + } - if (Sensor.getFromName(button.ariaId).isJacdac()) { - if (!this.jacdacSensorSelected) { - this.selectedSensorAriaIDs.push(button.ariaId) - this.jacdacSensorSelected = true + private overrideControllerButtonBindings() { + const tutorialTextCountDownTimer = () => { + control.inBackground(() => { + basic.pause(3000) + this.tutorialHintIndex = 3 + basic.pause(5000) + this.tutorialHintIndex = NUMBER_OF_TUTORIAL_HINTS + }) + } + + context.onEvent( + ControllerButtonEvent.Pressed, + controller.right.id, + () => { + if (this.userHasPressedABtn && this.tutorialHintIndex <= 1) { + this.tutorialHintIndex = 2 + tutorialTextCountDownTimer() + } + else if (this.tutorialHintIndex == 0) + this.tutorialHintIndex = 1 + this.moveCursor(user_interface_base.CursorDir.Right) + } + ) + + context.onEvent( + ControllerButtonEvent.Pressed, + controller.up.id, + () => { + if (this.navigator.getRow() == 0) { + this.tutorialTextYOffset = TUTORIAL_TEXT_TOP_ALIGNMENT; + } - this.setOtherJacdacButtonsTo(false, button) - } + if (this.navigator.getRow() == 1) { + this.tutorialTextYOffset = TUTORIAL_TEXT_BOT_ALIGNMENT + } + + if (this.userHasPressedABtn && this.tutorialHintIndex <= 1) { + this.tutorialHintIndex = 2 + tutorialTextCountDownTimer() + } + else if (this.tutorialHintIndex == 0) + this.tutorialHintIndex = 1 + this.moveCursor(user_interface_base.CursorDir.Up) } + ) + + context.onEvent( + ControllerButtonEvent.Pressed, + controller.down.id, + () => { + if (this.navigator.getRow() == 1) { + this.tutorialTextYOffset = TUTORIAL_TEXT_TOP_ALIGNMENT; + } - else { - this.selectedSensorAriaIDs.push(button.ariaId) - button.pressable = true + if (this.navigator.getRow() == 3) { + this.tutorialTextYOffset = TUTORIAL_TEXT_BOT_ALIGNMENT; + } + + if (this.userHasPressedABtn && this.tutorialHintIndex <= 1) { + this.tutorialHintIndex = 2 + tutorialTextCountDownTimer() + } + else if (this.tutorialHintIndex == 0) + this.tutorialHintIndex = 1 + this.moveCursor(user_interface_base.CursorDir.Down) } - } - - // Prevention: - if (this.selectedSensorAriaIDs.length >= MAX_NUMBER_OF_SENSORS) { - for (let i = 0; i < this.btns.length; i++) { - for (let j = 0; j < rowLengths[i]; j++) { - let buttonInUse = false - for (let k = 0; k < this.selectedSensorAriaIDs.length; k++) { - if (this.btns[i][j].ariaId == this.selectedSensorAriaIDs[k]) { - buttonInUse = true - break - } - } - - if (!buttonInUse) - this.btns[i][j].pressable = false - } + ) + context.onEvent( + ControllerButtonEvent.Pressed, + controller.left.id, + () => { + if (this.userHasPressedABtn && this.tutorialHintIndex <= 1) { + this.tutorialHintIndex = 2 + tutorialTextCountDownTimer() + } + else if (this.tutorialHintIndex == 0) + this.tutorialHintIndex = 1 + this.moveCursor(user_interface_base.CursorDir.Left) } - } - }, - dynamicBoundaryColorsOn: true, - }) - - x += 30 - if (x > 60) { - x = -60 - y += Screen.HEIGHT * 0.21875 // 28 on 128 pixel high Arcade Shield - } - - iconIndex++; - } - } - - this.btns[3].push(new Button({ - parent: null, - style: ButtonStyles.Transparent, - icon: "green_tick", - ariaId: "Done", - x, - y, - onClick: () => { - if (this.selectedSensorAriaIDs.length === 0) { - return - } - const sensors = this.selectedSensorAriaIDs.map((ariaID) => Sensor.getFromName(ariaID)) - - this.app.popScene() - if (this.nextSceneEnum === CursorSceneEnum.LiveDataViewer) { - this.app.pushScene(new LiveDataViewer(this.app, sensors)) - } - - else if (this.nextSceneEnum === CursorSceneEnum.RecordingConfigSelect) - this.app.pushScene(new RecordingConfigSelection(this.app, sensors)) - - else if (this.nextSceneEnum === CursorSceneEnum.DistributedLogging) - this.app.pushScene(new RecordingConfigSelection(this.app, sensors, CursorSceneEnum.DistributedLogging)) + ) + + context.onEvent( + ControllerButtonEvent.Pressed, + controller.A.id, + () => { + this.userHasPressedABtn = true; + if (this.tutorialHintIndex == 1) { + this.tutorialHintIndex = 2 + tutorialTextCountDownTimer() + } + this.cursor.click() + } + ) + + context.onEvent( + ControllerButtonEvent.Pressed, + controller.B.id, + () => { + this.app.popScene() + this.app.pushScene(new Home(this.app)) + } + ) } - })) + draw() { + Screen.fillRect( + Screen.LEFT_EDGE, + Screen.TOP_EDGE, + Screen.WIDTH, + Screen.HEIGHT, + 0xc + ) + + this.navigator.drawComponents(); + super.draw() + + if (TUTORIAL_MODE) { + const drawTutorialTextBox = () => { + Screen.fillRect( + Screen.LEFT_EDGE, + this.tutorialTextYOffset, + Screen.WIDTH - 36, + 24, + 15 + ) + + Screen.fillRect( + Screen.LEFT_EDGE, + this.tutorialTextYOffset + 2, + Screen.WIDTH - (36 - 4), + 24 - (4), + 6 + ) + } - this.navigator.setBtns(this.btns) - } + switch (this.tutorialHintIndex) { + case 0: { + drawTutorialTextBox(); + Screen.print( + "Navigate with the\narrow keys.", + Screen.LEFT_EDGE + 2, + this.tutorialTextYOffset + 2, + 1 + ) + break; + } - /** - * Modify the mutability of all of the Jacdac buttons at once. - * Neccessary since only one Jacdac sensor should be selected at once. - * @param pressableStatus to set all Jacdac buttons to. - * @param buttonToIgnore Optional case that ignores the pressableStatus - */ - private setOtherJacdacButtonsTo(pressableStatus: boolean, buttonToIgnore?: Button) { - let currentIndex = 0; - - for (let i = 0; i < this.btns.length; i++) { - for (let j = 0; j < this.btns[0].length; j++) { - if (currentIndex >= START_OF_JACDAC_BUTTONS_INDEX && currentIndex != (4 * 5) - 1) // Don't touch the last button ('Done') - this.btns[i][j].pressable = pressableStatus - currentIndex++; - } - } + case 1: { + drawTutorialTextBox(); + Screen.print( + "Press A to select\na sensor.", + Screen.LEFT_EDGE + 2, + this.tutorialTextYOffset + 2, + 1 + ) + break; + } - if (buttonToIgnore) - buttonToIgnore.pressable = !pressableStatus - } + case 2: { + drawTutorialTextBox(); + Screen.print( + "You can select up\nto three sensors.", + Screen.LEFT_EDGE + 2, + this.tutorialTextYOffset + 2, + 1 + ) + break; + } - draw() { - Screen.fillRect( - Screen.LEFT_EDGE, - Screen.TOP_EDGE, - Screen.WIDTH, - Screen.HEIGHT, - 0xc - ) - - this.navigator.drawComponents(); - super.draw() + case 3: { + Screen.fillRect( + Screen.LEFT_EDGE, + this.tutorialTextYOffset, + Screen.WIDTH - 40, + 35, + 15 + ) + + Screen.fillRect( + Screen.LEFT_EDGE, + this.tutorialTextYOffset + 2, + Screen.WIDTH - (40 - 4), + 35 - 4, + 6 + ) + + Screen.print( + "When you're ready\nclick Done in the\nbottom-right corner.", + Screen.LEFT_EDGE + 2, + this.tutorialTextYOffset + 2, + 1 + ) + break; + } + } + } + } } - } } diff --git a/sensors.ts b/sensors.ts index 93dc455..34d89a9 100644 --- a/sensors.ts +++ b/sensors.ts @@ -1,706 +1,727 @@ namespace microdata { - import Screen = user_interface_base.Screen + import Screen = user_interface_base.Screen + + /** The period that the scheduler should wait before comparing a reading with the event's inequality */ + export const SENSOR_EVENT_POLLING_PERIOD_MS: number = 100 + + /** + * Used to lookup the implemented events via sensorEventFunctionLookup[] + * + * Currently only events that check for inequalities are implemented, + * The only sensors that are incompatible with this are Buttons + * The following code may be generalised to support them though. + */ + export const sensorEventSymbols = ["=", ">", "<", ">=", "<="] + + /** + * Type for value bound to inequality key within sensorEventFunctionLookup + * + * One of these is optionally held by a sensor - see by sensor.setRecordingConfig + */ + export type SensorEventFunction = (reading: number, comparator: number) => boolean + + /** + * Get aa function that performs that inequality check & logs it with an event description if the event has triggered. + */ + export const sensorEventFunctionLookup: { [inequality: string]: SensorEventFunction } = { + "=": function (reading: number, comparator: number) { return reading == comparator }, + ">": function (reading: number, comparator: number) { return reading > comparator }, + "<": function (reading: number, comparator: number) { return reading < comparator }, + ">=": function (reading: number, comparator: number) { return reading >= comparator }, + "<=": function (reading: number, comparator: number) { return reading <= comparator } + } + + /** How many times should a line be duplicated when drawn? */ + const PLOT_SMOOTHING_CONSTANT: number = 4 + + /** To what precision whould readings fromt he sensor be cut to when they're logged? */ + const READING_PRECISION: number = 9 + + /** + * Responsible for making an array of sensors with configurations read & log their data accurately. + * This class is used by both the DataRecorder (when an Arcade Shield is connected), and by a microbit without an Arcade Shield (see DistributedLoggingProtocol). + * The scheduler runs in a separate thread and accounts for sensors with different numbers of measurements, periods and events. + * see .start() + */ + export class SensorScheduler { + /** Ordered sensor periods */ + private schedule: { sensor: Sensor, waitTime: number }[]; + + /** These are configured sensors that will be scheduled upon. */ + private sensors: Sensor[]; + + /** This class can be used evven if an Arcade Shield is not connected; the 5x5 matrix will display the number of measurements for the sensor with the most time left if this is the case */ + private sensorWithMostTimeLeft: Sensor; + + /** Should the information from the sensorWithMostTimeLeft be shown on the basic's 5x5 LED matrix? */ + private showOnBasicScreen: boolean = false; + + private continueLogging: boolean; + + constructor(sensors: Sensor[], showOnBasicScreen?: boolean) { + this.schedule = [] + this.sensors = sensors + + if (showOnBasicScreen != null) + this.showOnBasicScreen = showOnBasicScreen + + // Get the sensor that will take the longest to complete: + // The number of measurements this sensor has left is displayed on the microbit 5x5 led grid; when the Arcade Shield is not connected. + this.sensorWithMostTimeLeft = sensors[0] + let mostTimeLeft = this.sensorWithMostTimeLeft.totalMeasurements * this.sensorWithMostTimeLeft.getPeriod() + + this.sensors.forEach(sensor => { + if ((sensor.totalMeasurements * sensor.getPeriod()) > mostTimeLeft) { + mostTimeLeft = sensor.totalMeasurements * sensor.getPeriod() + this.sensorWithMostTimeLeft = sensor + } + }) - /** The period that the scheduler should wait before comparing a reading with the event's inequality */ - export const SENSOR_EVENT_POLLING_PERIOD_MS: number = 100 + this.continueLogging = true; - /** - * Used to lookup the implemented events via sensorEventFunctionLookup[] - * - * Currently only events that check for inequalities are implemented, - * The only sensors that are incompatible with this are Buttons - * The following code may be generalised to support them though. - */ - export const sensorEventSymbols = ["=", ">", "<", ">=", "<="] + // Setup schedule so that periods are in order ascending + sensors.sort((a, b) => a.getPeriod() - b.getPeriod()) + this.schedule = sensors.map((sensor) => { return { sensor, waitTime: sensor.getPeriod() } }) + } - /** - * Type for value bound to inequality key within sensorEventFunctionLookup - * - * One of these is optionally held by a sensor - see by sensor.setRecordingConfig - */ - export type SensorEventFunction = (reading: number, comparator: number) => boolean + //---------------------------------------------- + // Outward facing methods: + // Invoked by distributedLogging & dataRecorder: + //---------------------------------------------- - /** - * Get aa function that performs that inequality check & logs it with an event description if the event has triggered. - */ - export const sensorEventFunctionLookup: { [inequality: string]: SensorEventFunction } = { - "=": function(reading: number, comparator: number) { return reading == comparator }, - ">": function(reading: number, comparator: number) { return reading > comparator }, - "<": function(reading: number, comparator: number) { return reading < comparator }, - ">=": function(reading: number, comparator: number) { return reading >= comparator }, - "<=": function(reading: number, comparator: number) { return reading <= comparator } - } + public loggingComplete(): boolean { return !(this.schedule.length > 0) } - /** How many times should a line be duplicated when drawn? */ - const PLOT_SMOOTHING_CONSTANT: number = 4 + public stop() { + this.continueLogging = false; - /** To what precision whould readings fromt he sensor be cut to when they're logged? */ - const READING_PRECISION: number = 9 + // Force unregistry of all buttons: + // There's a bug where the control.inBackground() doesn't die properly. + // So when you enter another scene it re-triggers some code - causing it to go back to home twice. + context.onEvent(ControllerButtonEvent.Pressed, controller.A.id, () => { }) + // context.onEvent(ControllerButtonEvent.Pressed, controller.B.id, () => { }) + context.onEvent(ControllerButtonEvent.Pressed, controller.up.id, () => { }) + context.onEvent(ControllerButtonEvent.Pressed, controller.down.id, () => { }) + context.onEvent(ControllerButtonEvent.Pressed, controller.left.id, () => { }) + context.onEvent(ControllerButtonEvent.Pressed, controller.right.id, () => { }) - /** - * Responsible for making an array of sensors with configurations read & log their data accurately. - * This class is used by both the DataRecorder (when an Arcade Shield is connected), and by a microbit without an Arcade Shield (see DistributedLoggingProtocol). - * The scheduler runs in a separate thread and accounts for sensors with different numbers of measurements, periods and events. - * see .start() - */ - export class SensorScheduler { - /** Ordered sensor periods */ - private schedule: { sensor: Sensor, waitTime: number }[]; + } - /** These are configured sensors that will be scheduled upon. */ - private sensors: Sensor[]; + /** + * Schedules the sensors and orders them to .log() + * Runs within a separate fiber. + * + * Time it takes for this algorithm to run is accounted for when calculating how long to wait inbetween logs + * Mutates this.schedule + * + * Temp disabled elements relating to callbackObj (no mem) + * @param callbackObj is used by the DistributedLoggingProtocol; after each log & after the algorithm finishes a callback will be made + */ + public start(callbackObj?: ITargetDataLoggedCallback) { + const callbackAfterLog: boolean = (callbackObj == null) ? false : true + + control.inBackground(() => { + let lastLogTime = control.millis(); + let currentTime = 0; + + // Log all sensors once: + for (let i = 0; i < this.schedule.length; i++) { + if (this.showOnBasicScreen && this.schedule[i].sensor == this.sensorWithMostTimeLeft) + basic.showNumber(this.sensorWithMostTimeLeft.getMeasurements()) + + // Make the datalogger log the data: + const logAsCSV = this.schedule[i].sensor.log(0) + + // Optionally inform the caller of the log (In the case of the DistributedLoggingProtocol this information can be forwarded to the Commander over radio) + if (callbackAfterLog) + callbackObj.callback(logAsCSV) + + // Clear from schedule (A sensor may only have 1 reading): + if (!this.schedule[i].sensor.hasMeasurements()) + this.schedule.splice(i, 1); + } - /** This class can be used evven if an Arcade Shield is not connected; the 5x5 matrix will display the number of measurements for the sensor with the most time left if this is the case */ - private sensorWithMostTimeLeft: Sensor; + let nowMs; + while (this.schedule.length > 0) { + const beforeUs = control.micros() - /** Should the information from the sensorWithMostTimeLeft be shown on the basic's 5x5 LED matrix? */ - private showOnBasicScreen: boolean = false; + const nextLogTime = this.schedule[0].waitTime; + const sleepTime = nextLogTime - currentTime; - private continueLogging: boolean; + // Wait the required period, discount operation time, in 100ms chunks + // Check if there last been a request to stop logging each chunk - constructor(sensors: Sensor[], showOnBasicScreen?: boolean) { - this.schedule = [] - this.sensors = sensors + nowMs = control.millis(); + // const pauseTime = Math.max(1, sleepTime - (nowMs - lastLogTime)); // Discount for operation time - if (showOnBasicScreen != null) - this.showOnBasicScreen = showOnBasicScreen + const loopCostMS = 8; + const pauseTime = Math.max(sleepTime - loopCostMS, 0); + control.dmesg(`pt: ${(pauseTime)} ${(nowMs - lastLogTime) - sleepTime}`) + // basic.pause(pauseTime); - // Get the sensor that will take the longest to complete: - // The number of measurements this sensor has left is displayed on the microbit 5x5 led grid; when the Arcade Shield is not connected. - this.sensorWithMostTimeLeft = sensors[0] - let mostTimeLeft = this.sensorWithMostTimeLeft.totalMeasurements * this.sensorWithMostTimeLeft.getPeriod() + lastLogTime = nowMs; + currentTime += sleepTime - this.sensors.forEach(sensor => { - if ((sensor.totalMeasurements * sensor.getPeriod()) > mostTimeLeft) { - mostTimeLeft = sensor.totalMeasurements * sensor.getPeriod() - this.sensorWithMostTimeLeft = sensor - } - }) + for (let i = 0; i < pauseTime; i += 10) { + if (!this.continueLogging) { + return; + // break; + } + basic.pause(10) + } + basic.pause(pauseTime % 10) + + if (!this.continueLogging) + // break; + return; + + for (let i = 0; i < this.schedule.length; i++) { + // Clear from schedule: + if (!this.schedule[i].sensor.hasMeasurements()) { + this.schedule.splice(i, 1); + } - this.continueLogging = true; + // Log sensors: + else if (currentTime % this.schedule[i].waitTime == 0) { + if (this.showOnBasicScreen && this.schedule[i].sensor == this.sensorWithMostTimeLeft) + basic.showNumber(this.sensorWithMostTimeLeft.getMeasurements()) - // Setup schedule so that periods are in order ascending - sensors.sort((a, b) => a.getPeriod() - b.getPeriod()) - this.schedule = sensors.map((sensor) => { return { sensor, waitTime: sensor.getPeriod() } }) - } + // Make the datalogger log the data: + const logAsCSV = this.schedule[i].sensor.log(currentTime) - //---------------------------------------------- - // Outward facing methods: - // Invoked by distributedLogging & dataRecorder: - //---------------------------------------------- + // Optionally inform the caller of the log (In the case of the DistributedLoggingProtocol this information can be forwarded to the Commander over radio) + if (callbackAfterLog) + callbackObj.callback(logAsCSV) - public loggingComplete(): boolean { return !(this.schedule.length > 0) } + // Update schedule with when they should next be logged: + if (this.schedule[i].sensor.hasMeasurements()) { + this.schedule[i].waitTime = nextLogTime + this.schedule[i].sensor.getPeriod() + } + } + } + + // Ensure the schedule remains ordely after these potential deletions & recalculations: + this.schedule.sort(( + a: { sensor: Sensor; waitTime: number; }, + b: { sensor: Sensor; waitTime: number; }) => + a.waitTime - b.waitTime + ) + control.dmesg(`logTime: ${(control.micros() - beforeUs)}`) + } - public stop() { - this.continueLogging = false; + // Done: + if (this.showOnBasicScreen) { + basic.showLeds(` + . # . # . + . # . # . + . . . . . + # . . . # + . # # # . + `) } - /** - * Schedules the sensors and orders them to .log() - * Runs within a separate fiber. - * - * Time it takes for this algorithm to run is accounted for when calculating how long to wait inbetween logs - * Mutates this.schedule - * - * Temp disabled elements relating to callbackObj (no mem) - * @param callbackObj is used by the DistributedLoggingProtocol; after each log & after the algorithm finishes a callback will be made - */ - public start(callbackObj?: ITargetDataLoggedCallback) { - const callbackAfterLog: boolean = (callbackObj == null) ? false : true - - control.inBackground(() => { - let currentTime = 0; - - // Log all sensors once: - for (let i = 0; i < this.schedule.length; i++) { - if (this.showOnBasicScreen && this.schedule[i].sensor == this.sensorWithMostTimeLeft) - basic.showNumber(this.sensorWithMostTimeLeft.getMeasurements()) - - // Make the datalogger log the data: - const logAsCSV = this.schedule[i].sensor.log(0) - - // Optionally inform the caller of the log (In the case of the DistributedLoggingProtocol this information can be forwarded to the Commander over radio) - if (callbackAfterLog) - callbackObj.callback(logAsCSV) - - // Clear from schedule (A sensor may only have 1 reading): - if (!this.schedule[i].sensor.hasMeasurements()) - this.schedule.splice(i, 1); - } - - - let lastLogTime = input.runningTime() - - while (this.schedule.length > 0) { - const nextLogTime = this.schedule[0].waitTime; - const sleepTime = nextLogTime - currentTime; - - - // Wait the required period, discount operation time, in 100ms chunks - // Check if there last been a request to stop logging each chunk - - const pauseTime = sleepTime + lastLogTime - input.runningTime() // Discount for operation time - for (let i = 0; i < pauseTime; i += 100) { - if (!this.continueLogging) { - return - } - basic.pause(100) - } - basic.pause(pauseTime % 100) - - if (!this.continueLogging) - break; - - lastLogTime = input.runningTime() - currentTime += sleepTime - - for (let i = 0; i < this.schedule.length; i++) { - // Clear from schedule: - if (!this.schedule[i].sensor.hasMeasurements()) { - this.schedule.splice(i, 1); - } - - // Log sensors: - else if (currentTime % this.schedule[i].waitTime == 0) { - if (this.showOnBasicScreen && this.schedule[i].sensor == this.sensorWithMostTimeLeft) - basic.showNumber(this.sensorWithMostTimeLeft.getMeasurements()) - - // Make the datalogger log the data: - const logAsCSV = this.schedule[i].sensor.log(currentTime) - - // Optionally inform the caller of the log (In the case of the DistributedLoggingProtocol this information can be forwarded to the Commander over radio) - if (callbackAfterLog) - callbackObj.callback(logAsCSV) - - // Update schedule with when they should next be logged: - if (this.schedule[i].sensor.hasMeasurements()) { - this.schedule[i].waitTime = nextLogTime + this.schedule[i].sensor.getPeriod() - } - } - } - - // Ensure the schedule remains ordely after these potential deletions & recalculations: - this.schedule.sort(( - a: { sensor: Sensor; waitTime: number; }, - b: { sensor: Sensor; waitTime: number; }) => - a.waitTime - b.waitTime - ) - } - - // Done: - if (this.showOnBasicScreen) { - basic.showLeds(` - . # . # . - . # . # . - . . . . . - # . . . # - . # # # . - `) - } - - if (callbackAfterLog) { - DistributedLoggingProtocol.finishedLogging = true - callbackObj.callback("") - } - }) + if (callbackAfterLog) { + DistributedLoggingProtocol.finishedLogging = true + callbackObj.callback("") } + }) + } + } + + /** + * Abstraction for all available sensors. + * This class is extended by each of the concrete sensors which add on static methods for their name, getting their readings & optionally min/max readings + */ + export class Sensor { + /** Immutable: Forward facing name that is presented to the user in LiveDataViewer, Sensor Selection & TabularDataViewer */ + private name: string; + /** Immutable: Name used for Radio Communication, a unique shorthand, see distributedLogging.ts */ + private radioName: string; + /** Immutable: Minimum possible sensor reading, based on datasheet of peripheral. Some sensors transform their output (Analog pins transform 0->1023, into 0->3V volt range) */ + private minimum: number; + /** Immutable: Maximum possible sensor reading, based on datasheet of peripheral. Some sensors transform their output (Analog pins transform 0->1023, into 0->3V volt range) */ + private maximum: number; + /** Immutable: Abs(minimum) + Abs(maximum); calculated once at start since min & max can't change */ + private range: number; + /** Immutable: Wrapper around the sensors call, e.g: sensorFn = () => input.acceleration(Dimension.X) */ + private sensorFn: () => number; + /** Immutable: Need to know whether or not this sensor is on the microbit or is an external Jacdac one; see sensorSelection.ts */ + private isJacdacSensor: boolean; + + /** Set inside .setConfig() */ + public totalMeasurements: number + + /** Increased on the event of the graph zooming in for example. */ + private maxBufferSize: number + + /** + * Used by the live data viewer to write the small abscissa + * Always increases: even when data buffer is shifted to avoid reaching the BUFFER_LIMIT + */ + public numberOfReadings: number + /** Used to determine sensor information to write in DataRecorder and liveDataViewer */ + public isInEventMode: boolean /** - * Abstraction for all available sensors. - * This class is extended by each of the concrete sensors which add on static methods for their name, getting their readings & optionally min/max readings + * Determines behaviour of .log() */ - export class Sensor { - /** Immutable: Forward facing name that is presented to the user in LiveDataViewer, Sensor Selection & TabularDataViewer */ - private name: string; - /** Immutable: Name used for Radio Communication, a unique shorthand, see distributedLogging.ts */ - private radioName: string; - /** Immutable: Minimum possible sensor reading, based on datasheet of peripheral. Some sensors transform their output (Analog pins transform 0->1023, into 0->3V volt range) */ - private minimum: number; - /** Immutable: Maximum possible sensor reading, based on datasheet of peripheral. Some sensors transform their output (Analog pins transform 0->1023, into 0->3V volt range) */ - private maximum: number; - /** Immutable: Abs(minimum) + Abs(maximum); calculated once at start since min & max can't change */ - private range: number; - /** Immutable: Wrapper around the sensors call, e.g: sensorFn = () => input.acceleration(Dimension.X) */ - private sensorFn: () => number; - /** Immutable: Need to know whether or not this sensor is on the microbit or is an external Jacdac one; see sensorSelection.ts */ - private isJacdacSensor: boolean; - - /** Set inside .setConfig() */ - public totalMeasurements: number - - /** Increased on the event of the graph zooming in for example. */ - private maxBufferSize: number - - /** - * Used by the live data viewer to write the small abscissa - * Always increases: even when data buffer is shifted to avoid reaching the BUFFER_LIMIT - */ - public numberOfReadings: number - - /** Used to determine sensor information to write in DataRecorder and liveDataViewer */ - public isInEventMode: boolean - - /** - * Determines behaviour of .log() - */ - private config: RecordingConfig - - - /** Event statistic used by the dataRecorder. */ - public lastLoggedEventDescription: string - - /** - * Holds the sensor's readings. - * Filled via .readIntoBufferOnce() - * Used by the ticker in liveDataViewer. - * Values are shifted out from FIFO if at max capacity. - * Needed since the entire normalisedBuffer may need to be recalculated upon scrolling or zooming. - */ - private dataBuffer: number[] - - private lastLoggedReading: number; - - /** - * Holds what the Y axis position should be for the corresponding read value, relative to a granted fromY value. - * Filled alongside dataBuffer alongside .readIntoBufferOnce() - * Entire dataBuffer may be recalculated via .normaliseDataBuffer() - * Values are shifted out from FIFO if at max capacity. - */ - private heightNormalisedDataBuffer: number[] - - constructor(opts: { - name: string, - rName: string, - f: () => number, - min: number, - max: number, - isJacdacSensor: boolean, - setupFn?: () => void - }) { - this.maxBufferSize = 80 - this.totalMeasurements = 0 - this.numberOfReadings = 0 - this.isInEventMode = false - - this.lastLoggedEventDescription = "" - this.dataBuffer = [] - this.lastLoggedReading = 0 - this.heightNormalisedDataBuffer = [] - - // Data from opts: - this.name = opts.name - this.radioName = opts.rName - this.minimum = opts.min - this.maximum = opts.max - this.range = Math.abs(this.minimum) + this.maximum - this.sensorFn = opts.f - this.isJacdacSensor = opts.isJacdacSensor - - // Could be additional functions required to set up the sensor (see Jacdac modules or Accelerometers): - if (opts.setupFn != null) - opts.setupFn(); - } + private config: RecordingConfig - //------------------ - // Factory Function: - //------------------ - - /** - * Factory function used to generate a Sensor from that sensors: .getName(), sensorSelect name, or its radio name - * This is a single factory within this abstract class to reduce binary size - * @param name either sensor.getName(), sensor.getRadioName() or the ariaID the button that represents the sensor in SensorSelect uses. - * @returns concrete sensor that the input name corresponds to. - */ - public static getFromName(name: string): Sensor { - if (name == "Accel. X" || name == "Accelerometer X" || name == "AX") - return new Sensor({ - name: "Accel. X", - rName: "AX", - f: () => input.acceleration(Dimension.X), - min: -2048, - max: 2048, - isJacdacSensor: false, - setupFn: () => input.setAccelerometerRange(AcceleratorRange.OneG) - }); - - else if (name == "Accel. Y" || name == "Accelerometer Y" || name == "AY") - return new Sensor({ - name: "Accel. Y", - rName: "AY", - f: () => input.acceleration(Dimension.Y), - min: -2048, - max: 2048, - isJacdacSensor: false, - setupFn: () => input.setAccelerometerRange(AcceleratorRange.OneG) - }); - - else if (name == "Accel. Z" || name == "Accelerometer Z" || name == "AZ") - return new Sensor({ - name: "Accel. Z", - rName: "AZ", - f: () => input.acceleration(Dimension.Z), - min: -2048, - max: 2048, - isJacdacSensor: false, - setupFn: () => input.setAccelerometerRange(AcceleratorRange.OneG) - }); - - else if (name == "Pitch" || name == "P") - return new Sensor({ - name: "Pitch", - rName: "P", - f: () => input.rotation(Rotation.Pitch), - min: -180, - max: 180, - isJacdacSensor: false - }); - - else if (name == "Roll" || name == "R") - return new Sensor({ - name: "Roll", - rName: "R", - f: () => input.rotation(Rotation.Roll), - min: -180, - max: 180, - isJacdacSensor: false - }); - - else if (name == "A. Pin 0" || name == "Analog Pin 0" || name == "AP0") - return new Sensor({ - name: "A. Pin 0", - rName: "AP0", - f: () => pins.analogReadPin(AnalogPin.P0) / 340, - min: 0, - max: 3, - isJacdacSensor: false - }); - - else if (name == "A. Pin 1" || name == "Analog Pin 1" || name == "AP1") - return new Sensor({ - name: "A. Pin 1", - rName: "AP1", - f: () => pins.analogReadPin(AnalogPin.P1) / 340, - min: 0, - max: 3, - isJacdacSensor: false - }); - - else if (name == "A. Pin 2" || name == "Analog Pin 2" || name == "AP2") - return new Sensor({ - name: "A. Pin 2", - rName: "AP2", - f: () => pins.analogReadPin(AnalogPin.P2) / 340, - min: 0, - max: 3, - isJacdacSensor: false - }); - - else if (name == "Light" || name == "L") - return new Sensor({ - name: "Light", - rName: "L", - f: () => input.lightLevel(), - min: 0, - max: 255, - isJacdacSensor: false - }); - - else if (name == "Temp." || name == "Temperature" || name == "T") - return new Sensor({ - name: "Temp.", - rName: "T", - f: () => input.temperature(), - min: -40, - max: 100, - isJacdacSensor: false - }); - - else if (name == "Magnet" || name == "M") - return new Sensor({ - name: "Magnet", - rName: "M", - f: () => input.magneticForce(Dimension.Strength), - min: -5000, - max: 5000, - isJacdacSensor: false - }); - - else if (name == "Logo Pressed" || name == "Logo Press" || name == "LP") - return new Sensor({ - name: "Logo Press", - rName: "LP", - f: () => (input.logoIsPressed() ? 1 : 0), - min: 0, - max: 1, - isJacdacSensor: false - }); - - else if (name == "Volume" || name == "Microphone" || name == "V") - return new Sensor({ - name: "Microphone", - rName: "V", - f: () => input.soundLevel(), - min: 0, - max: 255, - isJacdacSensor: false - }); - - else if (name == "Compass" || name == "C") - return new Sensor({ - name: "Compass", - rName: "C", - f: () => input.compassHeading(), - min: 0, - max: 360, - isJacdacSensor: false - }); - - //-------------------------------------------- - // Jacdac Sensors: - // See https://github.com/microsoft/pxt-jacdac - //-------------------------------------------- - - else if (name == "Jac Light" || name == "Jacdac Light" || name == "JL") - return new Sensor({ - name: "Jac Light", - rName: "JL", - f: () => modules.lightLevel1.isConnected() ? modules.lightLevel1.lightLevel() : undefined, - min: 0, - max: 100, - isJacdacSensor: true, - setupFn: () => modules.lightLevel1.start() - }); - - else if (name == "Jac Moist" || name == "Jacdac Moisture" || name == "JM") - return new Sensor({ - name: "Jac Moist", - rName: "JM", - f: () => modules.soilMoisture1.isConnected() ? modules.soilMoisture1.moisture() : undefined, - min: 0, - max: 100, - isJacdacSensor: true, - setupFn: () => modules.soilMoisture1.start() - }); - - else if (name == "Jac Dist" || name == "Jacdac Distance" || name == "JD") - return new Sensor({ - name: "Jac Dist", - rName: "JD", - f: () => modules.distance1.isConnected() ? modules.distance1.distance() : undefined, - min: 0, - max: 4, - isJacdacSensor: true, - setupFn: () => modules.distance1.start() - }); - - else if (name == "Jac Flex" || name == "Jacdac Flex" || name == "JF") - return new Sensor({ - name: "Jac Flex", - rName: "JF", - f: () => modules.flex1.isConnected() ? modules.flex1.bending() : undefined, - min: -100, - max: 100, - isJacdacSensor: true, - setupFn: () => modules.flex1.start() - }); - - else - return new Sensor({ - name: "Jac Temp", - rName: "JT", - f: () => modules.temperature1.isConnected() ? modules.temperature1.temperature() : undefined, - min: -40, - max: 120, - isJacdacSensor: true, - setupFn: () => modules.temperature1.start() - }); - } + /** Event statistic used by the dataRecorder. */ + public lastLoggedEventDescription: string - //--------------------- - // Interface Functions: - //--------------------- - - getName(): string { return this.name } - getRadioName(): string { return this.radioName } - getReading(): number { return this.sensorFn() } - getNormalisedReading(): number { return Math.abs(this.getReading()) / this.range } - getMinimum(): number { return this.minimum; } - getMaximum(): number { return this.maximum; } - isJacdac(): boolean { return this.isJacdacSensor; } - - getMaxBufferSize(): number { return this.maxBufferSize } - getNthReading(n: number): number { return this.dataBuffer[n] } - getNthHeightNormalisedReading(n: number): number { return this.heightNormalisedDataBuffer[n] } - getBufferLength(): number { return this.dataBuffer.length } - getHeightNormalisedBufferLength(): number { return this.heightNormalisedDataBuffer.length } - getPeriod(): number { return this.config.period; } - getMeasurements(): number { return this.config.measurements } - hasMeasurements(): boolean { return this.config.measurements > 0; } - - - /** - * Used by the DataRecorder to display information about the sensor as it is logging. - * @returns linles of information that can be printed out into a box for display. - */ - getRecordingInformation(): string[] { - if (this.hasMeasurements()) - return [ - this.getPeriod() / 1000 + " second period", - this.config.measurements.toString() + " measurements left", - ((this.config.measurements * this.getPeriod()) / 1000).toString() + " seconds left", - "Last log was " + this.lastLoggedReading.toString().slice(0, 5), - ] - else - return [ - "Logging complete.", - "Last log was " + this.lastLoggedReading.toString().slice(0, 5), - ] - } + /** + * Holds the sensor's readings. + * Filled via .readIntoBufferOnce() + * Used by the ticker in liveDataViewer. + * Values are shifted out from FIFO if at max capacity. + * Needed since the entire normalisedBuffer may need to be recalculated upon scrolling or zooming. + */ + private dataBuffer: number[] - /** - * Used by the DataRecorder to display information about the sensor as it is logging. - * @returns lines of information that can be printed out into a box for display. - */ - getEventInformation(): string[] { - if (this.hasMeasurements()) - return [ - this.config.measurements.toString() + " events left", - "Logging " + this.config.inequality + " " + this.config.comparator + " events", - "Last log was " + this.lastLoggedReading.toString().slice(0, 5), - this.lastLoggedEventDescription - ] - - else - return [ - "Logging complete.", - "Last log was " + this.lastLoggedReading.toString().slice(0, 5) - ] - } + private lastLoggedReading: number; - /** - * Change the size of the buffer used for this.dataBuffer & this.normalisedBuffer - * Will shift out old this.dataBuffer & this.normalisedBuffer values from the front. - * @param newBufferSize absolute new value for both this.dataBuffer & this.normalisedBuffer - */ - setBufferSize(newBufferSize: number): void { - // Remove additional values if neccessary: - if (this.dataBuffer.length > newBufferSize) { - const difference = this.dataBuffer.length - newBufferSize - this.dataBuffer.splice(0, difference) - this.heightNormalisedDataBuffer.splice(0, difference) - } - this.maxBufferSize = newBufferSize - } + /** + * Holds what the Y axis position should be for the corresponding read value, relative to a granted fromY value. + * Filled alongside dataBuffer alongside .readIntoBufferOnce() + * Entire dataBuffer may be recalculated via .normaliseDataBuffer() + * Values are shifted out from FIFO if at max capacity. + */ + private heightNormalisedDataBuffer: number[] + + constructor(opts: { + name: string, + rName: string, + f: () => number, + min: number, + max: number, + isJacdacSensor: boolean, + setupFn?: () => void + }) { + this.maxBufferSize = 80 + this.totalMeasurements = 0 + this.numberOfReadings = 0 + this.isInEventMode = false + + this.lastLoggedEventDescription = "" + this.dataBuffer = [] + this.lastLoggedReading = 0 + this.heightNormalisedDataBuffer = [] + + // Data from opts: + this.name = opts.name + this.radioName = opts.rName + this.minimum = opts.min + this.maximum = opts.max + this.range = Math.abs(this.minimum) + this.maximum + this.sensorFn = opts.f + this.isJacdacSensor = opts.isJacdacSensor + + // Could be additional functions required to set up the sensor (see Jacdac modules or Accelerometers): + if (opts.setupFn != null) + opts.setupFn(); + } - /** - * Add one value to this.dataBuffer, add that value normalised into this.normalisedBuffer too. - * No value is added if the reading is undefined (such as from a disconnected Jacdac sensor). - * If the (this.dataBuffer.length >= this.maxBufferSize) then then the oldest values are removed. - * @param fromY the offset by which the reading should be raised before adding to this.normalisedBuffer - * @returns - */ - readIntoBufferOnce(fromY: number): void { - const reading = this.getReading() - - if (this.dataBuffer.length >= this.maxBufferSize || reading === undefined) { - this.dataBuffer.shift(); - this.heightNormalisedDataBuffer.shift(); - } + //------------------ + // Factory Function: + //------------------ + + /** + * Factory function used to generate a Sensor from that sensors: .getName(), sensorSelect name, or its radio name + * This is a single factory within this abstract class to reduce binary size + * @param name either sensor.getName(), sensor.getRadioName() or the ariaID the button that represents the sensor in SensorSelect uses. + * @returns concrete sensor that the input name corresponds to. + */ + public static getFromName(name: string): Sensor { + if (name == "Accel. X" || name == "Accelerometer X" || name == "AX") + return new Sensor({ + name: "Accel. X", + rName: "AX", + f: () => input.acceleration(Dimension.X), + min: -2048, + max: 2048, + isJacdacSensor: false, + setupFn: () => input.setAccelerometerRange(AcceleratorRange.OneG) + }); + + else if (name == "Accel. Y" || name == "Accelerometer Y" || name == "AY") + return new Sensor({ + name: "Accel. Y", + rName: "AY", + f: () => input.acceleration(Dimension.Y), + min: -2048, + max: 2048, + isJacdacSensor: false, + setupFn: () => input.setAccelerometerRange(AcceleratorRange.OneG) + }); + + else if (name == "Accel. Z" || name == "Accelerometer Z" || name == "AZ") + return new Sensor({ + name: "Accel. Z", + rName: "AZ", + f: () => input.acceleration(Dimension.Z), + min: -2048, + max: 2048, + isJacdacSensor: false, + setupFn: () => input.setAccelerometerRange(AcceleratorRange.OneG) + }); + + else if (name == "Pitch" || name == "P") + return new Sensor({ + name: "Pitch", + rName: "P", + f: () => input.rotation(Rotation.Pitch), + min: -180, + max: 180, + isJacdacSensor: false + }); + + else if (name == "Roll" || name == "R") + return new Sensor({ + name: "Roll", + rName: "R", + f: () => input.rotation(Rotation.Roll), + min: -180, + max: 180, + isJacdacSensor: false + }); + + else if (name == "A. Pin 0" || name == "Analog Pin 0" || name == "AP0") + return new Sensor({ + name: "A. Pin 0", + rName: "AP0", + f: () => pins.analogReadPin(AnalogPin.P0) / 340, + min: 0, + max: 3, + isJacdacSensor: false + }); + + else if (name == "A. Pin 1" || name == "Analog Pin 1" || name == "AP1") + return new Sensor({ + name: "A. Pin 1", + rName: "AP1", + f: () => pins.analogReadPin(AnalogPin.P1) / 340, + min: 0, + max: 3, + isJacdacSensor: false + }); + + else if (name == "A. Pin 2" || name == "Analog Pin 2" || name == "AP2") + return new Sensor({ + name: "A. Pin 2", + rName: "AP2", + f: () => pins.analogReadPin(AnalogPin.P2) / 340, + min: 0, + max: 3, + isJacdacSensor: false + }); + + else if (name == "Light" || name == "L") + return new Sensor({ + name: "Light", + rName: "L", + f: () => input.lightLevel(), + min: 0, + max: 255, + isJacdacSensor: false + }); + + else if (name == "Temp." || name == "Temperature" || name == "T") + return new Sensor({ + name: "Temp.", + rName: "T", + f: () => input.temperature(), + min: -40, + max: 100, + isJacdacSensor: false + }); + + else if (name == "Magnet" || name == "M") + return new Sensor({ + name: "Magnet", + rName: "M", + f: () => input.magneticForce(Dimension.Strength), + min: 0, + max: 5000, + isJacdacSensor: false + }); + + else if (name == "Logo Pressed" || name == "Logo Press" || name == "LP") + return new Sensor({ + name: "Logo Press", + rName: "LP", + f: () => (input.logoIsPressed() ? 1 : 0), + min: 0, + max: 1, + isJacdacSensor: false + }); + + else if (name == "Volume" || name == "Microphone" || name == "V") + return new Sensor({ + name: "Microphone", + rName: "V", + f: () => input.soundLevel(), + min: 0, + max: 255, + isJacdacSensor: false + }); + + else if (name == "Compass" || name == "C") + return new Sensor({ + name: "Compass", + rName: "C", + f: () => input.compassHeading(), + min: 0, + max: 360, + isJacdacSensor: false + }); + + //-------------------------------------------- + // Jacdac Sensors: + // See https://github.com/microsoft/pxt-jacdac + //-------------------------------------------- + + else if (name == "Jac Light" || name == "Jacdac Light" || name == "JL") + return new Sensor({ + name: "Jac Light", + rName: "JL", + f: () => modules.lightLevel1.isConnected() ? modules.lightLevel1.lightLevel() : undefined, + min: 0, + max: 100, + isJacdacSensor: true, + setupFn: () => modules.lightLevel1.start() + }); + + else if (name == "Jac Moist" || name == "Jacdac Moisture" || name == "JM") + return new Sensor({ + name: "Jac Moist", + rName: "JM", + f: () => modules.soilMoisture1.isConnected() ? modules.soilMoisture1.moisture() : undefined, + min: 0, + max: 100, + isJacdacSensor: true, + setupFn: () => modules.soilMoisture1.start() + }); + + else if (name == "Jac Dist" || name == "Jacdac Distance" || name == "JD") + return new Sensor({ + name: "Jac Dist", + rName: "JD", + f: () => modules.distance1.isConnected() ? modules.distance1.distance() : undefined, + min: 0, + max: 4, + isJacdacSensor: true, + setupFn: () => modules.distance1.start() + }); + + else if (name == "Jac Flex" || name == "Jacdac Flex" || name == "JF") + return new Sensor({ + name: "Jac Flex", + rName: "JF", + f: () => modules.flex1.isConnected() ? modules.flex1.bending() : undefined, + min: -100, + max: 100, + isJacdacSensor: true, + setupFn: () => modules.flex1.start() + }); + + else + return new Sensor({ + name: "Jac Temp", + rName: "JT", + f: () => modules.temperature1.isConnected() ? modules.temperature1.temperature() : undefined, + min: -40, + max: 120, + isJacdacSensor: true, + setupFn: () => modules.temperature1.start() + }); + } - if (reading === undefined) - return - this.numberOfReadings += 1 - this.dataBuffer.push(reading); - this.heightNormalisedDataBuffer.push(Math.round(Screen.HEIGHT - ((reading - this.getMinimum()) / this.range) * (BUFFERED_SCREEN_HEIGHT - fromY)) - fromY); - } + //--------------------- + // Interface Functions: + //--------------------- - /** - * Populates this.normalisedBuffer with the Y position for each element in this.dataBuffer. - * Uses BUFFERED_SCREEN_HEIGHT. - * Invoked upon scrolling in the live-data-viewer. - * @param fromY The y value that each element should be offset by. - */ - normaliseDataBuffer(fromY: number) { - const min = this.getMinimum() - const range: number = Math.abs(min) + this.getMaximum(); - - this.heightNormalisedDataBuffer = [] - for (let i = 0; i < this.dataBuffer.length; i++) { - this.heightNormalisedDataBuffer.push(Math.round(Screen.HEIGHT - ((this.dataBuffer[i] - min) / range) * (BUFFERED_SCREEN_HEIGHT - fromY)) - fromY); - } - } + getName(): string { return this.name } + getRadioName(): string { return this.radioName } + getReading(): number { return this.sensorFn() } + getNormalisedReading(): number { return Math.abs(this.getReading()) / this.range } + getMinimum(): number { return this.minimum; } + getMaximum(): number { return this.maximum; } + isJacdac(): boolean { return this.isJacdacSensor; } - /** - * Set inside of recordingConfigSelection. - * @param config see recordingConfigSelection. - */ - setConfig(config: RecordingConfig) { - const isInEventMode = config.comparator != null && config.inequality != null - this.config = config - this.totalMeasurements = this.config.measurements - this.isInEventMode = isInEventMode - } + getMaxBufferSize(): number { return this.maxBufferSize } + getNthReading(n: number): number { return this.dataBuffer[n] } + getNthHeightNormalisedReading(n: number): number { return this.heightNormalisedDataBuffer[n] } + getBufferLength(): number { return this.dataBuffer.length } + getHeightNormalisedBufferLength(): number { return this.heightNormalisedDataBuffer.length } + getPeriod(): number { return this.config.period; } + getMeasurements(): number { return this.config.measurements } + hasMeasurements(): boolean { return this.config.measurements > 0; } - /** - * Records a sensor's reading to the datalogger. - * Will set the event column in the datalogger to "N/A" if not in event mode. - * Invoked by dataRecorder.log(). - * Writes the "Time (Ms)" column using a cumulative period. - */ - log(time: number): string { - this.lastLoggedReading = this.getReading() - - const reading = this.lastLoggedReading.toString().slice(0, READING_PRECISION) - - if (this.isInEventMode) { - if (sensorEventFunctionLookup[this.config.inequality](this.lastLoggedReading, this.config.comparator)) { - datalogger.log( - datalogger.createCV("Sensor", this.getName()), - datalogger.createCV("Time (ms)", time), - datalogger.createCV("Reading", reading), - datalogger.createCV("Event", this.config.inequality + " " + this.config.comparator) - ) - this.config.measurements -= 1 - return this.getRadioName() + "," + time.toString() + "," + reading + "," + this.config.inequality + " " + this.config.comparator - } - } - else { - datalogger.log( - datalogger.createCV("Sensor", this.getName()), - datalogger.createCV("Time (ms)", time.toString()), - datalogger.createCV("Reading", reading), - datalogger.createCV("Event", "N/A") - ) - this.config.measurements -= 1 - return this.getRadioName() + "," + time.toString() + "," + reading + "," + "N/A" - } - return "" + /** + * Used by the DataRecorder to display information about the sensor as it is logging. + * @returns linles of information that can be printed out into a box for display. + */ + getRecordingInformation(): string[] { + if (this.hasMeasurements()) + return [ + this.getPeriod() / 1000 + " second period", + this.config.measurements.toString() + " measurements left", + ((this.config.measurements * this.getPeriod()) / 1000).toString() + " seconds left", + "Last log was " + this.lastLoggedReading.toString().slice(0, 5), + ] + else + return [ + "Logging complete.", + "Last log was " + this.lastLoggedReading.toString().slice(0, 5), + ] + } + + /** + * Used by the DataRecorder to display information about the sensor as it is logging. + * @returns lines of information that can be printed out into a box for display. + */ + getEventInformation(): string[] { + if (this.hasMeasurements()) + return [ + this.config.measurements.toString() + " events left", + "Logging " + this.config.inequality + " " + this.config.comparator + " events", + "Last log was " + this.lastLoggedReading.toString().slice(0, 5), + this.lastLoggedEventDescription + ] + + else + return [ + "Logging complete.", + "Last log was " + this.lastLoggedReading.toString().slice(0, 5) + ] + } + + /** + * Change the size of the buffer used for this.dataBuffer & this.normalisedBuffer + * Will shift out old this.dataBuffer & this.normalisedBuffer values from the front. + * @param newBufferSize absolute new value for both this.dataBuffer & this.normalisedBuffer + */ + setBufferSize(newBufferSize: number): void { + // Remove additional values if neccessary: + if (this.dataBuffer.length > newBufferSize) { + const difference = this.dataBuffer.length - newBufferSize + this.dataBuffer.splice(0, difference) + this.heightNormalisedDataBuffer.splice(0, difference) + } + this.maxBufferSize = newBufferSize + } + + /** + * Add one value to this.dataBuffer, add that value normalised into this.normalisedBuffer too. + * No value is added if the reading is undefined (such as from a disconnected Jacdac sensor). + * If the (this.dataBuffer.length >= this.maxBufferSize) then then the oldest values are removed. + * @param fromY the offset by which the reading should be raised before adding to this.normalisedBuffer + * @returns + */ + readIntoBufferOnce(fromY: number): void { + const reading = this.getReading() + + if (this.dataBuffer.length >= this.maxBufferSize || reading === undefined) { + this.dataBuffer.shift(); + this.heightNormalisedDataBuffer.shift(); + } + + if (reading === undefined) + return + + this.numberOfReadings += 1 + this.dataBuffer.push(reading); + this.heightNormalisedDataBuffer.push(Math.round(Screen.HEIGHT - ((reading - this.getMinimum()) / this.range) * (BUFFERED_SCREEN_HEIGHT - fromY)) - fromY); + } + + /** + * Populates this.normalisedBuffer with the Y position for each element in this.dataBuffer. + * Uses BUFFERED_SCREEN_HEIGHT. + * Invoked upon scrolling in the live-data-viewer. + * @param fromY The y value that each element should be offset by. + */ + normaliseDataBuffer(fromY: number) { + const min = this.getMinimum() + const range: number = Math.abs(min) + this.getMaximum(); + + this.heightNormalisedDataBuffer = [] + for (let i = 0; i < this.dataBuffer.length; i++) { + this.heightNormalisedDataBuffer.push(Math.round(Screen.HEIGHT - ((this.dataBuffer[i] - min) / range) * (BUFFERED_SCREEN_HEIGHT - fromY)) - fromY); + } + } + + /** + * Set inside of recordingConfigSelection. + * @param config see recordingConfigSelection. + */ + setConfig(config: RecordingConfig) { + const isInEventMode = config.comparator != null && config.inequality != null + this.config = config + this.totalMeasurements = this.config.measurements + this.isInEventMode = isInEventMode + } + + /** + * Records a sensor's reading to the datalogger. + * Will set the event column in the datalogger to "N/A" if not in event mode. + * Invoked by dataRecorder.log(). + * Writes the "Time (Ms)" column using a cumulative period. + */ + log(time: number): string { + this.lastLoggedReading = this.getReading() + + const reading = this.lastLoggedReading.toString().slice(0, READING_PRECISION) + + if (this.isInEventMode) { + if (sensorEventFunctionLookup[this.config.inequality](this.lastLoggedReading, this.config.comparator)) { + datalogger.log( + datalogger.createCV("Sensor", this.getName()), + datalogger.createCV("Time (ms)", time), + datalogger.createCV("Reading", reading), + datalogger.createCV("Event", this.config.inequality + " " + this.config.comparator) + ) + this.config.measurements -= 1 + return this.getRadioName() + "," + time.toString() + "," + reading + "," + this.config.inequality + " " + this.config.comparator } + } + + else { + datalogger.log( + datalogger.createCV("Sensor", this.getName()), + datalogger.createCV("Time (ms)", time.toString()), + datalogger.createCV("Reading", reading), + datalogger.createCV("Event", "N/A") + ) + this.config.measurements -= 1 + return this.getRadioName() + "," + time.toString() + "," + reading + "," + "N/A" + } + return "" + } - /** - * Default draw mode: may be overriden to accommodate multiple draw modes - * Each value in the data buffer is normalised and scaled to screen size per frame. - * This is inefficient since only one value is added per frame - * @param fromX starting x coordinate - * @param color - */ - draw(fromX: number, color: number): void { - for (let i = 0; i < this.heightNormalisedDataBuffer.length - 1; i++) { - for (let j = -(PLOT_SMOOTHING_CONSTANT >> 1); j < PLOT_SMOOTHING_CONSTANT >> 1; j++) { - screen().drawLine( - fromX + i, - this.heightNormalisedDataBuffer[i] + j, - fromX + i + 1, - this.heightNormalisedDataBuffer[i + 1] + j, - color - ); - } - } + /** + * Default draw mode: may be overriden to accommodate multiple draw modes + * Each value in the data buffer is normalised and scaled to screen size per frame. + * This is inefficient since only one value is added per frame + * @param fromX starting x coordinate + * @param color + */ + draw(fromX: number, color: number): void { + for (let i = 0; i < this.heightNormalisedDataBuffer.length - 1; i++) { + for (let j = -(PLOT_SMOOTHING_CONSTANT >> 1); j < PLOT_SMOOTHING_CONSTANT >> 1; j++) { + screen().drawLine( + fromX + i, + this.heightNormalisedDataBuffer[i] + j, + fromX + i + 1, + this.heightNormalisedDataBuffer[i + 1] + j, + color + ); } + } } + } } diff --git a/tabularDataViewer.ts b/tabularDataViewer.ts index ef23c03..110a881 100644 --- a/tabularDataViewer.ts +++ b/tabularDataViewer.ts @@ -11,6 +11,8 @@ namespace microdata { */ const TABULAR_MAX_ROWS = 8 + const LOAD_QTY = 250 + /** * Locally used to control flow upon button presses: A, B, UP, DOWN */ @@ -48,6 +50,10 @@ namespace microdata { private static dataRows: string[][]; + + private static cache: string[][]; + + //--------- // FOR GUI: //--------- @@ -122,11 +128,14 @@ namespace microdata { /** TabularDataViewer may be entered from the Command Mode, DataViewSelect or View Data (Home screen 4th button) */ private goBack1PageFn: () => void + private static numOfRows: number; + + private currentlyFiltering: boolean; + constructor(app: AppInterface, goBack1PageFn: () => void) { super(app, "recordedDataViewer") this.guiState = DATA_VIEW_DISPLAY_MODE.UNFILTERED_DATA_VIEW - TabularDataViewer.needToScroll = datalogger.getNumberOfRows() > TABULAR_MAX_ROWS // Start on the 2nd row; since the first row is for headers: this.currentRow = 1 @@ -137,6 +146,7 @@ namespace microdata { this.filteredValue = "" this.filteredReadStarts = [0] this.filteredCol = 0 + this.currentlyFiltering = false; this.goBack1PageFn = goBack1PageFn } @@ -144,10 +154,13 @@ namespace microdata { /* override */ startup() { super.startup() basic.pause(50); + TabularDataViewer.numOfRows = datalogger.getNumberOfRows() TabularDataViewer.currentRowOffset = 0 TabularDataViewer.dataLoggerHeader = datalogger.getRows(TabularDataViewer.currentRowOffset, 1).split("\n")[0].split(","); TabularDataViewer.currentRowOffset = 1 + + TabularDataViewer.fillCache(0); TabularDataViewer.nextDataChunk(); this.headerStringLengths = TabularDataViewer.dataLoggerHeader.map((header) => (header.length + 5) * font.charWidth) @@ -156,10 +169,13 @@ namespace microdata { // Controls: //---------- - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.B.id, () => { + if (this.currentlyFiltering) + return; + if (this.guiState == DATA_VIEW_DISPLAY_MODE.FILTERED_DATA_VIEW) { TabularDataViewer.currentRowOffset = 1 this.currentRow = 1 @@ -173,7 +189,7 @@ namespace microdata { } ) - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.A.id, () => { @@ -183,28 +199,31 @@ namespace microdata { TabularDataViewer.currentRowOffset = 0 this.currentRow = 1 - + this.currentlyFiltering = true; this.nextFilteredDataChunk(); this.updateNeedToScroll(); + this.currentlyFiltering = false; this.guiState = DATA_VIEW_DISPLAY_MODE.FILTERED_DATA_VIEW } } ) - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.up.id, () => { let tick = true; - control.onEvent( + context.onEvent( ControllerButtonEvent.Released, controller.up.id, () => tick = false ) - // Control logic: while (tick) { + if (this.currentlyFiltering) + return; + if (this.currentRow > 0) this.currentRow = Math.max(this.currentRow - 1, 1); @@ -218,6 +237,10 @@ namespace microdata { if (this.guiState == DATA_VIEW_DISPLAY_MODE.UNFILTERED_DATA_VIEW) { TabularDataViewer.currentRowOffset = Math.max(TabularDataViewer.currentRowOffset - 1, 1); + + // if (TabularDataViewer.currentRowOffset % (LOAD_QTY - 6) == 0) { + // TabularDataViewer.fillCache(TabularDataViewer.currentRowOffset); + // } TabularDataViewer.nextDataChunk(); } else { @@ -230,16 +253,16 @@ namespace microdata { } // Reset binding - control.onEvent(ControllerButtonEvent.Released, controller.up.id, () => { }) + context.onEvent(ControllerButtonEvent.Released, controller.up.id, () => { }) } ) - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.down.id, () => { let tick = true; - control.onEvent( + context.onEvent( ControllerButtonEvent.Released, controller.down.id, () => tick = false @@ -247,7 +270,11 @@ namespace microdata { // Control logic: while (tick) { - let rowQty = (TabularDataViewer.dataRows.length < TABULAR_MAX_ROWS) ? TabularDataViewer.dataRows.length - 1 : datalogger.getNumberOfRows(); + if (this.currentlyFiltering) + return; + + let rowQty = (TabularDataViewer.dataRows.length < TABULAR_MAX_ROWS) ? TabularDataViewer.dataRows.length - 1 : TabularDataViewer.numOfRows; + // control.dmesg(`d ${(TabularDataViewer.currentRowOffset)}`) /** * Same situation as when scrolling UP: @@ -257,22 +284,27 @@ namespace microdata { */ // Boundary where there are TABULAR_MAX_ROWS - 1 number of rows: - if (datalogger.getNumberOfRows() == TABULAR_MAX_ROWS) + if (TabularDataViewer.numOfRows == TABULAR_MAX_ROWS) rowQty = TABULAR_MAX_ROWS - 1 if (this.guiState == DATA_VIEW_DISPLAY_MODE.FILTERED_DATA_VIEW) rowQty = this.numberOfFilteredRows if (TabularDataViewer.needToScroll) { + // if (true) { if (this.currentRow + 1 < TABULAR_MAX_ROWS) this.currentRow += 1; else if (TabularDataViewer.currentRowOffset <= rowQty - TABULAR_MAX_ROWS - 1) { TabularDataViewer.currentRowOffset += 1; - if (this.guiState == DATA_VIEW_DISPLAY_MODE.UNFILTERED_DATA_VIEW) + if (this.guiState == DATA_VIEW_DISPLAY_MODE.UNFILTERED_DATA_VIEW) { + // if (TabularDataViewer.currentRowOffset % (LOAD_QTY - 7) == 0) { + // TabularDataViewer.fillCache(TabularDataViewer.currentRowOffset - this.currentRow - TABULAR_MAX_ROWS); + // } + TabularDataViewer.nextDataChunk(); - else + } else this.nextFilteredDataChunk() } } @@ -282,11 +314,11 @@ namespace microdata { } basic.pause(100) } - control.onEvent(ControllerButtonEvent.Released, controller.down.id, () => { }) + context.onEvent(ControllerButtonEvent.Released, controller.down.id, () => { }) } ) - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.left.id, () => { @@ -294,7 +326,7 @@ namespace microdata { } ) - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.right.id, () => { @@ -313,21 +345,25 @@ namespace microdata { TabularDataViewer.nextDataChunk() } + private static fillCache(from: number) { + const rows = datalogger.getRows(from, LOAD_QTY).split("\n"); + // const rows = datalogger.getRows(TabularDataViewer.currentRowOffset, TABULAR_MAX_ROWS).split("\n"); + + TabularDataViewer.cache = [] + for (let i = 0; i < rows.length; i++) { + if (rows[i][0] != "") + TabularDataViewer.cache.push(rows[i].split(",")); + } + } + /** * Used to retrieve the next chunk of data. * Invoked when this.tabularYScrollOffset reaches its screen boundaries. * Mutates: this.dataRows */ private static nextDataChunk() { - const rows = datalogger.getRows(TabularDataViewer.currentRowOffset, TABULAR_MAX_ROWS).split("\n"); - TabularDataViewer.needToScroll = datalogger.getNumberOfRows() > TABULAR_MAX_ROWS - - let nextDataChunk = [TabularDataViewer.dataLoggerHeader] - for (let i = 0; i < rows.length; i++) { - if (rows[i][0] != "") - nextDataChunk.push(rows[i].split(",")); - } - TabularDataViewer.dataRows = nextDataChunk + TabularDataViewer.needToScroll = TabularDataViewer.numOfRows > TABULAR_MAX_ROWS + TabularDataViewer.dataRows = [TabularDataViewer.dataLoggerHeader].concat(TabularDataViewer.cache.slice(TabularDataViewer.currentRowOffset, TabularDataViewer.currentRowOffset + TABULAR_MAX_ROWS)); } @@ -350,26 +386,29 @@ namespace microdata { // if (TabularDataViewer.currentRowOffset == 0) // TabularDataViewer.dataRows.push(datalogger.getRows(1, 1).split("\n")[0].split(",")); // 0 -> 1; - while (start < datalogger.getNumberOfRows() && nextFilteredDataChunk.length < TABULAR_MAX_ROWS) { - const rows = datalogger.getRows(start, TABULAR_MAX_ROWS).split("\n"); // each row as 1 string - - // Turn each row into a column of data: - for (let i = 0; i < rows.length; i++) { - const cols = rows[i].split(","); + // while (start < TabularDataViewer.numOfRows && nextFilteredDataChunk.length < TABULAR_MAX_ROWS) { + // const rows = datalogger.getRows(start, TABULAR_MAX_ROWS).split("\n"); // each row as 1 string - // Only add if it's what we're looking for: - if (cols[this.filteredCol] == this.filteredValue) { - nextFilteredDataChunk.push(cols); + const rows = [TabularDataViewer.dataLoggerHeader].concat(TabularDataViewer.cache.slice(start, start + LOAD_QTY)); - // Document where this read started from, so the next read starts in the correct position: - // Either 3 or 2; since the first read has headers (1 additional row): - if (nextFilteredDataChunk.length == ((TabularDataViewer.currentRowOffset == 0) ? 3 : 2)) { - this.filteredReadStarts[TabularDataViewer.currentRowOffset + 1] = start + i - } - } + // Turn each row into a column of data: + for (let i = 0; i < rows.length; i++) { + // const cols = rows[i].split(","); + + // Only add if it's what we're looking for: + if (rows[i][this.filteredCol] == this.filteredValue) { + nextFilteredDataChunk.push(rows[i]); + + // Document where this read started from, so the next read starts in the correct position: + // Either 3 or 2; since the first read has headers (1 additional row): + // if (nextFilteredDataChunk.length == ((TabularDataViewer.currentRowOffset == 0) ? 3 : 2)) { + // this.filteredReadStarts[TabularDataViewer.currentRowOffset + 1] = start + i + this.filteredReadStarts[TabularDataViewer.currentRowOffset + 1] = this.filteredReadStarts[TabularDataViewer.currentRowOffset] + TABULAR_MAX_ROWS - 1 + // } } - start += Math.min(TABULAR_MAX_ROWS, datalogger.getNumberOfRows(start)) } + // start += Math.min(TABULAR_MAX_ROWS, TabularDataViewer.numOfRows - start) // NOTE: -start here + // } TabularDataViewer.dataRows = nextFilteredDataChunk } @@ -380,16 +419,18 @@ namespace microdata { * Based upon this.filteredValue */ private updateNeedToScroll() { - const chunkSize = Math.min(20, datalogger.getNumberOfRows()); // 20 as limit for search + // const chunkSize = Math.min(20, TabularDataViewer.numOfRows); // 20 as limit for search + const chunkSize = Math.min(LOAD_QTY, TabularDataViewer.numOfRows); // 20 as limit for search this.numberOfFilteredRows = 0 - for (let chunk = 0; chunk < datalogger.getNumberOfRows(); chunk += chunkSize) { - const rows = datalogger.getRows(chunk, chunkSize).split("\n"); - for (let i = 0; i < rows.length; i++) { - if (rows[i].split(",", 1)[this.filteredCol] == this.filteredValue) { - this.numberOfFilteredRows += 1 - } + // for (let chunk = 0; chunk < TabularDataViewer.numOfRows; chunk += chunkSize) { + // const rows = datalogger.getRows(chunk, chunkSize).split("\n"); + const rows = TabularDataViewer.cache.slice(0, chunkSize); + for (let i = 0; i < rows.length; i++) { + if (rows[i][this.filteredCol] == this.filteredValue) { + this.numberOfFilteredRows += 1 } + // } } // Are there more rows that we could display?