From 0e03c64810f7bfc7d67112c7f3d349fc038b10ef Mon Sep 17 00:00:00 2001 From: KierPalin <45743174+KierPalin@users.noreply.github.com> Date: Wed, 19 Nov 2025 14:37:17 +0000 Subject: [PATCH 1/9] version 1.7.5 --- app.ts | 114 ++++---- assets.ts | 30 +- clearDataloggerScreen.ts | 26 +- dataRecorder.ts | 12 +- dataViewSelect.ts | 28 +- distributedLogging.ts | 8 +- generateGraph.ts | 24 +- headlessMode.ts | 79 +++--- home.ts | 10 +- liveDataViewer.ts | 20 +- loggingConfig.ts | 20 +- package.json | 2 +- pxt.json | 2 +- sensorSelect.ts | 573 ++++++++++++++++++++++++++------------- sensors.ts | 2 +- tabularDataViewer.ts | 20 +- 16 files changed, 598 insertions(+), 372 deletions(-) diff --git a/app.ts b/app.ts index 3479372..e374563 100644 --- a/app.ts +++ b/app.ts @@ -1,57 +1,71 @@ 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) + + 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; } - // 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..847c4f3 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, () => { @@ -87,7 +87,7 @@ namespace microdata { ) // Scroll Up - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.up.id, () => { @@ -101,7 +101,7 @@ namespace microdata { ) // Scroll Down - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.down.id, () => { @@ -117,7 +117,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 +125,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..3837708 100644 --- a/dataViewSelect.ts +++ b/dataViewSelect.ts @@ -1,7 +1,6 @@ namespace microdata { import Screen = user_interface_base.Screen - import CursorSceneWithPriorPage = user_interface_base.CursorSceneWithPriorPage - import CursorSceneEnum = user_interface_base.CursorSceneEnum + import CursorScene = user_interface_base.CursorScene import Button = user_interface_base.Button import ButtonStyles = user_interface_base.ButtonStyles import AppInterface = user_interface_base.AppInterface @@ -12,15 +11,11 @@ namespace microdata { * A tabular view of the recorded data * A graph of the recorded data */ - export class DataViewSelect extends CursorSceneWithPriorPage { + export class DataViewSelect extends CursorScene { private dataloggerEmpty: boolean constructor(app: AppInterface) { - super(app, - function () { - this.app.popScene(); - this.app.pushScene(new Home(this.app)) - }) + super(app); } /* override */ startup() { @@ -36,12 +31,21 @@ namespace microdata { // No data in log (first row are headers) if (this.dataloggerEmpty) { - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.A.id, () => { this.app.popScene() - this.app.pushScene(new SensorSelect(this.app, CursorSceneEnum.RecordingConfigSelect)) + this.app.pushScene(new SensorSelect(this.app, MicroDataSceneEnum.RecordingConfigSelect)) + } + ) + + context.onEvent( + ControllerButtonEvent.Pressed, + controller.B.id, + () => { + this.app.popScene() + this.app.pushScene(new Home(this.app)) } ) } @@ -86,12 +90,12 @@ namespace microdata { datalogger.deleteLog() this.dataloggerEmpty = true - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.A.id, () => { this.app.popScene() - this.app.pushScene(new SensorSelect(this.app, CursorSceneEnum.RecordingConfigSelect)) + this.app.pushScene(new SensorSelect(this.app, MicroDataSceneEnum.RecordingConfigSelect)) } ) }, diff --git a/distributedLogging.ts b/distributedLogging.ts index 185306d..ac4fb49 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 @@ -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 diff --git a/generateGraph.ts b/generateGraph.ts index ef5c591..3354664 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, () => { diff --git a/headlessMode.ts b/headlessMode.ts index eac82e3..67a00f2 100644 --- a/headlessMode.ts +++ b/headlessMode.ts @@ -33,15 +33,16 @@ namespace microdata { ACCELERATION, TEMPERATURE, LIGHT, - MAGNET, - RADIO + MAGNET }; /** For module inside of B button. */ - const UI_SENSOR_SELECT_STATE_LEN = 5; + 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; + // const SENSOR_CFG: RecordingConfig = {measurements: undefined, period: undefined, } + /** * 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 @@ -64,15 +65,19 @@ namespace microdata { /** Mutated by the B button & .dynamicSensorSelectionLoop() */ private uiSensorSelectState: UI_SENSOR_SELECT_STATE; + private continueLogging: boolean; + constructor(app: App) { this.app = app; this.uiMode = UI_MODE.SENSOR_SELECTION; this.uiSensorSelectState = UI_SENSOR_SELECT_STATE.ACCELERATION; + this.continueLogging = false; // A Button input.onButtonPressed(1, () => { if (this.uiMode == UI_MODE.SENSOR_SELECTION) { this.uiMode = UI_MODE.LOGGING; + this.continueLogging = !this.continueLogging; this.log(); } }) @@ -285,28 +290,6 @@ namespace microdata { 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; - - basic.showLeds(` - . # # # . - # . . . # - . # # # . - # . . . # - . . # . . - `); - if (!this.waitUntilSensorSelectStateChange((SHOW_EACH_SENSOR_FOR_MS >> 1), 50, UI_SENSOR_SELECT_STATE.RADIO)) break; - - break; - } - default: break; } @@ -322,23 +305,39 @@ namespace microdata { private log() { const sensors = this.uiSelectionToSensors(); let time = 0; - control.inBackground(() => { - while (this.uiMode == UI_MODE.LOGGING) { + while (this.uiMode == UI_MODE.LOGGING && this.continueLogging) { 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") - ); + + const priorReadings: number[] = sensors.map((_) => undefined) + const EVENT_THRESHOLD_NORM = 0.2; + sensors.forEach((sensor, index) => { + // datalogger.log( + // datalogger.createCV("Sensor", sensor.getName()), + // datalogger.createCV("Time (ms)", time), + // datalogger.createCV("Reading", sensor.getReading()),Magnet + // datalogger.createCV("Event", "N/A") + // ); + + const reading = sensor.getNormalisedReading(); + if (priorReadings[index] == undefined) { + priorReadings[index] = reading; + } else if (Math.abs(priorReadings[index] - reading) >= EVENT_THRESHOLD_NORM) { + 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; + const WAIT_TIME_MS = 20; + // if (this.uiMode == UI_MODE.LOGGING) + // basic.showNumber((time / 1000)); + if (!this.waitUntilUIModeChanges(Math.max(0, WAIT_TIME_MS - (input.runningTime() - start)), 80, UI_MODE.LOGGING)) + break; + time += WAIT_TIME_MS; } return; }); @@ -367,10 +366,6 @@ namespace microdata { case UI_SENSOR_SELECT_STATE.MAGNET: return [Sensor.getFromName("Magnet")] - case UI_SENSOR_SELECT_STATE.RADIO: - new DistributedLoggingProtocol(this.app, false); - return [] - default: return [] } diff --git a/home.ts b/home.ts index 487980f..5f37a47 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)) }, }), @@ -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..962ab1e 100644 --- a/liveDataViewer.ts +++ b/liveDataViewer.ts @@ -141,7 +141,7 @@ namespace microdata { //-------------------------------- // Zoom in: - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.A.id, () => { @@ -168,7 +168,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 +194,7 @@ namespace microdata { } ) - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.up.id, () => { @@ -221,7 +221,7 @@ namespace microdata { } ) - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.down.id, () => { @@ -248,7 +248,7 @@ namespace microdata { } ) - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.left.id, () => { @@ -258,7 +258,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 +271,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 +294,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..63f01fb 100644 --- a/loggingConfig.ts +++ b/loggingConfig.ts @@ -1,7 +1,7 @@ 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 @@ -90,9 +90,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 +120,7 @@ namespace microdata { super.startup() basic.pause(50); - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.A.id, () => { @@ -148,7 +148,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 +210,7 @@ namespace microdata { } ) - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.B.id, () => { @@ -235,7 +235,7 @@ namespace microdata { } ) - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.up.id, () => { @@ -276,7 +276,7 @@ namespace microdata { } ) - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.down.id, () => { @@ -320,7 +320,7 @@ namespace microdata { } ) - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.left.id, () => { @@ -347,7 +347,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..565cd01 100644 --- a/pxt.json +++ b/pxt.json @@ -7,7 +7,7 @@ "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", diff --git a/sensorSelect.ts b/sensorSelect.ts index a14a1c9..de7a814 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; + } + } + } + } } - } -} +} \ No newline at end of file diff --git a/sensors.ts b/sensors.ts index 93dc455..7dc2eee 100644 --- a/sensors.ts +++ b/sensors.ts @@ -422,7 +422,7 @@ namespace microdata { name: "Magnet", rName: "M", f: () => input.magneticForce(Dimension.Strength), - min: -5000, + min: 0, max: 5000, isJacdacSensor: false }); diff --git a/tabularDataViewer.ts b/tabularDataViewer.ts index ef23c03..84c5c56 100644 --- a/tabularDataViewer.ts +++ b/tabularDataViewer.ts @@ -156,7 +156,7 @@ namespace microdata { // Controls: //---------- - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.B.id, () => { @@ -173,7 +173,7 @@ namespace microdata { } ) - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.A.id, () => { @@ -191,12 +191,12 @@ namespace microdata { } ) - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.up.id, () => { let tick = true; - control.onEvent( + context.onEvent( ControllerButtonEvent.Released, controller.up.id, () => tick = false @@ -230,16 +230,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 @@ -282,11 +282,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 +294,7 @@ namespace microdata { } ) - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.right.id, () => { From 1983a04388551fdba5c773ba851b8e8d890856dd Mon Sep 17 00:00:00 2001 From: KierPalin <45743174+KierPalin@users.noreply.github.com> Date: Wed, 19 Nov 2025 15:00:46 +0000 Subject: [PATCH 2/9] fixes --- dataViewSelect.ts | 50 +- home.ts | 2 +- pxt.json | 10 +- tabularDataViewer.ts | 1713 +++++++++++++++++++++++++++++++----------- 4 files changed, 1285 insertions(+), 490 deletions(-) diff --git a/dataViewSelect.ts b/dataViewSelect.ts index 3837708..0a28a6e 100644 --- a/dataViewSelect.ts +++ b/dataViewSelect.ts @@ -24,31 +24,6 @@ namespace microdata { // Includes the header: this.dataloggerEmpty = datalogger.getNumberOfRows() <= 1 - - //--------- - // Control: - //--------- - - // No data in log (first row are headers) - if (this.dataloggerEmpty) { - context.onEvent( - ControllerButtonEvent.Pressed, - controller.A.id, - () => { - this.app.popScene() - this.app.pushScene(new SensorSelect(this.app, MicroDataSceneEnum.RecordingConfigSelect)) - } - ) - - context.onEvent( - ControllerButtonEvent.Pressed, - controller.B.id, - () => { - this.app.popScene() - this.app.pushScene(new Home(this.app)) - } - ) - } const y = Screen.HEIGHT * 0.234 // y = 30 on an Arcade Shield of height 128 pixels @@ -101,6 +76,31 @@ namespace microdata { }, }) ]]) + + //--------- + // Control: + //--------- + + // No data in log (first row are headers) + if (this.dataloggerEmpty) { + context.onEvent( + ControllerButtonEvent.Pressed, + controller.A.id, + () => { + this.app.popScene() + this.app.pushScene(new SensorSelect(this.app, MicroDataSceneEnum.RecordingConfigSelect)) + } + ) + } + + context.onEvent( + ControllerButtonEvent.Pressed, + controller.B.id, + () => { + this.app.popScene() + this.app.pushScene(new Home(this.app)) + } + ) } draw() { diff --git a/home.ts b/home.ts index 5f37a47..3ebeba2 100644 --- a/home.ts +++ b/home.ts @@ -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), diff --git a/pxt.json b/pxt.json index 565cd01..2e72acd 100644 --- a/pxt.json +++ b/pxt.json @@ -3,11 +3,11 @@ "version": "1.7.5", "description": "Data science with micro:bit v2", "dependencies": { - "core": "*", - "radio": "*", - "microphone": "*", - "datalogger": "*", - "user-interface-base": "github:microbit-apps/user-interface-base#v0.0.33", + "core": "file:../core", + "radio": "file:../radio", + "microphone": "file:../microphone", + "datalogger": "file:../datalogger", + "user-interface-base": "file:../user-interface-base", "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", diff --git a/tabularDataViewer.ts b/tabularDataViewer.ts index 84c5c56..85f6d48 100644 --- a/tabularDataViewer.ts +++ b/tabularDataViewer.ts @@ -1,505 +1,1300 @@ namespace microdata { - import Screen = user_interface_base.Screen - import Scene = user_interface_base.Scene - import AppInterface = user_interface_base.AppInterface - import font = user_interface_base.font - - /** - * Display limits - * Data in excess will require scrolling to view - * Includes header row - */ - const TABULAR_MAX_ROWS = 8 - - /** - * Locally used to control flow upon button presses: A, B, UP, DOWN - */ - const enum DATA_VIEW_DISPLAY_MODE { - /** Show all data from all sensors. DEFAULT + State transition on B Press */ - UNFILTERED_DATA_VIEW, - /** Show the data from one selected sensors. State transition on A Press */ - FILTERED_DATA_VIEW, - } - - /** - * Used to view the information stored in the data logger - * Shows up to TABULAR_MAX_ROWS rows ordered by period descending. - * Shows the datalogger's header as the first row. - * UP, DOWN, LEFT, RIGHT to change rows & columns. - * Pressing shows all rows by the same sensor (filters the data) - */ - export class TabularDataViewer extends Scene { - /** Should the TabularDataViewer update dataRows on the next frame? Used by the DistributedLoggingProtocol to tell this screen to update dataRows when a new log is made in realtime */ - public static updateDataRowsOnNextFrame: boolean = false - - /** First row in the datalogger; always the first row in dataRows and thus always displayed at the top of the screen, even if scrolling past it. See .nextDataChunk() */ - public static dataLoggerHeader: string[]; + import Screen = user_interface_base.Screen + import Scene = user_interface_base.Scene + import AppInterface = user_interface_base.AppInterface + import font = user_interface_base.font /** - * Used to store a chunk of data <= TABULAR_MAX_ROWS in length. - * Either filtered or unfiltered data. - * Fetched on transition between states when pressing A or B. - * Or scrolling UP & DOWN - * - * Only modified by: - * .nextDataChunk() & - * .nextFilteredDataChunk() + * Display limits + * Data in excess will require scrolling to view + * Includes header row */ - private static dataRows: string[][]; - - - //--------- - // FOR GUI: - //--------- - - /** - * Needed to centre the headers in .draw() - * No need to calculate once per frame - */ - private headerStringLengths: number[]; - - /** - * Unfiltered at startJac Moist. - * Pressing A sets to Filtered - * Pressing B sets to Unfiltered - */ - private guiState: DATA_VIEW_DISPLAY_MODE; - - /** - * Will this viewer need to scroll to reveal all of the rows? - */ - private static needToScroll: boolean + const TABULAR_MAX_ROWS = 8 /** - * User modified column index; via UP & DOWN. - * - * Cursor location, when the cursor is on the first or last row - * and UP or DOWN is invoked this.currentRowOffset is modified once instead. - * Modified when pressing UP or DOWN + * Locally used to control flow upon button presses: A, B, UP, DOWN */ - private currentRow: number - - /** - * User modified column index; via LEFT & RIGHT. - * - * Used to determine which columns to draw. - */ - private currentCol: number - - /** - * Used as index into .filteredReadStarts by: - * .nextDataChunk() & - * .nextFilteredDataChunk() - * If .currentRow is on the first or last row this is modified - * causing the next chunk of data to be offset by 1. - * - * Modified when pressing UP or DOWN - */ - private static currentRowOffset: number - - /** - * This is unique per sensor, it is calculated once upon pressing A. - */ - private numberOfFilteredRows: number - - /** - * Set when pressing A, filtered against in this.nextFilteredDataChunk() - */ - private filteredValue: string - - - /** Which column did the user press A on? Corresponds to this.filteredValue */ - private filteredCol: number; + const enum DATA_VIEW_DISPLAY_MODE { + /** Show all data from all sensors. DEFAULT + State transition on B Press */ + UNFILTERED_DATA_VIEW, + /** Show the data from one selected sensors. State transition on A Press */ + FILTERED_DATA_VIEW, + } /** - * There may be any number of sensors, and each may have a unique period & number of measurements. - * Data is retrieved in batches via datalogger.getRow(): - * so it is neccessary to start at the index of the last filtered read. - * This array is a lookup for where to start reading from - using this.yScrollOffset as index + * Used to view the information stored in the data logger + * Shows up to TABULAR_MAX_ROWS rows ordered by period descending. + * Shows the datalogger's header as the first row. + * UP, DOWN, LEFT, RIGHT to change rows & columns. + * Pressing shows all rows by the same sensor (filters the data) */ - private filteredReadStarts: number[] - - /** TabularDataViewer may be entered from the Command Mode, DataViewSelect or View Data (Home screen 4th button) */ - private goBack1PageFn: () => void - - constructor(app: AppInterface, goBack1PageFn: () => void) { - super(app, "recordedDataViewer") + export class TabularDataViewer extends Scene { + /** Should the TabularDataViewer update dataRows on the next frame? Used by the DistributedLoggingProtocol to tell this screen to update dataRows when a new log is made in realtime */ + public static updateDataRowsOnNextFrame: boolean = false + + /** First row in the datalogger; always the first row in dataRows and thus always displayed at the top of the screen, even if scrolling past it. See .nextDataChunk() */ + public static dataLoggerHeader: string[]; + + /** + * Used to store a chunk of data <= TABULAR_MAX_ROWS in length. + * Either filtered or unfiltered data. + * Fetched on transition between states when pressing A or B. + * Or scrolling UP & DOWN + * + * Only modified by: + * .nextDataChunk() & + * .nextFilteredDataChunk() + */ + private static dataRows: string[][]; + + + //--------- + // FOR GUI: + //--------- + + /** + * Needed to centre the headers in .draw() + * No need to calculate once per frame + */ + private headerStringLengths: number[]; + + /** + * Unfiltered at startJac Moist. + * Pressing A sets to Filtered + * Pressing B sets to Unfiltered + */ + private guiState: DATA_VIEW_DISPLAY_MODE; + + /** + * Will this viewer need to scroll to reveal all of the rows? + */ + private static needToScroll: boolean + + /** + * User modified column index; via UP & DOWN. + * + * Cursor location, when the cursor is on the first or last row + * and UP or DOWN is invoked this.currentRowOffset is modified once instead. + * Modified when pressing UP or DOWN + */ + private currentRow: number + + /** + * User modified column index; via LEFT & RIGHT. + * + * Used to determine which columns to draw. + */ + private currentCol: number + + /** + * Used as index into .filteredReadStarts by: + * .nextDataChunk() & + * .nextFilteredDataChunk() + * If .currentRow is on the first or last row this is modified + * causing the next chunk of data to be offset by 1. + * + * Modified when pressing UP or DOWN + */ + private static currentRowOffset: number + + /** + * This is unique per sensor, it is calculated once upon pressing A. + */ + private numberOfFilteredRows: number + + /** + * Set when pressing A, filtered against in this.nextFilteredDataChunk() + */ + private filteredValue: string + + + /** Which column did the user press A on? Corresponds to this.filteredValue */ + private filteredCol: number; + + /** + * There may be any number of sensors, and each may have a unique period & number of measurements. + * Data is retrieved in batches via datalogger.getRow(): + * so it is neccessary to start at the index of the last filtered read. + * This array is a lookup for where to start reading from - using this.yScrollOffset as index + */ + private filteredReadStarts: number[] + + /** TabularDataViewer may be entered from the Command Mode, DataViewSelect or View Data (Home screen 4th button) */ + private goBack1PageFn: () => void + + constructor(app: AppInterface, goBack1PageFn: () => void) { + super(app, "recordedDataViewer") - this.guiState = DATA_VIEW_DISPLAY_MODE.UNFILTERED_DATA_VIEW - TabularDataViewer.needToScroll = datalogger.getNumberOfRows() > TABULAR_MAX_ROWS + 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 - this.currentCol = 0 + // Start on the 2nd row; since the first row is for headers: + this.currentRow = 1 + this.currentCol = 0 - this.numberOfFilteredRows = 0 + this.numberOfFilteredRows = 0 - this.filteredValue = "" - this.filteredReadStarts = [0] - this.filteredCol = 0 + this.filteredValue = "" + this.filteredReadStarts = [0] + this.filteredCol = 0 - this.goBack1PageFn = goBack1PageFn - } + this.goBack1PageFn = goBack1PageFn + } /* override */ startup() { - super.startup() - basic.pause(50); - - TabularDataViewer.currentRowOffset = 0 - TabularDataViewer.dataLoggerHeader = datalogger.getRows(TabularDataViewer.currentRowOffset, 1).split("\n")[0].split(","); - TabularDataViewer.currentRowOffset = 1 - TabularDataViewer.nextDataChunk(); + super.startup() - this.headerStringLengths = TabularDataViewer.dataLoggerHeader.map((header) => (header.length + 5) * font.charWidth) - - //---------- - // Controls: - //---------- - - context.onEvent( - ControllerButtonEvent.Pressed, - controller.B.id, - () => { - if (this.guiState == DATA_VIEW_DISPLAY_MODE.FILTERED_DATA_VIEW) { + TabularDataViewer.currentRowOffset = 0 + TabularDataViewer.dataLoggerHeader = datalogger.getRows(TabularDataViewer.currentRowOffset, 1).split("\n")[0].split(","); TabularDataViewer.currentRowOffset = 1 - this.currentRow = 1 - TabularDataViewer.nextDataChunk(); - this.guiState = DATA_VIEW_DISPLAY_MODE.UNFILTERED_DATA_VIEW - } - else { - this.goBack1PageFn() - } - } - ) - - context.onEvent( - ControllerButtonEvent.Pressed, - controller.A.id, - () => { - if (this.guiState == DATA_VIEW_DISPLAY_MODE.UNFILTERED_DATA_VIEW) { - this.filteredCol = this.currentCol; - this.filteredValue = TabularDataViewer.dataRows[this.currentRow][this.filteredCol] - TabularDataViewer.currentRowOffset = 0 - this.currentRow = 1 - - this.nextFilteredDataChunk(); - this.updateNeedToScroll(); - this.guiState = DATA_VIEW_DISPLAY_MODE.FILTERED_DATA_VIEW - } + this.headerStringLengths = TabularDataViewer.dataLoggerHeader.map((header) => (header.length + 5) * font.charWidth) + + //---------- + // Controls: + //---------- + + control.onEvent( + ControllerButtonEvent.Pressed, + controller.B.id, + () => { + if (this.guiState == DATA_VIEW_DISPLAY_MODE.FILTERED_DATA_VIEW) { + TabularDataViewer.currentRowOffset = 1 + this.currentRow = 1 + + TabularDataViewer.nextDataChunk(); + this.guiState = DATA_VIEW_DISPLAY_MODE.UNFILTERED_DATA_VIEW + } + else { + this.goBack1PageFn() + } + } + ) + + control.onEvent( + ControllerButtonEvent.Pressed, + controller.A.id, + () => { + if (this.guiState == DATA_VIEW_DISPLAY_MODE.UNFILTERED_DATA_VIEW) { + this.filteredCol = this.currentCol; + this.filteredValue = TabularDataViewer.dataRows[this.currentRow][this.filteredCol] + + TabularDataViewer.currentRowOffset = 0 + this.currentRow = 1 + + this.nextFilteredDataChunk(); + this.updateNeedToScroll(); + this.guiState = DATA_VIEW_DISPLAY_MODE.FILTERED_DATA_VIEW + } + } + ) + + control.onEvent( + ControllerButtonEvent.Pressed, + controller.up.id, + () => { + let tick = true; + control.onEvent( + ControllerButtonEvent.Released, + controller.up.id, + () => tick = false + ) + + + // Control logic: + while (tick) { + if (this.currentRow > 0) + this.currentRow = Math.max(this.currentRow - 1, 1); + + /** + * When scrolling up the cursor might be at the bottom of the screen; so just move the cursor up one. + * Or, the cursor could be on the 2nd row of the screen (index 1 since the first row are headers): + * So don't move the cursor, load a new chunk of data. + */ + if (TabularDataViewer.needToScroll && this.currentRow == 1) { + TabularDataViewer.currentRowOffset = Math.max(TabularDataViewer.currentRowOffset - 1, 1); + + if (this.guiState == DATA_VIEW_DISPLAY_MODE.UNFILTERED_DATA_VIEW) { + TabularDataViewer.currentRowOffset = Math.max(TabularDataViewer.currentRowOffset - 1, 1); + TabularDataViewer.nextDataChunk(); + } + else { + TabularDataViewer.currentRowOffset = Math.max(TabularDataViewer.currentRowOffset - 1, 0); + this.nextFilteredDataChunk() + } + } + if (!controller.up.isPressed()) + break + basic.pause(100) + } + + // Reset binding + control.onEvent(ControllerButtonEvent.Released, controller.up.id, () => { }) + } + ) + + control.onEvent( + ControllerButtonEvent.Pressed, + controller.down.id, + () => { + let tick = true; + control.onEvent( + ControllerButtonEvent.Released, + controller.down.id, + () => tick = false + ); + + // Control logic: + while (tick) { + let rowQty = (TabularDataViewer.dataRows.length < TABULAR_MAX_ROWS) ? TabularDataViewer.dataRows.length - 1 : datalogger.getNumberOfRows(); + + /** + * Same situation as when scrolling UP: + * When scrolling down the cursor might be at the top of the screen; so just move the cursor down one. + * Or, the cursor could be on the last row of the screen: + * So don't move the cursor, load a new chunk of data. + */ + + // Boundary where there are TABULAR_MAX_ROWS - 1 number of rows: + if (datalogger.getNumberOfRows() == 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 (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) + TabularDataViewer.nextDataChunk(); + else + this.nextFilteredDataChunk() + } + } + + else if (this.currentRow < rowQty) { + this.currentRow += 1; + } + + if (!controller.down.isPressed()) + break + basic.pause(100) + } + control.onEvent(ControllerButtonEvent.Released, controller.down.id, () => { }) + } + ) + + control.onEvent( + ControllerButtonEvent.Pressed, + controller.left.id, + () => { + this.currentCol = Math.max(this.currentCol - 1, 0) + } + ) + + control.onEvent( + ControllerButtonEvent.Pressed, + controller.right.id, + () => { + if (this.currentCol + 1 < TabularDataViewer.dataRows[0].length - 1) + this.currentCol += 1 + } + ) } - ) - - context.onEvent( - ControllerButtonEvent.Pressed, - controller.up.id, - () => { - let tick = true; - context.onEvent( - ControllerButtonEvent.Released, - controller.up.id, - () => tick = false - ) - - - // Control logic: - while (tick) { - if (this.currentRow > 0) - this.currentRow = Math.max(this.currentRow - 1, 1); - - /** - * When scrolling up the cursor might be at the bottom of the screen; so just move the cursor up one. - * Or, the cursor could be on the 2nd row of the screen (index 1 since the first row are headers): - * So don't move the cursor, load a new chunk of data. - */ - if (TabularDataViewer.needToScroll && this.currentRow == 1) { - TabularDataViewer.currentRowOffset = Math.max(TabularDataViewer.currentRowOffset - 1, 1); - - if (this.guiState == DATA_VIEW_DISPLAY_MODE.UNFILTERED_DATA_VIEW) { - TabularDataViewer.currentRowOffset = Math.max(TabularDataViewer.currentRowOffset - 1, 1); - TabularDataViewer.nextDataChunk(); - } - else { - TabularDataViewer.currentRowOffset = Math.max(TabularDataViewer.currentRowOffset - 1, 0); - this.nextFilteredDataChunk() - } - } - basic.pause(100) - } - // Reset binding - context.onEvent(ControllerButtonEvent.Released, controller.up.id, () => { }) - } - ) - - context.onEvent( - ControllerButtonEvent.Pressed, - controller.down.id, - () => { - let tick = true; - context.onEvent( - ControllerButtonEvent.Released, - controller.down.id, - () => tick = false - ); - - // Control logic: - while (tick) { - let rowQty = (TabularDataViewer.dataRows.length < TABULAR_MAX_ROWS) ? TabularDataViewer.dataRows.length - 1 : datalogger.getNumberOfRows(); - - /** - * Same situation as when scrolling UP: - * When scrolling down the cursor might be at the top of the screen; so just move the cursor down one. - * Or, the cursor could be on the last row of the screen: - * So don't move the cursor, load a new chunk of data. - */ - - // Boundary where there are TABULAR_MAX_ROWS - 1 number of rows: - if (datalogger.getNumberOfRows() == 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 (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) - TabularDataViewer.nextDataChunk(); - else - this.nextFilteredDataChunk() - } - } + //---------------- + // STATIC METHODS: + //---------------- - else if (this.currentRow < rowQty) { - this.currentRow += 1; - } - basic.pause(100) - } - context.onEvent(ControllerButtonEvent.Released, controller.down.id, () => { }) + public static updateDataChunks() { + TabularDataViewer.nextDataChunk() } - ) - context.onEvent( - ControllerButtonEvent.Pressed, - controller.left.id, - () => { - this.currentCol = Math.max(this.currentCol - 1, 0) - } - ) - - context.onEvent( - ControllerButtonEvent.Pressed, - controller.right.id, - () => { - if (this.currentCol + 1 < TabularDataViewer.dataRows[0].length - 1) - this.currentCol += 1 + /** + * 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 } - ) - } - - - //---------------- - // STATIC METHODS: - //---------------- - - public static updateDataChunks() { - TabularDataViewer.nextDataChunk() - } - - /** - * 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 - } - - - //------------------------------- - // Non static Data Chunk methods: - //------------------------------- - - - /** - * Fill this.dataRows with up to TABULAR_MAX_ROWS elements of data. - * Filter rows by this.filteredValue. - * Sets the next filteredReadStart by setting this.filteredReadStarts[this.yScrollOffset + 1] - * Mutates: this.dataRows - * Mutates: this.filteredReadStarts[this.yScrollOffset + 1] - */ - private nextFilteredDataChunk() { - let start = this.filteredReadStarts[TabularDataViewer.currentRowOffset]; - - let nextFilteredDataChunk = [TabularDataViewer.dataLoggerHeader] - // 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(","); - - // Only add if it's what we're looking for: - if (cols[this.filteredCol] == this.filteredValue) { - nextFilteredDataChunk.push(cols); - - // 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 + //------------------------------- + // Non static Data Chunk methods: + //------------------------------- + + + /** + * Fill this.dataRows with up to TABULAR_MAX_ROWS elements of data. + * Filter rows by this.filteredValue. + * Sets the next filteredReadStart by setting this.filteredReadStarts[this.yScrollOffset + 1] + * Mutates: this.dataRows + * Mutates: this.filteredReadStarts[this.yScrollOffset + 1] + */ + private nextFilteredDataChunk() { + let start = this.filteredReadStarts[TabularDataViewer.currentRowOffset]; + + let nextFilteredDataChunk = [TabularDataViewer.dataLoggerHeader] + // 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(","); + + // Only add if it's what we're looking for: + if (cols[this.filteredCol] == this.filteredValue) { + nextFilteredDataChunk.push(cols); + + // 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 + } + } + } + start += Math.min(TABULAR_MAX_ROWS, datalogger.getNumberOfRows(start)) } - } + + TabularDataViewer.dataRows = nextFilteredDataChunk } - start += Math.min(TABULAR_MAX_ROWS, datalogger.getNumberOfRows(start)) - } - TabularDataViewer.dataRows = nextFilteredDataChunk - } + /** + * Set this.numberOfFilteredRows & this.needToScroll + * Based upon this.filteredValue + */ + private updateNeedToScroll() { + const chunkSize = Math.min(20, datalogger.getNumberOfRows()); // 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 + } + } + } - /** - * Set this.numberOfFilteredRows & this.needToScroll - * Based upon this.filteredValue - */ - private updateNeedToScroll() { - const chunkSize = Math.min(20, datalogger.getNumberOfRows()); // 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 - } + // Are there more rows that we could display? + TabularDataViewer.needToScroll = this.numberOfFilteredRows > TABULAR_MAX_ROWS } - } - // Are there more rows that we could display? - TabularDataViewer.needToScroll = this.numberOfFilteredRows > TABULAR_MAX_ROWS - } + /** + * Each header and its corresopnding rows of data have variable lengths, + * The small screen sizes exaggerates these differences, hence variable column sizing. + * @param colBufferSizes this.headerStringLengths spliced by this.xScrollOffset + * @param rowBufferSize remains constant + */ + drawGridOfVariableColSize(colBufferSizes: number[], rowBufferSize: number) { + let cumulativeColOffset = 0 + + // Skip the first column: Time (Seconds): + for (let col = 0; col < colBufferSizes.length; col++) { + if (cumulativeColOffset + colBufferSizes[col] > Screen.WIDTH) { + break + } + + // The last column should use all remaining space, if it is lesser than that remaining space: + if (col == colBufferSizes.length - 1 || cumulativeColOffset + colBufferSizes[col] + colBufferSizes[col + 1] > Screen.WIDTH) + cumulativeColOffset += Screen.WIDTH - cumulativeColOffset; + else + cumulativeColOffset += colBufferSizes[col]; + + if (cumulativeColOffset <= Screen.WIDTH) { + Screen.drawLine( + Screen.LEFT_EDGE + cumulativeColOffset, + Screen.TOP_EDGE, + Screen.LEFT_EDGE + cumulativeColOffset, + Screen.HEIGHT, + 15 + ) + } + } - /** - * Each header and its corresopnding rows of data have variable lengths, - * The small screen sizes exaggerates these differences, hence variable column sizing. - * @param colBufferSizes this.headerStringLengths spliced by this.xScrollOffset - * @param rowBufferSize remains constant - */ - drawGridOfVariableColSize(colBufferSizes: number[], rowBufferSize: number) { - let cumulativeColOffset = 0 + for (let rowOffset = 0; rowOffset <= Screen.HEIGHT; rowOffset += rowBufferSize) { + Screen.drawLine( + Screen.LEFT_EDGE, + Screen.TOP_EDGE + rowOffset, + Screen.WIDTH, + Screen.TOP_EDGE + rowOffset, + 15 + ) + } - // Skip the first column: Time (Seconds): - for (let col = 0; col < colBufferSizes.length; col++) { - if (cumulativeColOffset + colBufferSizes[col] > Screen.WIDTH) { - break + // Draw selected box: + Screen.drawRect( + Screen.LEFT_EDGE, + Screen.TOP_EDGE + (this.currentRow * rowBufferSize), + colBufferSizes[0], + rowBufferSize, + 6 + ) } - // The last column should use all remaining space, if it is lesser than that remaining space: - if (col == colBufferSizes.length - 1 || cumulativeColOffset + colBufferSizes[col] + colBufferSizes[col + 1] > Screen.WIDTH) - cumulativeColOffset += Screen.WIDTH - cumulativeColOffset; - else - cumulativeColOffset += colBufferSizes[col]; - - if (cumulativeColOffset <= Screen.WIDTH) { - Screen.drawLine( - Screen.LEFT_EDGE + cumulativeColOffset, - Screen.TOP_EDGE, - Screen.LEFT_EDGE + cumulativeColOffset, - Screen.HEIGHT, - 15 - ) - } - } - - for (let rowOffset = 0; rowOffset <= Screen.HEIGHT; rowOffset += rowBufferSize) { - Screen.drawLine( - Screen.LEFT_EDGE, - Screen.TOP_EDGE + rowOffset, - Screen.WIDTH, - Screen.TOP_EDGE + rowOffset, - 15 - ) - } - - // Draw selected box: - Screen.drawRect( - Screen.LEFT_EDGE, - Screen.TOP_EDGE + (this.currentRow * rowBufferSize), - colBufferSizes[0], - rowBufferSize, - 6 - ) - } + draw() { + Screen.fillRect( + Screen.LEFT_EDGE, + Screen.TOP_EDGE, + Screen.WIDTH, + Screen.HEIGHT, + 0xC + ) + + if (TabularDataViewer.updateDataRowsOnNextFrame) + TabularDataViewer.nextDataChunk() + + + // Could be optimised by calculating the Col line boundaries once & re-using them, instead of each frame: + const tabularRowBufferSize = Screen.HEIGHT / Math.min(TabularDataViewer.dataRows.length, TABULAR_MAX_ROWS); + this.drawGridOfVariableColSize(this.headerStringLengths.slice(this.currentCol), tabularRowBufferSize) + + // Write the data into the grid: + for (let row = 0; row < Math.min(TabularDataViewer.dataRows.length, TABULAR_MAX_ROWS); row++) { + let cumulativeColOffset = 0; + + // Go through each column: + for (let col = 0; col < TabularDataViewer.dataRows[0].length - this.currentCol; col++) { + const colID: number = col + this.currentCol; + + let columnValue: string = TabularDataViewer.dataRows[row][colID]; + + // Bounds check: + if (cumulativeColOffset + this.headerStringLengths[colID] > Screen.WIDTH) + break; + + // In this.drawGridOfVariableSize: If the column after this one would not fit grant this one the remanining space + // This will align the text to the center of this column space + if (colID == TabularDataViewer.dataRows[0].length - 1 || cumulativeColOffset + this.headerStringLengths[colID] + this.headerStringLengths[colID + 1] > Screen.WIDTH) + cumulativeColOffset += ((Screen.WIDTH - cumulativeColOffset) >> 1) - (this.headerStringLengths[colID] >> 1); + + // Write the columnValue in the centre of each grid box: + Screen.print( + columnValue, + Screen.LEFT_EDGE + cumulativeColOffset + (this.headerStringLengths[colID] >> 1) - ((font.charWidth * columnValue.length) >> 1), + Screen.TOP_EDGE + (row * tabularRowBufferSize) + (tabularRowBufferSize >> 1) - 4, + // 0xb, + 1, + bitmaps.font8 + ) + + cumulativeColOffset += this.headerStringLengths[colID] + } + } - draw() { - Screen.fillRect( - Screen.LEFT_EDGE, - Screen.TOP_EDGE, - Screen.WIDTH, - Screen.HEIGHT, - 0xC - ) - - if (TabularDataViewer.updateDataRowsOnNextFrame) - TabularDataViewer.nextDataChunk() - - - // Could be optimised by calculating the Col line boundaries once & re-using them, instead of each frame: - const tabularRowBufferSize = Screen.HEIGHT / Math.min(TabularDataViewer.dataRows.length, TABULAR_MAX_ROWS); - this.drawGridOfVariableColSize(this.headerStringLengths.slice(this.currentCol), tabularRowBufferSize) - - // Write the data into the grid: - for (let row = 0; row < Math.min(TabularDataViewer.dataRows.length, TABULAR_MAX_ROWS); row++) { - let cumulativeColOffset = 0; - - // Go through each column: - for (let col = 0; col < TabularDataViewer.dataRows[0].length - this.currentCol; col++) { - const colID: number = col + this.currentCol; - - let columnValue: string = TabularDataViewer.dataRows[row][colID]; - - // Bounds check: - if (cumulativeColOffset + this.headerStringLengths[colID] > Screen.WIDTH) - break; - - // In this.drawGridOfVariableSize: If the column after this one would not fit grant this one the remanining space - // This will align the text to the center of this column space - if (colID == TabularDataViewer.dataRows[0].length - 1 || cumulativeColOffset + this.headerStringLengths[colID] + this.headerStringLengths[colID + 1] > Screen.WIDTH) - cumulativeColOffset += ((Screen.WIDTH - cumulativeColOffset) >> 1) - (this.headerStringLengths[colID] >> 1); - - // Write the columnValue in the centre of each grid box: - Screen.print( - columnValue, - Screen.LEFT_EDGE + cumulativeColOffset + (this.headerStringLengths[colID] >> 1) - ((font.charWidth * columnValue.length) >> 1), - Screen.TOP_EDGE + (row * tabularRowBufferSize) + (tabularRowBufferSize >> 1) - 4, - // 0xb, - 1, - bitmaps.font8 - ) - - cumulativeColOffset += this.headerStringLengths[colID] + super.draw() } - } - - super.draw() } - } } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From ed66f5b69de030fac4440a3c44f533564ed3623e Mon Sep 17 00:00:00 2001 From: KierPalin <45743174+KierPalin@users.noreply.github.com> Date: Sun, 23 Nov 2025 10:44:28 +0800 Subject: [PATCH 3/9] headless mode for faster polling, no fibers. experiment.ts to setup jacdac lights + pxt.json for its dependencies --- experiments.ts | 44 ++++ headlessMode.ts | 574 ++++++++++++++++++++---------------------------- pxt.json | 8 +- 3 files changed, 287 insertions(+), 339 deletions(-) create mode 100644 experiments.ts diff --git a/experiments.ts b/experiments.ts new file mode 100644 index 0000000..d747007 --- /dev/null +++ b/experiments.ts @@ -0,0 +1,44 @@ +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, () => { + // basic.showNumber(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, () => { + // basic.showNumber(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/headlessMode.ts b/headlessMode.ts index 67a00f2..4c6f5ee 100644 --- a/headlessMode.ts +++ b/headlessMode.ts @@ -1,374 +1,274 @@ 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 - }; - - /** 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; - - // const SENSOR_CFG: RecordingConfig = {measurements: undefined, period: undefined, } - - /** - * 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; - - private continueLogging: boolean; - - constructor(app: App) { - this.app = app; - this.uiMode = UI_MODE.SENSOR_SELECTION; - this.uiSensorSelectState = UI_SENSOR_SELECT_STATE.ACCELERATION; - this.continueLogging = false; - - // A Button - input.onButtonPressed(1, () => { - if (this.uiMode == UI_MODE.SENSOR_SELECTION) { - this.uiMode = UI_MODE.LOGGING; - this.continueLogging = !this.continueLogging; - 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 + }; + + /** 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; + + // 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; + if (!this.waitUntilSensorSelectStateChange((SHOW_EACH_SENSOR_FOR_MS >> 1), 50, UI_SENSOR_SELECT_STATE.MAGNET)) break; - 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 = 50; + let start = input.runningTime(); + 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") + ); }); + 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 && this.continueLogging) { - let start = input.runningTime(); - - const priorReadings: number[] = sensors.map((_) => undefined) - const EVENT_THRESHOLD_NORM = 0.2; - sensors.forEach((sensor, index) => { - // datalogger.log( - // datalogger.createCV("Sensor", sensor.getName()), - // datalogger.createCV("Time (ms)", time), - // datalogger.createCV("Reading", sensor.getReading()),Magnet - // datalogger.createCV("Event", "N/A") - // ); - - const reading = sensor.getNormalisedReading(); - if (priorReadings[index] == undefined) { - priorReadings[index] = reading; - } else if (Math.abs(priorReadings[index] - reading) >= EVENT_THRESHOLD_NORM) { - datalogger.log( - datalogger.createCV("Sensor", sensor.getName()), - datalogger.createCV("Time (ms)", time), - datalogger.createCV("Reading", sensor.getReading()), - datalogger.createCV("Event", "N/A") - ); - } - }); - - const WAIT_TIME_MS = 20; - // if (this.uiMode == UI_MODE.LOGGING) - // basic.showNumber((time / 1000)); - if (!this.waitUntilUIModeChanges(Math.max(0, WAIT_TIME_MS - (input.runningTime() - start)), 80, UI_MODE.LOGGING)) - break; - time += WAIT_TIME_MS; - } - 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; - /** - * 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")] + basic.pause(period) + } + return true; + } - case UI_SENSOR_SELECT_STATE.TEMPERATURE: - return [Sensor.getFromName("Temp.")] - case UI_SENSOR_SELECT_STATE.LIGHT: - return [Sensor.getFromName("Light")] + /** + * 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.MAGNET: - return [Sensor.getFromName("Magnet")] + case UI_SENSOR_SELECT_STATE.TEMPERATURE: + return [Sensor.getFromName("Temp.")] - default: - return [] - } - } + case UI_SENSOR_SELECT_STATE.LIGHT: + return [Sensor.getFromName("Light")] + + case UI_SENSOR_SELECT_STATE.MAGNET: + return [Sensor.getFromName("Magnet")] + + default: + return [] + } } + } } diff --git a/pxt.json b/pxt.json index 2e72acd..5deeaeb 100644 --- a/pxt.json +++ b/pxt.json @@ -13,7 +13,10 @@ "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" From ccebb7be2015189eb5187e650040d3b77b0cb351 Mon Sep 17 00:00:00 2001 From: KierPalin <45743174+KierPalin@users.noreply.github.com> Date: Sun, 23 Nov 2025 22:46:49 +0800 Subject: [PATCH 4/9] stability --- app.ts | 16 ++- dataViewSelect.ts | 232 +++++++++++++++++++++--------------------- distributedLogging.ts | 4 +- experiments.ts | 2 - home.ts | 9 +- 5 files changed, 139 insertions(+), 124 deletions(-) diff --git a/app.ts b/app.ts index e374563..b610e81 100644 --- a/app.ts +++ b/app.ts @@ -8,6 +8,7 @@ namespace microdata { * 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, @@ -45,11 +46,24 @@ namespace microdata { this.sceneManager = new SceneManager() datalogger.includeTimestamp(FlashLogTimeStampFormat.None) + + // datalogger.deleteLog(datalogger.DeleteType.Fast) + // for (let i = 0; i < 10; i++) { + // datalogger.log( + // datalogger.createCV("Sensor", "test"), + // 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(this); + new HeadlessMode(); } public pushScene(scene: Scene) { diff --git a/dataViewSelect.ts b/dataViewSelect.ts index 0a28a6e..98207dd 100644 --- a/dataViewSelect.ts +++ b/dataViewSelect.ts @@ -1,129 +1,129 @@ namespace microdata { - import Screen = user_interface_base.Screen - import CursorScene = user_interface_base.CursorScene - import Button = user_interface_base.Button - import ButtonStyles = user_interface_base.ButtonStyles - import AppInterface = user_interface_base.AppInterface - - /** - * Choose between: - * Resetting Datalogger - * A tabular view of the recorded data - * A graph of the recorded data - */ - export class DataViewSelect extends CursorScene { - private dataloggerEmpty: boolean - - constructor(app: AppInterface) { - super(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 + + /** + * Choose between: + * Resetting Datalogger + * A tabular view of the recorded data + * A graph of the recorded data + */ + export class DataViewSelect extends CursorScene { + private dataloggerEmpty: boolean + + constructor(app: AppInterface) { + super(app); + } /* 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 - - 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 - - context.onEvent( - ControllerButtonEvent.Pressed, - controller.A.id, - () => { - this.app.popScene() - this.app.pushScene(new SensorSelect(this.app, MicroDataSceneEnum.RecordingConfigSelect)) - } - ) - }, - }) - ]]) - - //--------- - // Control: - //--------- - - // No data in log (first row are headers) - if (this.dataloggerEmpty) { - context.onEvent( - ControllerButtonEvent.Pressed, - controller.A.id, - () => { - this.app.popScene() - this.app.pushScene(new SensorSelect(this.app, MicroDataSceneEnum.RecordingConfigSelect)) - } - ) - } + 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 + + 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, () => { 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 context.onEvent( - ControllerButtonEvent.Pressed, - controller.B.id, - () => { - this.app.popScene() - this.app.pushScene(new Home(this.app)) - } + ControllerButtonEvent.Pressed, + controller.A.id, + () => { + this.app.popScene() + this.app.pushScene(new SensorSelect(this.app, MicroDataSceneEnum.RecordingConfigSelect)) + } ) + }, + }) + ]]) + + //--------- + // Control: + //--------- + + // No data in log (first row are headers) + if (this.dataloggerEmpty) { + context.onEvent( + ControllerButtonEvent.Pressed, + controller.A.id, + () => { + this.app.popScene() + this.app.pushScene(new SensorSelect(this.app, MicroDataSceneEnum.RecordingConfigSelect)) + } + ) + } + + 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 - ) + 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; - } + if (this.dataloggerEmpty) { + screen().printCenter("No data has been recorded", 5) + screen().printCenter("Press A to Record some!", Screen.HALF_HEIGHT) + return; + } - else { - screen().printCenter("Recorded Data Options", 5) - this.navigator.drawComponents(); - } + else { + screen().printCenter("Recorded Data Options", 5) + this.navigator.drawComponents(); + } - super.draw() - } + super.draw() } + } } diff --git a/distributedLogging.ts b/distributedLogging.ts index ac4fb49..238a8d9 100644 --- a/distributedLogging.ts +++ b/distributedLogging.ts @@ -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)) })) } } } @@ -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 index d747007..0cc049f 100644 --- a/experiments.ts +++ b/experiments.ts @@ -15,7 +15,6 @@ namespace microdata { // A: input.onButtonPressed(1, () => { - // basic.showNumber(1) const red = 0xFF0000 for (let i = 0; i < modules.led1.numPixels(); i++) { modules.led1.setPixelColor(i, red) @@ -26,7 +25,6 @@ namespace microdata { // B: input.onButtonPressed(2, () => { - // basic.showNumber(2) const blue = 0x0000FF for (let i = 0; i < modules.led1.numPixels(); i++) { modules.led1.setPixelColor(i, blue) diff --git a/home.ts b/home.ts index 3ebeba2..90cc28e 100644 --- a/home.ts +++ b/home.ts @@ -48,13 +48,16 @@ namespace microdata { new Button({ parent: null, style: ButtonStyles.Transparent, - icon: "radio_set_group", - ariaId: "Command Mode", + // icon: "radio_set_group", + // ariaId: "Command Mode", + icon: "largeSettingsGear", + ariaId: "Experiments", x: 20, y, onClick: () => { this.app.popScene() - this.app.pushScene(new DistributedLoggingScreen(this.app)) + // this.app.pushScene(new DistributedLoggingScreen(this.app)) + this.app.pushScene(new JacdacLightExperiment(this.app)) }, }), From eecaaba77e475a6290bda30f821cfbd783305076 Mon Sep 17 00:00:00 2001 From: KierPalin <45743174+KierPalin@users.noreply.github.com> Date: Sun, 23 Nov 2025 22:47:16 +0800 Subject: [PATCH 5/9] bedrock for better sliding window solution. --- tabularDataViewer.ts | 1760 ++++++++++++------------------------------ 1 file changed, 514 insertions(+), 1246 deletions(-) diff --git a/tabularDataViewer.ts b/tabularDataViewer.ts index 85f6d48..df789a3 100644 --- a/tabularDataViewer.ts +++ b/tabularDataViewer.ts @@ -1,1300 +1,568 @@ namespace microdata { - import Screen = user_interface_base.Screen - import Scene = user_interface_base.Scene - import AppInterface = user_interface_base.AppInterface - import font = user_interface_base.font + import Screen = user_interface_base.Screen + import Scene = user_interface_base.Scene + import AppInterface = user_interface_base.AppInterface + import font = user_interface_base.font + + /** + * Display limits + * Data in excess will require scrolling to view + * Includes header row + */ + const TABULAR_MAX_ROWS = 8 + + + //** I think this can be far higher. Max row size is easy to calculate. */ + const MAX_ROWS_TO_CACHE = 256; + + /** + * Locally used to control flow upon button presses: A, B, UP, DOWN + */ + const enum DATA_VIEW_DISPLAY_MODE { + /** Show all data from all sensors. DEFAULT + State transition on B Press */ + UNFILTERED_DATA_VIEW, + /** Show the data from one selected sensors. State transition on A Press */ + FILTERED_DATA_VIEW, + } + + /** + * Used to view the information stored in the data logger + * Shows up to TABULAR_MAX_ROWS rows ordered by period descending. + * Shows the datalogger's header as the first row. + * UP, DOWN, LEFT, RIGHT to change rows & columns. + * Pressing shows all rows by the same sensor (filters the data) + */ + export class TabularDataViewer extends Scene { + /** Should the TabularDataViewer update dataRows on the next frame? Used by the DistributedLoggingProtocol to tell this screen to update dataRows when a new log is made in realtime */ + public static updateDataRowsOnNextFrame: boolean = false + + /** First row in the datalogger; always the first row in dataRows and thus always displayed at the top of the screen, even if scrolling past it. See .nextDataChunk() */ + public static dataLoggerHeader: string[]; /** - * Display limits - * Data in excess will require scrolling to view - * Includes header row + * Used to store a chunk of data <= TABULAR_MAX_ROWS in length. + * Either filtered or unfiltered data. + * Fetched on transition between states when pressing A or B. + * Or scrolling UP & DOWN + * + * Only modified by: + * .nextDataChunk() & + * .nextFilteredDataChunk() */ - const TABULAR_MAX_ROWS = 8 + private static dataRows: string[][]; + + + //--------- + // FOR GUI: + //--------- /** - * Locally used to control flow upon button presses: A, B, UP, DOWN + * Needed to centre the headers in .draw() + * No need to calculate once per frame */ - const enum DATA_VIEW_DISPLAY_MODE { - /** Show all data from all sensors. DEFAULT + State transition on B Press */ - UNFILTERED_DATA_VIEW, - /** Show the data from one selected sensors. State transition on A Press */ - FILTERED_DATA_VIEW, - } + private headerStringLengths: number[]; /** - * Used to view the information stored in the data logger - * Shows up to TABULAR_MAX_ROWS rows ordered by period descending. - * Shows the datalogger's header as the first row. - * UP, DOWN, LEFT, RIGHT to change rows & columns. - * Pressing shows all rows by the same sensor (filters the data) + * Unfiltered at startJac Moist. + * Pressing A sets to Filtered + * Pressing B sets to Unfiltered */ - export class TabularDataViewer extends Scene { - /** Should the TabularDataViewer update dataRows on the next frame? Used by the DistributedLoggingProtocol to tell this screen to update dataRows when a new log is made in realtime */ - public static updateDataRowsOnNextFrame: boolean = false - - /** First row in the datalogger; always the first row in dataRows and thus always displayed at the top of the screen, even if scrolling past it. See .nextDataChunk() */ - public static dataLoggerHeader: string[]; - - /** - * Used to store a chunk of data <= TABULAR_MAX_ROWS in length. - * Either filtered or unfiltered data. - * Fetched on transition between states when pressing A or B. - * Or scrolling UP & DOWN - * - * Only modified by: - * .nextDataChunk() & - * .nextFilteredDataChunk() - */ - private static dataRows: string[][]; - - - //--------- - // FOR GUI: - //--------- - - /** - * Needed to centre the headers in .draw() - * No need to calculate once per frame - */ - private headerStringLengths: number[]; - - /** - * Unfiltered at startJac Moist. - * Pressing A sets to Filtered - * Pressing B sets to Unfiltered - */ - private guiState: DATA_VIEW_DISPLAY_MODE; - - /** - * Will this viewer need to scroll to reveal all of the rows? - */ - private static needToScroll: boolean - - /** - * User modified column index; via UP & DOWN. - * - * Cursor location, when the cursor is on the first or last row - * and UP or DOWN is invoked this.currentRowOffset is modified once instead. - * Modified when pressing UP or DOWN - */ - private currentRow: number - - /** - * User modified column index; via LEFT & RIGHT. - * - * Used to determine which columns to draw. - */ - private currentCol: number - - /** - * Used as index into .filteredReadStarts by: - * .nextDataChunk() & - * .nextFilteredDataChunk() - * If .currentRow is on the first or last row this is modified - * causing the next chunk of data to be offset by 1. - * - * Modified when pressing UP or DOWN - */ - private static currentRowOffset: number - - /** - * This is unique per sensor, it is calculated once upon pressing A. - */ - private numberOfFilteredRows: number - - /** - * Set when pressing A, filtered against in this.nextFilteredDataChunk() - */ - private filteredValue: string - - - /** Which column did the user press A on? Corresponds to this.filteredValue */ - private filteredCol: number; - - /** - * There may be any number of sensors, and each may have a unique period & number of measurements. - * Data is retrieved in batches via datalogger.getRow(): - * so it is neccessary to start at the index of the last filtered read. - * This array is a lookup for where to start reading from - using this.yScrollOffset as index - */ - private filteredReadStarts: number[] - - /** TabularDataViewer may be entered from the Command Mode, DataViewSelect or View Data (Home screen 4th button) */ - private goBack1PageFn: () => void - - constructor(app: AppInterface, goBack1PageFn: () => void) { - super(app, "recordedDataViewer") + private guiState: DATA_VIEW_DISPLAY_MODE; - this.guiState = DATA_VIEW_DISPLAY_MODE.UNFILTERED_DATA_VIEW - TabularDataViewer.needToScroll = datalogger.getNumberOfRows() > TABULAR_MAX_ROWS + /** + * Will this viewer need to scroll to reveal all of the rows? + */ + private static needToScroll: boolean + + /** + * User modified column index; via UP & DOWN. + * + * Cursor location, when the cursor is on the first or last row + * and UP or DOWN is invoked this.currentRowOffset is modified once instead. + * Modified when pressing UP or DOWN + */ + private static currentRow: number + + /** + * User modified column index; via LEFT & RIGHT. + * + * Used to determine which columns to draw. + */ + private currentCol: number - // Start on the 2nd row; since the first row is for headers: - this.currentRow = 1 - this.currentCol = 0 + /** + * Used as index into .filteredReadStarts by: + * .nextDataChunk() & + * .nextFilteredDataChunk() + * If .currentRow is on the first or last row this is modified + * causing the next chunk of data to be offset by 1. + * + * Modified when pressing UP or DOWN + */ + private static currentRowOffset: number - this.numberOfFilteredRows = 0 + /** + * This is unique per sensor, it is calculated once upon pressing A. + */ + private numberOfFilteredRows: number - this.filteredValue = "" - this.filteredReadStarts = [0] - this.filteredCol = 0 + /** + * Set when pressing A, filtered against in this.nextFilteredDataChunk() + */ + private filteredValue: string - this.goBack1PageFn = goBack1PageFn - } - /* override */ startup() { - super.startup() + /** Which column did the user press A on? Corresponds to this.filteredValue */ + private filteredCol: number; - TabularDataViewer.currentRowOffset = 0 - TabularDataViewer.dataLoggerHeader = datalogger.getRows(TabularDataViewer.currentRowOffset, 1).split("\n")[0].split(","); + /** + * There may be any number of sensors, and each may have a unique period & number of measurements. + * Data is retrieved in batches via datalogger.getRow(): + * so it is neccessary to start at the index of the last filtered read. + * This array is a lookup for where to start reading from - using this.yScrollOffset as index + */ + private filteredReadStarts: number[] + + /** TabularDataViewer may be entered from the Command Mode, DataViewSelect or View Data (Home screen 4th button) */ + private goBack1PageFn: () => void + + private static datalogCache: string[][]; + + private static numberOfRows: number; + + constructor(app: AppInterface, goBack1PageFn: () => void) { + super(app, "recordedDataViewer") + + this.guiState = DATA_VIEW_DISPLAY_MODE.UNFILTERED_DATA_VIEW + + this.currentCol = 0 + + this.numberOfFilteredRows = 0 + + this.filteredValue = "" + this.filteredReadStarts = [0] + this.filteredCol = 0 + + this.goBack1PageFn = goBack1PageFn + } + + /* override */ startup() { + super.startup() + + TabularDataViewer.numberOfRows = datalogger.getNumberOfRows(); + TabularDataViewer.needToScroll = TabularDataViewer.numberOfRows > TABULAR_MAX_ROWS + + // Start on the 2nd row; since the first row is for headers: + TabularDataViewer.currentRow = 1 + TabularDataViewer.currentRowOffset = 0 + TabularDataViewer.dataLoggerHeader = datalogger.getRows(TabularDataViewer.currentRowOffset, 1).split("\n")[0].split(","); + TabularDataViewer.currentRowOffset = 1 // NOTE: ??? + TabularDataViewer.fillCache(); + TabularDataViewer.nextDataChunk(); + + this.headerStringLengths = TabularDataViewer.dataLoggerHeader.map((header) => (header.length + 5) * font.charWidth) + + //---------- + // Controls: + //---------- + + context.onEvent( + ControllerButtonEvent.Pressed, + controller.B.id, + () => { + if (this.guiState == DATA_VIEW_DISPLAY_MODE.FILTERED_DATA_VIEW) { TabularDataViewer.currentRowOffset = 1 + TabularDataViewer.currentRow = 1 + TabularDataViewer.nextDataChunk(); + this.guiState = DATA_VIEW_DISPLAY_MODE.UNFILTERED_DATA_VIEW + } + else { + this.app.popScene(); + this.app.pushScene(new DataViewSelect(this.app)); + } + } + ) - this.headerStringLengths = TabularDataViewer.dataLoggerHeader.map((header) => (header.length + 5) * font.charWidth) - - //---------- - // Controls: - //---------- - - control.onEvent( - ControllerButtonEvent.Pressed, - controller.B.id, - () => { - if (this.guiState == DATA_VIEW_DISPLAY_MODE.FILTERED_DATA_VIEW) { - TabularDataViewer.currentRowOffset = 1 - this.currentRow = 1 - - TabularDataViewer.nextDataChunk(); - this.guiState = DATA_VIEW_DISPLAY_MODE.UNFILTERED_DATA_VIEW - } - else { - this.goBack1PageFn() - } - } - ) - - control.onEvent( - ControllerButtonEvent.Pressed, - controller.A.id, - () => { - if (this.guiState == DATA_VIEW_DISPLAY_MODE.UNFILTERED_DATA_VIEW) { - this.filteredCol = this.currentCol; - this.filteredValue = TabularDataViewer.dataRows[this.currentRow][this.filteredCol] - - TabularDataViewer.currentRowOffset = 0 - this.currentRow = 1 - - this.nextFilteredDataChunk(); - this.updateNeedToScroll(); - this.guiState = DATA_VIEW_DISPLAY_MODE.FILTERED_DATA_VIEW - } - } - ) - - control.onEvent( - ControllerButtonEvent.Pressed, - controller.up.id, - () => { - let tick = true; - control.onEvent( - ControllerButtonEvent.Released, - controller.up.id, - () => tick = false - ) - - - // Control logic: - while (tick) { - if (this.currentRow > 0) - this.currentRow = Math.max(this.currentRow - 1, 1); - - /** - * When scrolling up the cursor might be at the bottom of the screen; so just move the cursor up one. - * Or, the cursor could be on the 2nd row of the screen (index 1 since the first row are headers): - * So don't move the cursor, load a new chunk of data. - */ - if (TabularDataViewer.needToScroll && this.currentRow == 1) { - TabularDataViewer.currentRowOffset = Math.max(TabularDataViewer.currentRowOffset - 1, 1); - - if (this.guiState == DATA_VIEW_DISPLAY_MODE.UNFILTERED_DATA_VIEW) { - TabularDataViewer.currentRowOffset = Math.max(TabularDataViewer.currentRowOffset - 1, 1); - TabularDataViewer.nextDataChunk(); - } - else { - TabularDataViewer.currentRowOffset = Math.max(TabularDataViewer.currentRowOffset - 1, 0); - this.nextFilteredDataChunk() - } - } - if (!controller.up.isPressed()) - break - basic.pause(100) - } - - // Reset binding - control.onEvent(ControllerButtonEvent.Released, controller.up.id, () => { }) - } - ) - - control.onEvent( - ControllerButtonEvent.Pressed, - controller.down.id, - () => { - let tick = true; - control.onEvent( - ControllerButtonEvent.Released, - controller.down.id, - () => tick = false - ); - - // Control logic: - while (tick) { - let rowQty = (TabularDataViewer.dataRows.length < TABULAR_MAX_ROWS) ? TabularDataViewer.dataRows.length - 1 : datalogger.getNumberOfRows(); - - /** - * Same situation as when scrolling UP: - * When scrolling down the cursor might be at the top of the screen; so just move the cursor down one. - * Or, the cursor could be on the last row of the screen: - * So don't move the cursor, load a new chunk of data. - */ - - // Boundary where there are TABULAR_MAX_ROWS - 1 number of rows: - if (datalogger.getNumberOfRows() == 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 (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) - TabularDataViewer.nextDataChunk(); - else - this.nextFilteredDataChunk() - } - } - - else if (this.currentRow < rowQty) { - this.currentRow += 1; - } - - if (!controller.down.isPressed()) - break - basic.pause(100) - } - control.onEvent(ControllerButtonEvent.Released, controller.down.id, () => { }) - } - ) + context.onEvent( + ControllerButtonEvent.Pressed, + controller.A.id, + () => { + if (this.guiState == DATA_VIEW_DISPLAY_MODE.UNFILTERED_DATA_VIEW) { + this.filteredCol = this.currentCol; + this.filteredValue = TabularDataViewer.dataRows[TabularDataViewer.currentRow][this.filteredCol] - control.onEvent( - ControllerButtonEvent.Pressed, - controller.left.id, - () => { - this.currentCol = Math.max(this.currentCol - 1, 0) - } - ) - - control.onEvent( - ControllerButtonEvent.Pressed, - controller.right.id, - () => { - if (this.currentCol + 1 < TabularDataViewer.dataRows[0].length - 1) - this.currentCol += 1 - } - ) + TabularDataViewer.currentRowOffset = 0 + TabularDataViewer.currentRow = 1 + + this.nextFilteredDataChunk(); + this.updateNeedToScroll(); //NOTE:FIX + this.guiState = DATA_VIEW_DISPLAY_MODE.FILTERED_DATA_VIEW + } } + ) + + context.onEvent( + ControllerButtonEvent.Pressed, + controller.up.id, + () => { + let tick = true; + context.onEvent( + ControllerButtonEvent.Released, + controller.up.id, + () => tick = false + ) + + // Control logic: + while (tick) { + if (TabularDataViewer.currentRow > 0) + TabularDataViewer.currentRow = Math.max(TabularDataViewer.currentRow - 1, 1); + + /** + * When scrolling up the cursor might be at the bottom of the screen; so just move the cursor up one. + * Or, the cursor could be on the 2nd row of the screen (index 1 since the first row are headers): + * So don't move the cursor, load a new chunk of data. + */ + if (TabularDataViewer.needToScroll && TabularDataViewer.currentRow == 1) { + TabularDataViewer.currentRowOffset = Math.max(TabularDataViewer.currentRowOffset - 1, 1); + + if (this.guiState == DATA_VIEW_DISPLAY_MODE.UNFILTERED_DATA_VIEW) { + TabularDataViewer.currentRowOffset = Math.max(TabularDataViewer.currentRowOffset - 1, 1); + TabularDataViewer.nextDataChunk(); + } + else { + TabularDataViewer.currentRowOffset = Math.max(TabularDataViewer.currentRowOffset - 1, 0); + this.nextFilteredDataChunk() + } + } + if (!controller.up.isPressed()) + break + basic.pause(100) + } + // Reset binding + context.onEvent(ControllerButtonEvent.Released, controller.up.id, () => { }) + } + ) + + context.onEvent( + ControllerButtonEvent.Pressed, + controller.down.id, + () => { + let tick = true; + context.onEvent( + ControllerButtonEvent.Released, + controller.down.id, + () => tick = false + ); + + // Control logic: + while (tick) { + // let rowQty = (TabularDataViewer.dataRows.length < TABULAR_MAX_ROWS) ? TabularDataViewer.dataRows.length - 1 : TabularDataViewer.numberOfRows; + let rowQty = (TabularDataViewer.datalogCache.length < TABULAR_MAX_ROWS) ? TabularDataViewer.datalogCache.length - 1 : TabularDataViewer.numberOfRows; + // control.dmesg(`d: ${(TabularDataViewer.numberOfRows - TABULAR_MAX_ROWS)} ${(TabularDataViewer.currentRowOffset + TabularDataViewer.currentRow)}`) + + /** + * Same situation as when scrolling UP: + * When scrolling down the cursor might be at the top of the screen; so just move the cursor down one. + * Or, the cursor could be on the last row of the screen: + * So don't move the cursor, load a new chunk of data. + */ + + // Boundary where there are TABULAR_MAX_ROWS - 1 number of rows: + // let beforeMs = control.millis() + + // if (TabularDataViewer.numberOfRows == TABULAR_MAX_ROWS) + // rowQty = TABULAR_MAX_ROWS - 1 + + // control.dmesg(`getNumberOfRows: ${(control.millis() - beforeMs)}`) + + if (this.guiState == DATA_VIEW_DISPLAY_MODE.FILTERED_DATA_VIEW) + rowQty = this.numberOfFilteredRows + if (TabularDataViewer.needToScroll) { + if (TabularDataViewer.currentRow + 1 < TABULAR_MAX_ROWS - 1) + TabularDataViewer.currentRow += 1; + + else if (TabularDataViewer.currentRowOffset <= rowQty - TABULAR_MAX_ROWS) { + TabularDataViewer.currentRowOffset += 1; + + if (this.guiState == DATA_VIEW_DISPLAY_MODE.UNFILTERED_DATA_VIEW) { + if ((TabularDataViewer.currentRowOffset + TABULAR_MAX_ROWS) % MAX_ROWS_TO_CACHE == 0) { + // basic.showNumber(9) + + // -2 since we start +1 from the header, and we want to move forward again. + const nextCacheStart = TabularDataViewer.currentRow - TabularDataViewer.currentRowOffset + TABULAR_MAX_ROWS - 2 + TabularDataViewer.fillCache(nextCacheStart); + } + TabularDataViewer.nextDataChunk(); + } else { + this.nextFilteredDataChunk() + } + } + } - //---------------- - // STATIC METHODS: - //---------------- + else if (TabularDataViewer.currentRow < rowQty) + TabularDataViewer.currentRow += 1; - public static updateDataChunks() { - TabularDataViewer.nextDataChunk() + if (!controller.down.isPressed()) + break + basic.pause(100) + } + context.onEvent(ControllerButtonEvent.Released, controller.down.id, () => { }) } + ) - /** - * 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 + context.onEvent( + ControllerButtonEvent.Pressed, + controller.left.id, + () => { + this.currentCol = Math.max(this.currentCol - 1, 0) + } + ) + + context.onEvent( + ControllerButtonEvent.Pressed, + controller.right.id, + () => { + if (this.currentCol + 1 < TabularDataViewer.dataRows[0].length - 1) + this.currentCol += 1 } + ) + } - //------------------------------- - // Non static Data Chunk methods: - //------------------------------- + //---------------- + // STATIC METHODS: + //---------------- + public static updateDataChunks() { + TabularDataViewer.nextDataChunk() + } - /** - * Fill this.dataRows with up to TABULAR_MAX_ROWS elements of data. - * Filter rows by this.filteredValue. - * Sets the next filteredReadStart by setting this.filteredReadStarts[this.yScrollOffset + 1] - * Mutates: this.dataRows - * Mutates: this.filteredReadStarts[this.yScrollOffset + 1] - */ - private nextFilteredDataChunk() { - let start = this.filteredReadStarts[TabularDataViewer.currentRowOffset]; + public static fillCache(from: number = 0) { + let beforeMs = control.millis() + const rows = datalogger.getRows(from, MAX_ROWS_TO_CACHE).split("\n"); + // control.dmesg(`fcl: ${(rows.length)}`) + // control.dmesg(`readTime: ${(control.millis() - beforeMs)}`) + + beforeMs = control.millis() + let nextDataChunk = [TabularDataViewer.dataLoggerHeader] + for (let i = 0; i < rows.length; i++) { + if (rows[i][0] != "") //NOTE: neccessary check now? + nextDataChunk.push(rows[i].split(",")); + } + TabularDataViewer.datalogCache = nextDataChunk + // control.dmesg(`else: ${(control.millis() - beforeMs)}`) + } - let nextFilteredDataChunk = [TabularDataViewer.dataLoggerHeader] - // if (TabularDataViewer.currentRowOffset == 0) - // TabularDataViewer.dataRows.push(datalogger.getRows(1, 1).split("\n")[0].split(",")); // 0 -> 1; + /** + * 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 + + + // control.dmesg(`b: ${(control.millis())}`) + // TabularDataViewer.needToScroll = TabularDataViewer.numberOfRows > TABULAR_MAX_ROWS + TabularDataViewer.needToScroll = TabularDataViewer.numberOfRows - TABULAR_MAX_ROWS > TabularDataViewer.currentRowOffset + TabularDataViewer.currentRow + // if (!TabularDataViewer.needToScroll) + // basic.showNumber(5) + + // const nextCacheStart = TabularDataViewer.currentRow - TabularDataViewer.currentRowOffset + TABULAR_MAX_ROWS - 2 + // const endIndex = TabularDataViewer.currentRowOffset + TabularDataViewer.currentRow + TABULAR_MAX_ROWS; + + // const numTimesCachFilled = Math.floor((TabularDataViewer.currentRowOffset + TabularDataViewer.currentRow - 2) / MAX_ROWS_TO_CACHE) + // const start = TabularDataViewer.currentRowOffset - (MAX_ROWS_TO_CACHE * (numTimesCachFilled + 0)); + // const start = TabularDataViewer.currentRowOffset % (MAX_ROWS_TO_CACHE + TabularDataViewer.currentRow); + const start = TabularDataViewer.currentRowOffset; + const end = start + TABULAR_MAX_ROWS; + TabularDataViewer.dataRows = TabularDataViewer.datalogCache.slice(start, end) + + // control.dmesg(`s: ${(start)}, ${(end)}`) + // control.dmesg(`a: ${(TabularDataViewer.dataRows.length)}, ${(TabularDataViewer.datalogCache.length)}`) + // control.dmesg(`a: ${(control.millis())}`) + } - 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(","); + //------------------------------- + // Non static Data Chunk methods: + //------------------------------- - // Only add if it's what we're looking for: - if (cols[this.filteredCol] == this.filteredValue) { - nextFilteredDataChunk.push(cols); - // 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 - } - } - } - start += Math.min(TABULAR_MAX_ROWS, datalogger.getNumberOfRows(start)) - } + /** + * Fill this.dataRows with up to TABULAR_MAX_ROWS elements of data. + * Filter rows by this.filteredValue. + * Sets the next filteredReadStart by setting this.filteredReadStarts[this.yScrollOffset + 1] + * Mutates: this.dataRows + * Mutates: this.filteredReadStarts[this.yScrollOffset + 1] + */ + private nextFilteredDataChunk() { + let start = this.filteredReadStarts[TabularDataViewer.currentRowOffset]; - TabularDataViewer.dataRows = nextFilteredDataChunk - } + let nextFilteredDataChunk = [TabularDataViewer.dataLoggerHeader] + // if (TabularDataViewer.currentRowOffset == 0) + // TabularDataViewer.dataRows.push(datalogger.getRows(1, 1).split("\n")[0].split(",")); // 0 -> 1; + while (start < TabularDataViewer.numberOfRows && nextFilteredDataChunk.length < TABULAR_MAX_ROWS) { + const rows = datalogger.getRows(start, TABULAR_MAX_ROWS).split("\n"); // each row as 1 string - /** - * Set this.numberOfFilteredRows & this.needToScroll - * Based upon this.filteredValue - */ - private updateNeedToScroll() { - const chunkSize = Math.min(20, datalogger.getNumberOfRows()); // 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 - } - } - } + // 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 (cols[this.filteredCol] == this.filteredValue) { + nextFilteredDataChunk.push(cols); - // Are there more rows that we could display? - TabularDataViewer.needToScroll = this.numberOfFilteredRows > TABULAR_MAX_ROWS + // 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 + } + } } + start += Math.min(TABULAR_MAX_ROWS, TabularDataViewer.numberOfRows) + } + TabularDataViewer.dataRows = nextFilteredDataChunk + } - /** - * Each header and its corresopnding rows of data have variable lengths, - * The small screen sizes exaggerates these differences, hence variable column sizing. - * @param colBufferSizes this.headerStringLengths spliced by this.xScrollOffset - * @param rowBufferSize remains constant - */ - drawGridOfVariableColSize(colBufferSizes: number[], rowBufferSize: number) { - let cumulativeColOffset = 0 - // Skip the first column: Time (Seconds): - for (let col = 0; col < colBufferSizes.length; col++) { - if (cumulativeColOffset + colBufferSizes[col] > Screen.WIDTH) { - break - } + /** + * Set this.numberOfFilteredRows & this.needToScroll + * Based upon this.filteredValue + */ + private updateNeedToScroll() { + const chunkSize = Math.min(20, TabularDataViewer.numberOfRows); // 20 as limit for search + + this.numberOfFilteredRows = 0 + for (let chunk = 0; chunk < TabularDataViewer.numberOfRows; 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 + } + } + } - // The last column should use all remaining space, if it is lesser than that remaining space: - if (col == colBufferSizes.length - 1 || cumulativeColOffset + colBufferSizes[col] + colBufferSizes[col + 1] > Screen.WIDTH) - cumulativeColOffset += Screen.WIDTH - cumulativeColOffset; - else - cumulativeColOffset += colBufferSizes[col]; - - if (cumulativeColOffset <= Screen.WIDTH) { - Screen.drawLine( - Screen.LEFT_EDGE + cumulativeColOffset, - Screen.TOP_EDGE, - Screen.LEFT_EDGE + cumulativeColOffset, - Screen.HEIGHT, - 15 - ) - } - } + // Are there more rows that we could display? + TabularDataViewer.needToScroll = this.numberOfFilteredRows > TABULAR_MAX_ROWS + } - for (let rowOffset = 0; rowOffset <= Screen.HEIGHT; rowOffset += rowBufferSize) { - Screen.drawLine( - Screen.LEFT_EDGE, - Screen.TOP_EDGE + rowOffset, - Screen.WIDTH, - Screen.TOP_EDGE + rowOffset, - 15 - ) - } - // Draw selected box: - Screen.drawRect( - Screen.LEFT_EDGE, - Screen.TOP_EDGE + (this.currentRow * rowBufferSize), - colBufferSizes[0], - rowBufferSize, - 6 - ) + /** + * Each header and its corresopnding rows of data have variable lengths, + * The small screen sizes exaggerates these differences, hence variable column sizing. + * @param colBufferSizes this.headerStringLengths spliced by this.xScrollOffset + * @param rowBufferSize remains constant + */ + drawGridOfVariableColSize(colBufferSizes: number[], rowBufferSize: number) { + let cumulativeColOffset = 0 + + // Skip the first column: Time (Seconds): + for (let col = 0; col < colBufferSizes.length; col++) { + if (cumulativeColOffset + colBufferSizes[col] > Screen.WIDTH) { + break } - draw() { - Screen.fillRect( - Screen.LEFT_EDGE, - Screen.TOP_EDGE, - Screen.WIDTH, - Screen.HEIGHT, - 0xC - ) - - if (TabularDataViewer.updateDataRowsOnNextFrame) - TabularDataViewer.nextDataChunk() - - - // Could be optimised by calculating the Col line boundaries once & re-using them, instead of each frame: - const tabularRowBufferSize = Screen.HEIGHT / Math.min(TabularDataViewer.dataRows.length, TABULAR_MAX_ROWS); - this.drawGridOfVariableColSize(this.headerStringLengths.slice(this.currentCol), tabularRowBufferSize) - - // Write the data into the grid: - for (let row = 0; row < Math.min(TabularDataViewer.dataRows.length, TABULAR_MAX_ROWS); row++) { - let cumulativeColOffset = 0; - - // Go through each column: - for (let col = 0; col < TabularDataViewer.dataRows[0].length - this.currentCol; col++) { - const colID: number = col + this.currentCol; - - let columnValue: string = TabularDataViewer.dataRows[row][colID]; - - // Bounds check: - if (cumulativeColOffset + this.headerStringLengths[colID] > Screen.WIDTH) - break; - - // In this.drawGridOfVariableSize: If the column after this one would not fit grant this one the remanining space - // This will align the text to the center of this column space - if (colID == TabularDataViewer.dataRows[0].length - 1 || cumulativeColOffset + this.headerStringLengths[colID] + this.headerStringLengths[colID + 1] > Screen.WIDTH) - cumulativeColOffset += ((Screen.WIDTH - cumulativeColOffset) >> 1) - (this.headerStringLengths[colID] >> 1); - - // Write the columnValue in the centre of each grid box: - Screen.print( - columnValue, - Screen.LEFT_EDGE + cumulativeColOffset + (this.headerStringLengths[colID] >> 1) - ((font.charWidth * columnValue.length) >> 1), - Screen.TOP_EDGE + (row * tabularRowBufferSize) + (tabularRowBufferSize >> 1) - 4, - // 0xb, - 1, - bitmaps.font8 - ) - - cumulativeColOffset += this.headerStringLengths[colID] - } - } + // The last column should use all remaining space, if it is lesser than that remaining space: + if (col == colBufferSizes.length - 1 || cumulativeColOffset + colBufferSizes[col] + colBufferSizes[col + 1] > Screen.WIDTH) + cumulativeColOffset += Screen.WIDTH - cumulativeColOffset; + else + cumulativeColOffset += colBufferSizes[col]; + + if (cumulativeColOffset <= Screen.WIDTH) { + Screen.drawLine( + Screen.LEFT_EDGE + cumulativeColOffset, + Screen.TOP_EDGE, + Screen.LEFT_EDGE + cumulativeColOffset, + Screen.HEIGHT, + 15 + ) + } + } + + for (let rowOffset = 0; rowOffset <= Screen.HEIGHT; rowOffset += rowBufferSize) { + Screen.drawLine( + Screen.LEFT_EDGE, + Screen.TOP_EDGE + rowOffset, + Screen.WIDTH, + Screen.TOP_EDGE + rowOffset, + 15 + ) + } + + // Draw selected box: + Screen.drawRect( + Screen.LEFT_EDGE, + Screen.TOP_EDGE + (TabularDataViewer.currentRow * rowBufferSize), + colBufferSizes[0], + rowBufferSize, + 6 + ) + } - super.draw() + draw() { + Screen.fillRect( + Screen.LEFT_EDGE, + Screen.TOP_EDGE, + Screen.WIDTH, + Screen.HEIGHT, + 0xC + ) + + if (TabularDataViewer.updateDataRowsOnNextFrame) + TabularDataViewer.nextDataChunk() + + + // Could be optimised by calculating the Col line boundaries once & re-using them, instead of each frame: + const tabularRowBufferSize = Screen.HEIGHT / Math.min(TabularDataViewer.dataRows.length, TABULAR_MAX_ROWS); + this.drawGridOfVariableColSize(this.headerStringLengths.slice(this.currentCol), tabularRowBufferSize) + + // Write the data into the grid: + for (let row = 0; row < Math.min(TabularDataViewer.dataRows.length, TABULAR_MAX_ROWS); row++) { + let cumulativeColOffset = 0; + + // Go through each column: + for (let col = 0; col < TabularDataViewer.dataRows[0].length - this.currentCol; col++) { + const colID: number = col + this.currentCol; + + let columnValue: string = TabularDataViewer.dataRows[row][colID]; + + // Bounds check: + if (cumulativeColOffset + this.headerStringLengths[colID] > Screen.WIDTH) + break; + + // In this.drawGridOfVariableSize: If the column after this one would not fit grant this one the remanining space + // This will align the text to the center of this column space + if (colID == TabularDataViewer.dataRows[0].length - 1 || cumulativeColOffset + this.headerStringLengths[colID] + this.headerStringLengths[colID + 1] > Screen.WIDTH) + cumulativeColOffset += ((Screen.WIDTH - cumulativeColOffset) >> 1) - (this.headerStringLengths[colID] >> 1); + + // Write the columnValue in the centre of each grid box: + Screen.print( + columnValue, + Screen.LEFT_EDGE + cumulativeColOffset + (this.headerStringLengths[colID] >> 1) - ((font.charWidth * columnValue.length) >> 1), + Screen.TOP_EDGE + (row * tabularRowBufferSize) + (tabularRowBufferSize >> 1) - 4, + // 0xb, + 1, + bitmaps.font8 + ) + + cumulativeColOffset += this.headerStringLengths[colID] } + } + + super.draw() } + } } - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From ecf5a602a8a465ea7c07e75eaa845f095dc34ecf Mon Sep 17 00:00:00 2001 From: KierPalin <45743174+KierPalin@users.noreply.github.com> Date: Mon, 24 Nov 2025 02:28:42 +0800 Subject: [PATCH 6/9] Data recording cancelling bug --- dataRecorder.ts | 1 + sensors.ts | 1341 ++++++++++++++++++++++++----------------------- 2 files changed, 682 insertions(+), 660 deletions(-) diff --git a/dataRecorder.ts b/dataRecorder.ts index 847c4f3..cf492cf 100644 --- a/dataRecorder.ts +++ b/dataRecorder.ts @@ -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)) } diff --git a/sensors.ts b/sensors.ts index 7dc2eee..d61b696 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: 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() - }); - } + /** 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 + ); } + } } + } } From 494924d7864b060d57cd7a53382df33fe19fa4ea Mon Sep 17 00:00:00 2001 From: KierPalin <45743174+KierPalin@users.noreply.github.com> Date: Mon, 24 Nov 2025 22:52:22 +0800 Subject: [PATCH 7/9] tabularDataViewer rework --- tabularDataViewer.ts | 225 +++++++++++++++++++++++++------------------ 1 file changed, 129 insertions(+), 96 deletions(-) diff --git a/tabularDataViewer.ts b/tabularDataViewer.ts index df789a3..ac44477 100644 --- a/tabularDataViewer.ts +++ b/tabularDataViewer.ts @@ -13,7 +13,7 @@ namespace microdata { //** I think this can be far higher. Max row size is easy to calculate. */ - const MAX_ROWS_TO_CACHE = 256; + const MAX_ROWS_TO_CACHE = 20; /** * Locally used to control flow upon button presses: A, B, UP, DOWN @@ -51,6 +51,8 @@ namespace microdata { */ private static dataRows: string[][]; + private static datalogCache: string[][]; + //--------- // FOR GUI: @@ -88,7 +90,7 @@ namespace microdata { * * Used to determine which columns to draw. */ - private currentCol: number + private static currentCol: number /** * Used as index into .filteredReadStarts by: @@ -99,7 +101,7 @@ namespace microdata { * * Modified when pressing UP or DOWN */ - private static currentRowOffset: number + private static dataRowsIndex: number /** * This is unique per sensor, it is calculated once upon pressing A. @@ -123,30 +125,25 @@ namespace microdata { */ private filteredReadStarts: number[] - /** TabularDataViewer may be entered from the Command Mode, DataViewSelect or View Data (Home screen 4th button) */ - private goBack1PageFn: () => void - - private static datalogCache: string[][]; - private static numberOfRows: number; - constructor(app: AppInterface, goBack1PageFn: () => void) { + private backBtnFn: () => void; + + constructor(app: AppInterface, backBtnFn: () => void) { super(app, "recordedDataViewer") this.guiState = DATA_VIEW_DISPLAY_MODE.UNFILTERED_DATA_VIEW - this.currentCol = 0 - this.numberOfFilteredRows = 0 this.filteredValue = "" this.filteredReadStarts = [0] this.filteredCol = 0 - this.goBack1PageFn = goBack1PageFn + this.backBtnFn = backBtnFn } - /* override */ startup() { + /* override */ startup() { super.startup() TabularDataViewer.numberOfRows = datalogger.getNumberOfRows(); @@ -154,11 +151,14 @@ namespace microdata { // Start on the 2nd row; since the first row is for headers: TabularDataViewer.currentRow = 1 - TabularDataViewer.currentRowOffset = 0 - TabularDataViewer.dataLoggerHeader = datalogger.getRows(TabularDataViewer.currentRowOffset, 1).split("\n")[0].split(","); - TabularDataViewer.currentRowOffset = 1 // NOTE: ??? + TabularDataViewer.currentCol = 0 + + TabularDataViewer.dataRowsIndex = 0 + TabularDataViewer.dataLoggerHeader = datalogger.getRows(TabularDataViewer.dataRowsIndex, 1).split("\n")[0].split(","); + TabularDataViewer.fillCache(); - TabularDataViewer.nextDataChunk(); + TabularDataViewer.dataRowsIndex = 1 // Don't start the user on the HEADER row, bump to the first row of actual data. + TabularDataViewer.updateDataRows(); this.headerStringLengths = TabularDataViewer.dataLoggerHeader.map((header) => (header.length + 5) * font.charWidth) @@ -166,33 +166,32 @@ namespace microdata { // Controls: //---------- - context.onEvent( + control.onEvent( ControllerButtonEvent.Pressed, controller.B.id, () => { if (this.guiState == DATA_VIEW_DISPLAY_MODE.FILTERED_DATA_VIEW) { - TabularDataViewer.currentRowOffset = 1 + TabularDataViewer.dataRowsIndex = 1 TabularDataViewer.currentRow = 1 - TabularDataViewer.nextDataChunk(); + TabularDataViewer.updateDataRows(); this.guiState = DATA_VIEW_DISPLAY_MODE.UNFILTERED_DATA_VIEW } else { - this.app.popScene(); - this.app.pushScene(new DataViewSelect(this.app)); + this.backBtnFn(); } } ) - context.onEvent( + control.onEvent( ControllerButtonEvent.Pressed, controller.A.id, () => { if (this.guiState == DATA_VIEW_DISPLAY_MODE.UNFILTERED_DATA_VIEW) { - this.filteredCol = this.currentCol; + this.filteredCol = TabularDataViewer.currentCol; this.filteredValue = TabularDataViewer.dataRows[TabularDataViewer.currentRow][this.filteredCol] - TabularDataViewer.currentRowOffset = 0 + TabularDataViewer.dataRowsIndex = 0 TabularDataViewer.currentRow = 1 this.nextFilteredDataChunk(); @@ -202,12 +201,12 @@ namespace microdata { } ) - context.onEvent( + control.onEvent( ControllerButtonEvent.Pressed, controller.up.id, () => { let tick = true; - context.onEvent( + control.onEvent( ControllerButtonEvent.Released, controller.up.id, () => tick = false @@ -224,14 +223,14 @@ namespace microdata { * So don't move the cursor, load a new chunk of data. */ if (TabularDataViewer.needToScroll && TabularDataViewer.currentRow == 1) { - TabularDataViewer.currentRowOffset = Math.max(TabularDataViewer.currentRowOffset - 1, 1); + TabularDataViewer.dataRowsIndex = Math.max(TabularDataViewer.dataRowsIndex - 1, 1); if (this.guiState == DATA_VIEW_DISPLAY_MODE.UNFILTERED_DATA_VIEW) { - TabularDataViewer.currentRowOffset = Math.max(TabularDataViewer.currentRowOffset - 1, 1); - TabularDataViewer.nextDataChunk(); + TabularDataViewer.dataRowsIndex = Math.max(TabularDataViewer.dataRowsIndex - 1, 1); + TabularDataViewer.updateDataRows(); } else { - TabularDataViewer.currentRowOffset = Math.max(TabularDataViewer.currentRowOffset - 1, 0); + TabularDataViewer.dataRowsIndex = Math.max(TabularDataViewer.dataRowsIndex - 1, 0); this.nextFilteredDataChunk() } } @@ -241,16 +240,20 @@ namespace microdata { } // Reset binding - context.onEvent(ControllerButtonEvent.Released, controller.up.id, () => { }) + control.onEvent(ControllerButtonEvent.Released, controller.up.id, () => { }) } ) - context.onEvent( + // LOAD 200 rows to cacheLog + // Copy to dataRows. + // cacheLog pointer, currentRow pointer. + + control.onEvent( ControllerButtonEvent.Pressed, controller.down.id, () => { let tick = true; - context.onEvent( + control.onEvent( ControllerButtonEvent.Released, controller.down.id, () => tick = false @@ -259,14 +262,27 @@ namespace microdata { // Control logic: while (tick) { // let rowQty = (TabularDataViewer.dataRows.length < TABULAR_MAX_ROWS) ? TabularDataViewer.dataRows.length - 1 : TabularDataViewer.numberOfRows; - let rowQty = (TabularDataViewer.datalogCache.length < TABULAR_MAX_ROWS) ? TabularDataViewer.datalogCache.length - 1 : TabularDataViewer.numberOfRows; - // control.dmesg(`d: ${(TabularDataViewer.numberOfRows - TABULAR_MAX_ROWS)} ${(TabularDataViewer.currentRowOffset + TabularDataViewer.currentRow)}`) + control.dmesg(`d: ${(TabularDataViewer.numberOfRows - TABULAR_MAX_ROWS)} ${(TabularDataViewer.dataRowsIndex + TabularDataViewer.currentRow)}`) + + if (this.guiState == DATA_VIEW_DISPLAY_MODE.UNFILTERED_DATA_VIEW) { + if (TabularDataViewer.currentRow < TABULAR_MAX_ROWS - 1) { + TabularDataViewer.currentRow++; + } else if (TabularDataViewer.needToScroll) { + TabularDataViewer.dataRowsIndex++; + + // if (TabularDataViewer.dataRowsIndex % MAX_ROWS_TO_CACHE == 0) { + if ((TabularDataViewer.dataRowsIndex + TABULAR_MAX_ROWS - 1) % MAX_ROWS_TO_CACHE == 0) { + TabularDataViewer.fillCache(); + } + TabularDataViewer.updateDataRows(); + } + } /** - * Same situation as when scrolling UP: - * When scrolling down the cursor might be at the top of the screen; so just move the cursor down one. - * Or, the cursor could be on the last row of the screen: - * So don't move the cursor, load a new chunk of data. + * Same situation as when scrolling UP: + * When scrolling down the cursor might be at the top of the screen; so just move the cursor down one. + * Or, the cursor could be on the last row of the screen: + * So don't move the cursor, load a new chunk of data. */ // Boundary where there are TABULAR_MAX_ROWS - 1 number of rows: @@ -277,55 +293,55 @@ namespace microdata { // control.dmesg(`getNumberOfRows: ${(control.millis() - beforeMs)}`) - if (this.guiState == DATA_VIEW_DISPLAY_MODE.FILTERED_DATA_VIEW) - rowQty = this.numberOfFilteredRows - if (TabularDataViewer.needToScroll) { - if (TabularDataViewer.currentRow + 1 < TABULAR_MAX_ROWS - 1) - TabularDataViewer.currentRow += 1; - - else if (TabularDataViewer.currentRowOffset <= rowQty - TABULAR_MAX_ROWS) { - TabularDataViewer.currentRowOffset += 1; - - if (this.guiState == DATA_VIEW_DISPLAY_MODE.UNFILTERED_DATA_VIEW) { - if ((TabularDataViewer.currentRowOffset + TABULAR_MAX_ROWS) % MAX_ROWS_TO_CACHE == 0) { - // basic.showNumber(9) - - // -2 since we start +1 from the header, and we want to move forward again. - const nextCacheStart = TabularDataViewer.currentRow - TabularDataViewer.currentRowOffset + TABULAR_MAX_ROWS - 2 - TabularDataViewer.fillCache(nextCacheStart); - } - TabularDataViewer.nextDataChunk(); - } else { - this.nextFilteredDataChunk() - } - } - } - - else if (TabularDataViewer.currentRow < rowQty) - TabularDataViewer.currentRow += 1; + // if (this.guiState == DATA_VIEW_DISPLAY_MODE.FILTERED_DATA_VIEW) + // rowQty = this.numberOfFilteredRows + // if (TabularDataViewer.needToScroll) { + // if (TabularDataViewer.currentRow + 1 < TABULAR_MAX_ROWS - 1) + // TabularDataViewer.currentRow += 1; + + // else if (TabularDataViewer.currentRowOffset <= rowQty - TABULAR_MAX_ROWS) { + // TabularDataViewer.currentRowOffset += 1; + + // if (this.guiState == DATA_VIEW_DISPLAY_MODE.UNFILTERED_DATA_VIEW) { + // if ((TabularDataViewer.currentRowOffset + TABULAR_MAX_ROWS) % MAX_ROWS_TO_CACHE == 0) { + // basic.showNumber(9) + + // // -2 since we start +1 from the header, and we want to move forward again. + // const nextCacheStart = TabularDataViewer.currentRow - TabularDataViewer.currentRowOffset + TABULAR_MAX_ROWS - 2 + // TabularDataViewer.fillCache(nextCacheStart); + // } + // TabularDataViewer.nextDataChunk(); + // } else { + // this.nextFilteredDataChunk() + // } + // } + // } + + // else if (TabularDataViewer.currentRow < rowQty) + // TabularDataViewer.currentRow += 1; if (!controller.down.isPressed()) break basic.pause(100) } - context.onEvent(ControllerButtonEvent.Released, controller.down.id, () => { }) + control.onEvent(ControllerButtonEvent.Released, controller.down.id, () => { }) } ) - context.onEvent( + control.onEvent( ControllerButtonEvent.Pressed, controller.left.id, () => { - this.currentCol = Math.max(this.currentCol - 1, 0) + TabularDataViewer.currentCol = Math.max(TabularDataViewer.currentCol - 1, 0) } ) - context.onEvent( + control.onEvent( ControllerButtonEvent.Pressed, controller.right.id, () => { - if (this.currentCol + 1 < TabularDataViewer.dataRows[0].length - 1) - this.currentCol += 1 + if (TabularDataViewer.currentCol + 1 < TabularDataViewer.dataRows[0].length - 1) + TabularDataViewer.currentCol += 1 } ) } @@ -335,17 +351,20 @@ namespace microdata { // STATIC METHODS: //---------------- - public static updateDataChunks() { - TabularDataViewer.nextDataChunk() - } + // public static updateDataChunks() { + // TabularDataViewer.nextDataChunk() + // } - public static fillCache(from: number = 0) { - let beforeMs = control.millis() + public static fillCache() { + // const from = (TabularDataViewer.dataRowsIndex < 5) ? TabularDataViewer.dataRowsIndex : TabularDataViewer.dataRowsIndex - TabularDataViewer.currentRow; + const from = TabularDataViewer.dataRowsIndex; + + // let beforeMs = control.millis() const rows = datalogger.getRows(from, MAX_ROWS_TO_CACHE).split("\n"); // control.dmesg(`fcl: ${(rows.length)}`) // control.dmesg(`readTime: ${(control.millis() - beforeMs)}`) - beforeMs = control.millis() + // beforeMs = control.millis() let nextDataChunk = [TabularDataViewer.dataLoggerHeader] for (let i = 0; i < rows.length; i++) { if (rows[i][0] != "") //NOTE: neccessary check now? @@ -360,7 +379,7 @@ namespace microdata { * Invoked when this.tabularYScrollOffset reaches its screen boundaries. * Mutates: this.dataRows */ - private static nextDataChunk() { + private static updateDataRows() { // const rows = datalogger.getRows(TabularDataViewer.currentRowOffset, TABULAR_MAX_ROWS).split("\n"); // TabularDataViewer.needToScroll = datalogger.getNumberOfRows() > TABULAR_MAX_ROWS // @@ -374,7 +393,7 @@ namespace microdata { // control.dmesg(`b: ${(control.millis())}`) // TabularDataViewer.needToScroll = TabularDataViewer.numberOfRows > TABULAR_MAX_ROWS - TabularDataViewer.needToScroll = TabularDataViewer.numberOfRows - TABULAR_MAX_ROWS > TabularDataViewer.currentRowOffset + TabularDataViewer.currentRow + // TabularDataViewer.needToScroll = TabularDataViewer.numberOfRows - TABULAR_MAX_ROWS > TabularDataViewer.dataRowsIndex + TabularDataViewer.currentRow // YES // if (!TabularDataViewer.needToScroll) // basic.showNumber(5) @@ -384,13 +403,29 @@ namespace microdata { // const numTimesCachFilled = Math.floor((TabularDataViewer.currentRowOffset + TabularDataViewer.currentRow - 2) / MAX_ROWS_TO_CACHE) // const start = TabularDataViewer.currentRowOffset - (MAX_ROWS_TO_CACHE * (numTimesCachFilled + 0)); // const start = TabularDataViewer.currentRowOffset % (MAX_ROWS_TO_CACHE + TabularDataViewer.currentRow); - const start = TabularDataViewer.currentRowOffset; - const end = start + TABULAR_MAX_ROWS; - TabularDataViewer.dataRows = TabularDataViewer.datalogCache.slice(start, end) + + + // YES to 3: + // const start = TabularDataViewer.dataRowsIndex; + // const end = start + TABULAR_MAX_ROWS; + // TabularDataViewer.dataRows = TabularDataViewer.datalogCache.slice(start, end) // control.dmesg(`s: ${(start)}, ${(end)}`) // control.dmesg(`a: ${(TabularDataViewer.dataRows.length)}, ${(TabularDataViewer.datalogCache.length)}`) // control.dmesg(`a: ${(control.millis())}`) + + + // control.dmesg(`a: ${(TabularDataViewer.needToScroll)} ${(TabularDataViewer.dataRows.length)} ${(TabularDataViewer.datalogCache.length)}`) + // + // Balance this eq: + // control.dmesg(`d: ${(TabularDataViewer.numberOfRows)} ${(TabularDataViewer.dataRowsIndex - TABULAR_MAX_ROWS)}`) + control.dmesg(`u: ${(TabularDataViewer.numberOfRows)} ${(TabularDataViewer.dataRowsIndex - TABULAR_MAX_ROWS)} ${(TabularDataViewer.numberOfRows)}`) + TabularDataViewer.needToScroll = TabularDataViewer.dataRowsIndex + TabularDataViewer.currentRow < TabularDataViewer.numberOfRows // YES + // TabularDataViewer.needToScroll = TabularDataViewer.dataRowsIndex + TabularDataViewer.currentRow < MAX_ROWS_TO_CACHE // YES + + const start = TabularDataViewer.dataRowsIndex % MAX_ROWS_TO_CACHE; + const end = start + TABULAR_MAX_ROWS; + TabularDataViewer.dataRows = TabularDataViewer.datalogCache.slice(start, end) } @@ -407,7 +442,7 @@ namespace microdata { * Mutates: this.filteredReadStarts[this.yScrollOffset + 1] */ private nextFilteredDataChunk() { - let start = this.filteredReadStarts[TabularDataViewer.currentRowOffset]; + let start = this.filteredReadStarts[TabularDataViewer.dataRowsIndex]; let nextFilteredDataChunk = [TabularDataViewer.dataLoggerHeader] // if (TabularDataViewer.currentRowOffset == 0) @@ -426,8 +461,8 @@ namespace microdata { // 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 + if (nextFilteredDataChunk.length == ((TabularDataViewer.dataRowsIndex == 0) ? 3 : 2)) { + this.filteredReadStarts[TabularDataViewer.dataRowsIndex + 1] = start + i } } } @@ -487,7 +522,7 @@ namespace microdata { Screen.TOP_EDGE, Screen.LEFT_EDGE + cumulativeColOffset, Screen.HEIGHT, - 15 + 15 // black ) } } @@ -498,7 +533,7 @@ namespace microdata { Screen.TOP_EDGE + rowOffset, Screen.WIDTH, Screen.TOP_EDGE + rowOffset, - 15 + 15 // black ) } @@ -508,7 +543,7 @@ namespace microdata { Screen.TOP_EDGE + (TabularDataViewer.currentRow * rowBufferSize), colBufferSizes[0], rowBufferSize, - 6 + 6 // blue ) } @@ -518,24 +553,23 @@ namespace microdata { Screen.TOP_EDGE, Screen.WIDTH, Screen.HEIGHT, - 0xC + 0xC // Purple ) if (TabularDataViewer.updateDataRowsOnNextFrame) - TabularDataViewer.nextDataChunk() - + TabularDataViewer.updateDataRows() // Could be optimised by calculating the Col line boundaries once & re-using them, instead of each frame: const tabularRowBufferSize = Screen.HEIGHT / Math.min(TabularDataViewer.dataRows.length, TABULAR_MAX_ROWS); - this.drawGridOfVariableColSize(this.headerStringLengths.slice(this.currentCol), tabularRowBufferSize) + this.drawGridOfVariableColSize(this.headerStringLengths.slice(TabularDataViewer.currentCol), tabularRowBufferSize) // Write the data into the grid: for (let row = 0; row < Math.min(TabularDataViewer.dataRows.length, TABULAR_MAX_ROWS); row++) { let cumulativeColOffset = 0; // Go through each column: - for (let col = 0; col < TabularDataViewer.dataRows[0].length - this.currentCol; col++) { - const colID: number = col + this.currentCol; + for (let col = 0; col < TabularDataViewer.dataRows[0].length - TabularDataViewer.currentCol; col++) { + const colID: number = col + TabularDataViewer.currentCol; let columnValue: string = TabularDataViewer.dataRows[row][colID]; @@ -553,7 +587,6 @@ namespace microdata { columnValue, Screen.LEFT_EDGE + cumulativeColOffset + (this.headerStringLengths[colID] >> 1) - ((font.charWidth * columnValue.length) >> 1), Screen.TOP_EDGE + (row * tabularRowBufferSize) + (tabularRowBufferSize >> 1) - 4, - // 0xb, 1, bitmaps.font8 ) From 88b92d83ff537872cdc1697c7da49309de591726 Mon Sep 17 00:00:00 2001 From: KierPalin <45743174+KierPalin@users.noreply.github.com> Date: Tue, 25 Nov 2025 06:33:41 +0800 Subject: [PATCH 8/9] Good :) --- app.ts | 5 +- dataViewSelect.ts | 111 ++++++------- generateGraph.ts | 2 +- headlessMode.ts | 41 ++++- home.ts | 11 +- liveDataViewer.ts | 10 +- loggingConfig.ts | 14 +- sensorSelect.ts | 2 +- sensors.ts | 2 +- tabularDataViewer.ts | 365 ++++++++++++++++++------------------------- 10 files changed, 273 insertions(+), 290 deletions(-) diff --git a/app.ts b/app.ts index b610e81..7c33697 100644 --- a/app.ts +++ b/app.ts @@ -46,11 +46,10 @@ namespace microdata { this.sceneManager = new SceneManager() datalogger.includeTimestamp(FlashLogTimeStampFormat.None) - // datalogger.deleteLog(datalogger.DeleteType.Fast) - // for (let i = 0; i < 10; i++) { + // for (let i = 0; i < 400; i++) { // datalogger.log( - // datalogger.createCV("Sensor", "test"), + // datalogger.createCV("Sensor", "testtest"), // datalogger.createCV("Time (ms)", i * 1000), // datalogger.createCV("Reading", (i * 43) % 5000), // datalogger.createCV("Event", "N/A") diff --git a/dataViewSelect.ts b/dataViewSelect.ts index 98207dd..27f5228 100644 --- a/dataViewSelect.ts +++ b/dataViewSelect.ts @@ -9,7 +9,7 @@ namespace microdata { * Choose between: * Resetting Datalogger * A tabular view of the recorded data - * A graph of the recorded data + * Jacdac light experiment */ export class DataViewSelect extends CursorScene { private dataloggerEmpty: boolean @@ -18,7 +18,7 @@ namespace microdata { super(app); } - /* override */ startup() { + /* override */ startup() { super.startup() basic.pause(50); @@ -27,72 +27,76 @@ namespace microdata { const y = Screen.HEIGHT * 0.234 // y = 30 on an Arcade Shield of height 128 pixels - this.navigator.setBtns([[ - new Button({ + let btns: Button[][] = [[]]; + + if (this.dataloggerEmpty) { + btns[0].push(new Button({ parent: null, style: ButtonStyles.Transparent, - icon: "largeDisk", - ariaId: "View Data", + icon: "edit_program", + ariaId: "Log 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)) })) + this.app.pushScene(new SensorSelect(this.app, MicroDataSceneEnum.RecordingConfigSelect)) }, - }), - - new Button({ + })) + } else { + btns[0].push(new Button({ parent: null, style: ButtonStyles.Transparent, - icon: "linear_graph_1", - ariaId: "View Graph", - x: 0, + icon: "largeDisk", + ariaId: "View Data", + x: -50, y, onClick: () => { this.app.popScene() - this.app.pushScene(new GraphGenerator(this.app)) - }, - }), + this.app.pushScene(new TabularDataViewer(this.app, () => { this.app.popScene(); this.app.pushScene(new DataViewSelect(this.app)) })) + } + })) + } - 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)) - } - ) - }, - }) - ]]) + 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) //--------- // Control: //--------- - // No data in log (first row are headers) - if (this.dataloggerEmpty) { - context.onEvent( - ControllerButtonEvent.Pressed, - controller.A.id, - () => { - this.app.popScene() - this.app.pushScene(new SensorSelect(this.app, MicroDataSceneEnum.RecordingConfigSelect)) - } - ) - } - context.onEvent( ControllerButtonEvent.Pressed, controller.B.id, @@ -114,15 +118,14 @@ namespace microdata { if (this.dataloggerEmpty) { screen().printCenter("No data has been recorded", 5) - screen().printCenter("Press A to Record some!", Screen.HALF_HEIGHT) - return; + screen().printCenter("Log Data to collect some!", Screen.HALF_HEIGHT - 30) } else { - screen().printCenter("Recorded Data Options", 5) - this.navigator.drawComponents(); + screen().printCenter("View Data, Experiment or Clear Data", 5) } + this.navigator.drawComponents(); super.draw() } } diff --git a/generateGraph.ts b/generateGraph.ts index 3354664..0fe222f 100644 --- a/generateGraph.ts +++ b/generateGraph.ts @@ -675,4 +675,4 @@ namespace microdata { ) } } -} \ No newline at end of file +} diff --git a/headlessMode.ts b/headlessMode.ts index 4c6f5ee..ea65bef 100644 --- a/headlessMode.ts +++ b/headlessMode.ts @@ -26,6 +26,13 @@ namespace microdata { 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. */ @@ -55,6 +62,8 @@ namespace microdata { constructor() { this.uiMode = UI_MODE.SENSOR_SELECTION; this.uiSensorSelectState = UI_SENSOR_SELECT_STATE.ACCELERATION; + datalogger.deleteLog(datalogger.DeleteType.Fast) + // A Button input.onButtonPressed(1, () => { @@ -187,16 +196,30 @@ namespace microdata { `) // control.inBackground(() => { - const WAIT_TIME_MS = 50; + 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") - ); + // 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; @@ -270,5 +293,9 @@ namespace microdata { return [] } } + + private uiSelectionToSensorEventThresholds(): number { + return sensorEventThresholds[this.uiSensorSelectState]; + } } } diff --git a/home.ts b/home.ts index 90cc28e..93c4d75 100644 --- a/home.ts +++ b/home.ts @@ -48,16 +48,13 @@ namespace microdata { new Button({ parent: null, style: ButtonStyles.Transparent, - // icon: "radio_set_group", - // ariaId: "Command Mode", - icon: "largeSettingsGear", - ariaId: "Experiments", + icon: "radio_set_group", + ariaId: "Command Mode", x: 20, y, onClick: () => { this.app.popScene() - // this.app.pushScene(new DistributedLoggingScreen(this.app)) - this.app.pushScene(new JacdacLightExperiment(this.app)) + this.app.pushScene(new DistributedLoggingScreen(this.app)) }, }), @@ -65,7 +62,7 @@ namespace microdata { parent: null, style: ButtonStyles.Transparent, icon: "largeDisk", - ariaId: "View Data", + ariaId: "View Data & Settings", x: 58, y, onClick: () => { diff --git a/liveDataViewer.ts b/liveDataViewer.ts index 962ab1e..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); @@ -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; diff --git a/loggingConfig.ts b/loggingConfig.ts index 63f01fb..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 AppInterface = user_interface_base.AppInterface import font = user_interface_base.font + /** * Generated at recordingConfigSelection * Passed to and owned by a sensor @@ -324,9 +325,18 @@ namespace microdata { 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: { diff --git a/sensorSelect.ts b/sensorSelect.ts index de7a814..93deea7 100644 --- a/sensorSelect.ts +++ b/sensorSelect.ts @@ -406,4 +406,4 @@ namespace microdata { } } } -} \ No newline at end of file +} diff --git a/sensors.ts b/sensors.ts index d61b696..34d89a9 100644 --- a/sensors.ts +++ b/sensors.ts @@ -98,7 +98,7 @@ namespace microdata { // 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.B.id, () => { }) context.onEvent(ControllerButtonEvent.Pressed, controller.up.id, () => { }) context.onEvent(ControllerButtonEvent.Pressed, controller.down.id, () => { }) context.onEvent(ControllerButtonEvent.Pressed, controller.left.id, () => { }) diff --git a/tabularDataViewer.ts b/tabularDataViewer.ts index ac44477..110a881 100644 --- a/tabularDataViewer.ts +++ b/tabularDataViewer.ts @@ -11,9 +11,7 @@ namespace microdata { */ const TABULAR_MAX_ROWS = 8 - - //** I think this can be far higher. Max row size is easy to calculate. */ - const MAX_ROWS_TO_CACHE = 20; + const LOAD_QTY = 250 /** * Locally used to control flow upon button presses: A, B, UP, DOWN @@ -51,7 +49,9 @@ namespace microdata { */ private static dataRows: string[][]; - private static datalogCache: string[][]; + + + private static cache: string[][]; //--------- @@ -83,14 +83,14 @@ namespace microdata { * and UP or DOWN is invoked this.currentRowOffset is modified once instead. * Modified when pressing UP or DOWN */ - private static currentRow: number + private currentRow: number /** * User modified column index; via LEFT & RIGHT. * * Used to determine which columns to draw. */ - private static currentCol: number + private currentCol: number /** * Used as index into .filteredReadStarts by: @@ -101,7 +101,7 @@ namespace microdata { * * Modified when pressing UP or DOWN */ - private static dataRowsIndex: number + private static currentRowOffset: number /** * This is unique per sensor, it is calculated once upon pressing A. @@ -125,40 +125,43 @@ namespace microdata { */ private filteredReadStarts: number[] - private static numberOfRows: number; + /** TabularDataViewer may be entered from the Command Mode, DataViewSelect or View Data (Home screen 4th button) */ + private goBack1PageFn: () => void + + private static numOfRows: number; - private backBtnFn: () => void; + private currentlyFiltering: boolean; - constructor(app: AppInterface, backBtnFn: () => void) { + constructor(app: AppInterface, goBack1PageFn: () => void) { super(app, "recordedDataViewer") this.guiState = DATA_VIEW_DISPLAY_MODE.UNFILTERED_DATA_VIEW + // Start on the 2nd row; since the first row is for headers: + this.currentRow = 1 + this.currentCol = 0 + this.numberOfFilteredRows = 0 this.filteredValue = "" this.filteredReadStarts = [0] this.filteredCol = 0 + this.currentlyFiltering = false; - this.backBtnFn = backBtnFn + this.goBack1PageFn = goBack1PageFn } - /* override */ startup() { + /* override */ startup() { super.startup() + basic.pause(50); + TabularDataViewer.numOfRows = datalogger.getNumberOfRows() - TabularDataViewer.numberOfRows = datalogger.getNumberOfRows(); - TabularDataViewer.needToScroll = TabularDataViewer.numberOfRows > TABULAR_MAX_ROWS + TabularDataViewer.currentRowOffset = 0 + TabularDataViewer.dataLoggerHeader = datalogger.getRows(TabularDataViewer.currentRowOffset, 1).split("\n")[0].split(","); + TabularDataViewer.currentRowOffset = 1 - // Start on the 2nd row; since the first row is for headers: - TabularDataViewer.currentRow = 1 - TabularDataViewer.currentCol = 0 - - TabularDataViewer.dataRowsIndex = 0 - TabularDataViewer.dataLoggerHeader = datalogger.getRows(TabularDataViewer.dataRowsIndex, 1).split("\n")[0].split(","); - - TabularDataViewer.fillCache(); - TabularDataViewer.dataRowsIndex = 1 // Don't start the user on the HEADER row, bump to the first row of actual data. - TabularDataViewer.updateDataRows(); + TabularDataViewer.fillCache(0); + TabularDataViewer.nextDataChunk(); this.headerStringLengths = TabularDataViewer.dataLoggerHeader.map((header) => (header.length + 5) * font.charWidth) @@ -166,47 +169,51 @@ 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.dataRowsIndex = 1 - TabularDataViewer.currentRow = 1 + TabularDataViewer.currentRowOffset = 1 + this.currentRow = 1 - TabularDataViewer.updateDataRows(); + TabularDataViewer.nextDataChunk(); this.guiState = DATA_VIEW_DISPLAY_MODE.UNFILTERED_DATA_VIEW } else { - this.backBtnFn(); + this.goBack1PageFn() } } ) - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.A.id, () => { if (this.guiState == DATA_VIEW_DISPLAY_MODE.UNFILTERED_DATA_VIEW) { - this.filteredCol = TabularDataViewer.currentCol; - this.filteredValue = TabularDataViewer.dataRows[TabularDataViewer.currentRow][this.filteredCol] - - TabularDataViewer.dataRowsIndex = 0 - TabularDataViewer.currentRow = 1 + this.filteredCol = this.currentCol; + this.filteredValue = TabularDataViewer.dataRows[this.currentRow][this.filteredCol] + TabularDataViewer.currentRowOffset = 0 + this.currentRow = 1 + this.currentlyFiltering = true; this.nextFilteredDataChunk(); - this.updateNeedToScroll(); //NOTE:FIX + 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 @@ -214,46 +221,48 @@ namespace microdata { // Control logic: while (tick) { - if (TabularDataViewer.currentRow > 0) - TabularDataViewer.currentRow = Math.max(TabularDataViewer.currentRow - 1, 1); + if (this.currentlyFiltering) + return; + + if (this.currentRow > 0) + this.currentRow = Math.max(this.currentRow - 1, 1); /** * When scrolling up the cursor might be at the bottom of the screen; so just move the cursor up one. * Or, the cursor could be on the 2nd row of the screen (index 1 since the first row are headers): * So don't move the cursor, load a new chunk of data. */ - if (TabularDataViewer.needToScroll && TabularDataViewer.currentRow == 1) { - TabularDataViewer.dataRowsIndex = Math.max(TabularDataViewer.dataRowsIndex - 1, 1); + if (TabularDataViewer.needToScroll && this.currentRow == 1) { + TabularDataViewer.currentRowOffset = Math.max(TabularDataViewer.currentRowOffset - 1, 1); if (this.guiState == DATA_VIEW_DISPLAY_MODE.UNFILTERED_DATA_VIEW) { - TabularDataViewer.dataRowsIndex = Math.max(TabularDataViewer.dataRowsIndex - 1, 1); - TabularDataViewer.updateDataRows(); + TabularDataViewer.currentRowOffset = Math.max(TabularDataViewer.currentRowOffset - 1, 1); + + // if (TabularDataViewer.currentRowOffset % (LOAD_QTY - 6) == 0) { + // TabularDataViewer.fillCache(TabularDataViewer.currentRowOffset); + // } + TabularDataViewer.nextDataChunk(); } else { - TabularDataViewer.dataRowsIndex = Math.max(TabularDataViewer.dataRowsIndex - 1, 0); + TabularDataViewer.currentRowOffset = Math.max(TabularDataViewer.currentRowOffset - 1, 0); this.nextFilteredDataChunk() } } - if (!controller.up.isPressed()) - break + basic.pause(100) } // Reset binding - control.onEvent(ControllerButtonEvent.Released, controller.up.id, () => { }) + context.onEvent(ControllerButtonEvent.Released, controller.up.id, () => { }) } ) - // LOAD 200 rows to cacheLog - // Copy to dataRows. - // cacheLog pointer, currentRow pointer. - - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.down.id, () => { let tick = true; - control.onEvent( + context.onEvent( ControllerButtonEvent.Released, controller.down.id, () => tick = false @@ -261,87 +270,68 @@ namespace microdata { // Control logic: while (tick) { - // let rowQty = (TabularDataViewer.dataRows.length < TABULAR_MAX_ROWS) ? TabularDataViewer.dataRows.length - 1 : TabularDataViewer.numberOfRows; - control.dmesg(`d: ${(TabularDataViewer.numberOfRows - TABULAR_MAX_ROWS)} ${(TabularDataViewer.dataRowsIndex + TabularDataViewer.currentRow)}`) - - if (this.guiState == DATA_VIEW_DISPLAY_MODE.UNFILTERED_DATA_VIEW) { - if (TabularDataViewer.currentRow < TABULAR_MAX_ROWS - 1) { - TabularDataViewer.currentRow++; - } else if (TabularDataViewer.needToScroll) { - TabularDataViewer.dataRowsIndex++; - - // if (TabularDataViewer.dataRowsIndex % MAX_ROWS_TO_CACHE == 0) { - if ((TabularDataViewer.dataRowsIndex + TABULAR_MAX_ROWS - 1) % MAX_ROWS_TO_CACHE == 0) { - TabularDataViewer.fillCache(); - } - TabularDataViewer.updateDataRows(); - } - } + 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: * When scrolling down the cursor might be at the top of the screen; so just move the cursor down one. * Or, the cursor could be on the last row of the screen: * So don't move the cursor, load a new chunk of data. - */ + */ // Boundary where there are TABULAR_MAX_ROWS - 1 number of rows: - // let beforeMs = control.millis() - - // if (TabularDataViewer.numberOfRows == TABULAR_MAX_ROWS) - // rowQty = TABULAR_MAX_ROWS - 1 - - // control.dmesg(`getNumberOfRows: ${(control.millis() - beforeMs)}`) - - // if (this.guiState == DATA_VIEW_DISPLAY_MODE.FILTERED_DATA_VIEW) - // rowQty = this.numberOfFilteredRows - // if (TabularDataViewer.needToScroll) { - // if (TabularDataViewer.currentRow + 1 < TABULAR_MAX_ROWS - 1) - // TabularDataViewer.currentRow += 1; - - // else if (TabularDataViewer.currentRowOffset <= rowQty - TABULAR_MAX_ROWS) { - // TabularDataViewer.currentRowOffset += 1; - - // if (this.guiState == DATA_VIEW_DISPLAY_MODE.UNFILTERED_DATA_VIEW) { - // if ((TabularDataViewer.currentRowOffset + TABULAR_MAX_ROWS) % MAX_ROWS_TO_CACHE == 0) { - // basic.showNumber(9) - - // // -2 since we start +1 from the header, and we want to move forward again. - // const nextCacheStart = TabularDataViewer.currentRow - TabularDataViewer.currentRowOffset + TABULAR_MAX_ROWS - 2 - // TabularDataViewer.fillCache(nextCacheStart); - // } - // TabularDataViewer.nextDataChunk(); - // } else { - // this.nextFilteredDataChunk() - // } - // } - // } - - // else if (TabularDataViewer.currentRow < rowQty) - // TabularDataViewer.currentRow += 1; - - if (!controller.down.isPressed()) - break + 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 (TabularDataViewer.currentRowOffset % (LOAD_QTY - 7) == 0) { + // TabularDataViewer.fillCache(TabularDataViewer.currentRowOffset - this.currentRow - TABULAR_MAX_ROWS); + // } + + TabularDataViewer.nextDataChunk(); + } else + this.nextFilteredDataChunk() + } + } + + else if (this.currentRow < rowQty) { + this.currentRow += 1; + } 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, () => { - TabularDataViewer.currentCol = Math.max(TabularDataViewer.currentCol - 1, 0) + this.currentCol = Math.max(this.currentCol - 1, 0) } ) - control.onEvent( + context.onEvent( ControllerButtonEvent.Pressed, controller.right.id, () => { - if (TabularDataViewer.currentCol + 1 < TabularDataViewer.dataRows[0].length - 1) - TabularDataViewer.currentCol += 1 + if (this.currentCol + 1 < TabularDataViewer.dataRows[0].length - 1) + this.currentCol += 1 } ) } @@ -351,27 +341,19 @@ namespace microdata { // STATIC METHODS: //---------------- - // public static updateDataChunks() { - // TabularDataViewer.nextDataChunk() - // } - - public static fillCache() { - // const from = (TabularDataViewer.dataRowsIndex < 5) ? TabularDataViewer.dataRowsIndex : TabularDataViewer.dataRowsIndex - TabularDataViewer.currentRow; - const from = TabularDataViewer.dataRowsIndex; + public static updateDataChunks() { + TabularDataViewer.nextDataChunk() + } - // let beforeMs = control.millis() - const rows = datalogger.getRows(from, MAX_ROWS_TO_CACHE).split("\n"); - // control.dmesg(`fcl: ${(rows.length)}`) - // control.dmesg(`readTime: ${(control.millis() - beforeMs)}`) + 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"); - // beforeMs = control.millis() - let nextDataChunk = [TabularDataViewer.dataLoggerHeader] + TabularDataViewer.cache = [] for (let i = 0; i < rows.length; i++) { - if (rows[i][0] != "") //NOTE: neccessary check now? - nextDataChunk.push(rows[i].split(",")); + if (rows[i][0] != "") + TabularDataViewer.cache.push(rows[i].split(",")); } - TabularDataViewer.datalogCache = nextDataChunk - // control.dmesg(`else: ${(control.millis() - beforeMs)}`) } /** @@ -379,53 +361,9 @@ namespace microdata { * Invoked when this.tabularYScrollOffset reaches its screen boundaries. * Mutates: this.dataRows */ - private static updateDataRows() { - // 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 - - - // control.dmesg(`b: ${(control.millis())}`) - // TabularDataViewer.needToScroll = TabularDataViewer.numberOfRows > TABULAR_MAX_ROWS - // TabularDataViewer.needToScroll = TabularDataViewer.numberOfRows - TABULAR_MAX_ROWS > TabularDataViewer.dataRowsIndex + TabularDataViewer.currentRow // YES - // if (!TabularDataViewer.needToScroll) - // basic.showNumber(5) - - // const nextCacheStart = TabularDataViewer.currentRow - TabularDataViewer.currentRowOffset + TABULAR_MAX_ROWS - 2 - // const endIndex = TabularDataViewer.currentRowOffset + TabularDataViewer.currentRow + TABULAR_MAX_ROWS; - - // const numTimesCachFilled = Math.floor((TabularDataViewer.currentRowOffset + TabularDataViewer.currentRow - 2) / MAX_ROWS_TO_CACHE) - // const start = TabularDataViewer.currentRowOffset - (MAX_ROWS_TO_CACHE * (numTimesCachFilled + 0)); - // const start = TabularDataViewer.currentRowOffset % (MAX_ROWS_TO_CACHE + TabularDataViewer.currentRow); - - - // YES to 3: - // const start = TabularDataViewer.dataRowsIndex; - // const end = start + TABULAR_MAX_ROWS; - // TabularDataViewer.dataRows = TabularDataViewer.datalogCache.slice(start, end) - - // control.dmesg(`s: ${(start)}, ${(end)}`) - // control.dmesg(`a: ${(TabularDataViewer.dataRows.length)}, ${(TabularDataViewer.datalogCache.length)}`) - // control.dmesg(`a: ${(control.millis())}`) - - - // control.dmesg(`a: ${(TabularDataViewer.needToScroll)} ${(TabularDataViewer.dataRows.length)} ${(TabularDataViewer.datalogCache.length)}`) - // - // Balance this eq: - // control.dmesg(`d: ${(TabularDataViewer.numberOfRows)} ${(TabularDataViewer.dataRowsIndex - TABULAR_MAX_ROWS)}`) - control.dmesg(`u: ${(TabularDataViewer.numberOfRows)} ${(TabularDataViewer.dataRowsIndex - TABULAR_MAX_ROWS)} ${(TabularDataViewer.numberOfRows)}`) - TabularDataViewer.needToScroll = TabularDataViewer.dataRowsIndex + TabularDataViewer.currentRow < TabularDataViewer.numberOfRows // YES - // TabularDataViewer.needToScroll = TabularDataViewer.dataRowsIndex + TabularDataViewer.currentRow < MAX_ROWS_TO_CACHE // YES - - const start = TabularDataViewer.dataRowsIndex % MAX_ROWS_TO_CACHE; - const end = start + TABULAR_MAX_ROWS; - TabularDataViewer.dataRows = TabularDataViewer.datalogCache.slice(start, end) + private static nextDataChunk() { + TabularDataViewer.needToScroll = TabularDataViewer.numOfRows > TABULAR_MAX_ROWS + TabularDataViewer.dataRows = [TabularDataViewer.dataLoggerHeader].concat(TabularDataViewer.cache.slice(TabularDataViewer.currentRowOffset, TabularDataViewer.currentRowOffset + TABULAR_MAX_ROWS)); } @@ -442,32 +380,35 @@ namespace microdata { * Mutates: this.filteredReadStarts[this.yScrollOffset + 1] */ private nextFilteredDataChunk() { - let start = this.filteredReadStarts[TabularDataViewer.dataRowsIndex]; + let start = this.filteredReadStarts[TabularDataViewer.currentRowOffset]; let nextFilteredDataChunk = [TabularDataViewer.dataLoggerHeader] // if (TabularDataViewer.currentRowOffset == 0) // TabularDataViewer.dataRows.push(datalogger.getRows(1, 1).split("\n")[0].split(",")); // 0 -> 1; - while (start < TabularDataViewer.numberOfRows && 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.dataRowsIndex == 0) ? 3 : 2)) { - this.filteredReadStarts[TabularDataViewer.dataRowsIndex + 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, TabularDataViewer.numberOfRows) } + // start += Math.min(TABULAR_MAX_ROWS, TabularDataViewer.numOfRows - start) // NOTE: -start here + // } TabularDataViewer.dataRows = nextFilteredDataChunk } @@ -478,16 +419,18 @@ namespace microdata { * Based upon this.filteredValue */ private updateNeedToScroll() { - const chunkSize = Math.min(20, TabularDataViewer.numberOfRows); // 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 < TabularDataViewer.numberOfRows; 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? @@ -522,7 +465,7 @@ namespace microdata { Screen.TOP_EDGE, Screen.LEFT_EDGE + cumulativeColOffset, Screen.HEIGHT, - 15 // black + 15 ) } } @@ -533,17 +476,17 @@ namespace microdata { Screen.TOP_EDGE + rowOffset, Screen.WIDTH, Screen.TOP_EDGE + rowOffset, - 15 // black + 15 ) } // Draw selected box: Screen.drawRect( Screen.LEFT_EDGE, - Screen.TOP_EDGE + (TabularDataViewer.currentRow * rowBufferSize), + Screen.TOP_EDGE + (this.currentRow * rowBufferSize), colBufferSizes[0], rowBufferSize, - 6 // blue + 6 ) } @@ -553,23 +496,24 @@ namespace microdata { Screen.TOP_EDGE, Screen.WIDTH, Screen.HEIGHT, - 0xC // Purple + 0xC ) if (TabularDataViewer.updateDataRowsOnNextFrame) - TabularDataViewer.updateDataRows() + TabularDataViewer.nextDataChunk() + // Could be optimised by calculating the Col line boundaries once & re-using them, instead of each frame: const tabularRowBufferSize = Screen.HEIGHT / Math.min(TabularDataViewer.dataRows.length, TABULAR_MAX_ROWS); - this.drawGridOfVariableColSize(this.headerStringLengths.slice(TabularDataViewer.currentCol), tabularRowBufferSize) + this.drawGridOfVariableColSize(this.headerStringLengths.slice(this.currentCol), tabularRowBufferSize) // Write the data into the grid: for (let row = 0; row < Math.min(TabularDataViewer.dataRows.length, TABULAR_MAX_ROWS); row++) { let cumulativeColOffset = 0; // Go through each column: - for (let col = 0; col < TabularDataViewer.dataRows[0].length - TabularDataViewer.currentCol; col++) { - const colID: number = col + TabularDataViewer.currentCol; + for (let col = 0; col < TabularDataViewer.dataRows[0].length - this.currentCol; col++) { + const colID: number = col + this.currentCol; let columnValue: string = TabularDataViewer.dataRows[row][colID]; @@ -587,6 +531,7 @@ namespace microdata { columnValue, Screen.LEFT_EDGE + cumulativeColOffset + (this.headerStringLengths[colID] >> 1) - ((font.charWidth * columnValue.length) >> 1), Screen.TOP_EDGE + (row * tabularRowBufferSize) + (tabularRowBufferSize >> 1) - 4, + // 0xb, 1, bitmaps.font8 ) From d987237998041d1ae1f025df584fbe5b4d8be523 Mon Sep 17 00:00:00 2001 From: KierPalin <45743174+KierPalin@users.noreply.github.com> Date: Wed, 26 Nov 2025 05:43:57 +0800 Subject: [PATCH 9/9] pxt.json: non-local dependencies --- pxt.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pxt.json b/pxt.json index 5deeaeb..856924c 100644 --- a/pxt.json +++ b/pxt.json @@ -3,11 +3,11 @@ "version": "1.7.5", "description": "Data science with micro:bit v2", "dependencies": { - "core": "file:../core", - "radio": "file:../radio", - "microphone": "file:../microphone", - "datalogger": "file:../datalogger", - "user-interface-base": "file:../user-interface-base", + "core": "*", + "radio": "*", + "microphone": "*", + "datalogger": "*", + "user-interface-base": "github:microbit-apps/user-interface-base#v0.0.33", "jacdac": "github:jacdac/pxt-jacdac#v1.9.40", "jacdac-light-level": "github:jacdac/pxt-jacdac/light-level#v1.9.40", "jacdac-soil-moisture": "github:jacdac/pxt-jacdac/soil-moisture#v1.9.40",