Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions microbit/v2/source/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ MicroBitUARTService *uart;
MicroBitLEDService *led;
MicroBitIOPinService *io;
MicroBitButtonService *btn;
MicroBitMagnetometerService *mag;

// State
int connected = 0;
Expand Down Expand Up @@ -101,6 +102,7 @@ int main()
io = new MicroBitIOPinService(*uBit.ble, uBit.io);
btn = new MicroBitButtonService(*uBit.ble);
accel = new MicroBitAccelerometerService(*uBit.ble, uBit.accelerometer);
mag = new MicroBitMagnetometerService(*uBit.ble, uBit.compass);

uart->eventOn("#");

Expand Down
16,004 changes: 8,386 additions & 7,618 deletions public/firmware/MICROBIT.hex

Large diffs are not rendered by default.

10 changes: 3 additions & 7 deletions src/__tests__/ml.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@ beforeAll(async () => {
// No webgl in tests running in node.
tf.setBackend('cpu');

// This creates determinism in the model training step.
const randomSpy = vi.spyOn(Math, 'random');
randomSpy.mockImplementation(() => 0.5);

gestures.importFrom(gestureData);
tensorFlowModel = await trainModel();
});
Expand Down Expand Up @@ -91,8 +87,8 @@ describe('Model tests', () => {

test('returns correct results on testing data', async () => {
const { tensorFlowResultAccuracy } = getModelResults(testdataShakeStill);
// The model thinks two samples of still are circle.
// 14 samples; 1.0 / 14 = 0.0714; 0.0714 * 12 correct inferences = 0.8571
expect(parseFloat(tensorFlowResultAccuracy)).toBeGreaterThan(0.85);
// The model thinks one sample of still is circle.
// 14 samples; 1.0 / 14 = 0.0714; 0.0714 * 13 correct inferences = 0.9286
expect(parseFloat(tensorFlowResultAccuracy)).toBeGreaterThan(0.9);
});
});
8 changes: 4 additions & 4 deletions src/__tests__/smoothenXYZData.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* SPDX-License-Identifier: MIT
*/

import { smoothenXYZData } from '../script/smoothenXYZData';
import { smoothXYZData } from '../script/smoothenXYZData';

describe('smoothenXYZData', () => {
test('smoothen empty data', () => {
Expand All @@ -16,19 +16,19 @@ describe('smoothenXYZData', () => {
y: [],
z: [],
};
expect(smoothenXYZData(xyz)).toEqual(xyz);
expect(smoothXYZData(xyz)).toEqual(xyz);
});
test('smoothen xyz data', () => {
const xyz = {
x: [1, 1, 1, 1, 1],
y: [4, 4, 12, 10, 10],
z: [8, 8, 24, 20, 20],
};
const smoothXYZData = {
const smoothedXYZData = {
x: [1, 1, 1, 1, 1],
y: [4, 4, 6, 7, 7.75],
z: [8, 8, 12, 14, 15.5],
};
expect(smoothenXYZData(xyz)).toEqual(smoothXYZData);
expect(smoothXYZData(xyz)).toEqual(smoothedXYZData);
});
});
6 changes: 3 additions & 3 deletions src/components/Gesture.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,9 @@

// Set timeout to allow recording in 1s
const unsubscribe = livedata.subscribe(data => {
newData.x.push(data.accelX);
newData.y.push(data.accelY);
newData.z.push(data.accelZ);
newData.x.push(data.x);
newData.y.push(data.y);
newData.z.push(data.z);
});

// Once duration is over (1000ms default), stop recording
Expand Down
1 change: 0 additions & 1 deletion src/components/Recording.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
-->

<script lang="ts">
import { fade } from 'svelte/transition';
import type { RecordingData } from '../script/stores/mlStore';
import RecordingGraph from './graphs/RecordingGraph.svelte';
import IconButton from './IconButton.svelte';
Expand Down
2 changes: 2 additions & 0 deletions src/components/bottom/BottomPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import View3DLive from '../3d-inspector/View3DLive.svelte';
import Information from '../information/Information.svelte';
import { state } from '../../script/stores/uiStore';
import SensorSelector from './SensorSelector.svelte';

const live3dViewVisible = false;
const live3dViewSize = live3dViewVisible ? 160 : 0;
Expand All @@ -27,6 +28,7 @@
<div class="flex items-center gap-4">
<!-- The live text and info box -->
<LiveGraphInformationSection />
<SensorSelector />
<ConnectedLiveGraphButtons />
{#if $state.reconnectState.reconnecting && $state.isInputConnected}
<div class="py-1px bg-white rounded-4xl">
Expand Down
162 changes: 162 additions & 0 deletions src/components/bottom/SensorSelector.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
<!--
(c) 2023, Center for Computational Thinking and Design at Aarhus University and contributors

SPDX-License-Identifier: MIT
-->

<script lang="ts">
import { createSelect, CreateSelectProps, melt } from '@melt-ui/svelte';
import { get } from 'svelte/store';
import { fade } from 'svelte/transition';
import ArrowDownIcon from 'virtual:icons/ri/arrow-down-s-line';
import { t } from '../../i18n';
import { clearGestures, DataSource, settings } from '../../script/stores/mlStore';
import StandardDialog from '../dialogs/StandardDialog.svelte';
import StandardButton from '../StandardButton.svelte';
import { gestures } from '../../script/stores/Stores';

interface Option {
label: string;
value: DataSource;
}

const options: Option[] = [
{ label: 'Accelerometer', value: DataSource.ACCELEROMETER },
{ label: 'Magnetometer', value: DataSource.MAGNETOMETER },
];

let showWarningDialog = false;
let changeSensorPromise: Promise<boolean> | undefined;
let changeSensorRes: (value: boolean) => void;

const closeWarningDialog = () => {
showWarningDialog = false;
changeSensorRes(false);
};

const changeSensorContinue = () => {
clearGestures();
changeSensorRes(true);
showWarningDialog = false;
};

const warnUser = (): boolean => {
const totalRecordings = $gestures
.map(g => g.recordings.length)
.reduce((a, b) => a + b, 0);
return !!totalRecordings;
};

let programaticChange = false;
const handleSelectedChanged: CreateSelectProps<DataSource>['onSelectedChange'] = ({
curr,
next,
}) => {
if (programaticChange) {
programaticChange = false;
return next;
}
if (curr?.value === next?.value) {
return next;
}
if (!next) {
return curr;
}
if (warnUser()) {
programaticChange = true;
changeSensorPromise = new Promise(res => {
changeSensorRes = res;
});
showWarningDialog = true;
changeSensorPromise.then(result => {
if (result) {
settings.update(obj => {
obj.dataSource = next.value;
return obj;
});
selected.set(next);
} else {
selected.set(curr);
}
});
} else {
settings.update(obj => {
obj.dataSource = next.value;
return obj;
});
}
return next;
};

const getDefaultSelected = () => {
const value = $settings.dataSource;
const defaultOption = options.find(option => option.value === value);
return defaultOption;
};

$: {
programaticChange = true;
selected.set(options.find(o => o.value === $settings.dataSource));
}

const {
elements: { trigger, menu, option, label },
states: { selectedLabel, open, selected },
} = createSelect<DataSource>({
forceVisible: true,
positioning: {
placement: 'bottom',
fitViewport: true,
sameWidth: true,
},
onSelectedChange: handleSelectedChanged,
defaultSelected: getDefaultSelected(),
});
</script>

<StandardDialog
isOpen={showWarningDialog}
onClose={closeWarningDialog}
class="w-150 space-y-5">
<svelte:fragment slot="heading">Warning</svelte:fragment>
<svelte:fragment slot="body">
<p>
Changing the sensor will clear your existing actions and recordings. Are you sure
you want to continue?
</p>
<div class="flex justify-end gap-x-5">
<StandardButton onClick={closeWarningDialog}>{$t('actions.cancel')}</StandardButton>
<StandardButton type="primary" onClick={changeSensorContinue}>Yes</StandardButton>
</div>
</svelte:fragment>
</StandardDialog>

<div class="flex flex-col gap-1">
<label class="sr-only" use:melt={$label}>Sensor</label>
<button
class="flex h-10 min-w-[170px] items-center justify-between rounded-lg bg-white px-3 py-2
shadow"
use:melt={$trigger}
aria-label="Sensor">
{$selectedLabel || 'Select a sensor'}
<ArrowDownIcon class="size-5" />
</button>
{#if $open}
<div
class="z-10 flex flex-col
overflow-y-auto rounded-lg bg-white p-1
shadow focus:!ring-0"
use:melt={$menu}
transition:fade={{ duration: 150 }}>
{#each options as item}
<div
class="relative cursor-pointer rounded-lg py-1 px-2
hover:bg-gray-100 focus:z-10
data-[highlighted]:bg-gray-200"
use:melt={$option({ value: item.value, label: item.label })}>
{item.label}
</div>
{/each}
</div>
{/if}
</div>
20 changes: 16 additions & 4 deletions src/components/graphs/DimensionLabels.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<script lang="ts">
import { currentData } from '../../script/stores/mlStore';
import { state } from '../../script/stores/uiStore';
import { liveGraphRange } from './graph-utils';

$: {
const data = $currentData;
Expand All @@ -27,9 +28,20 @@
{ label: 'z', arrowHeight: 0, labelHeight: 0, color: '#808ef9', id: 2 },
];

function updateDimensionLabels(axes: number[]) {
function scale(value: number) {
const newMin = (10 / (liveGraphRange.max + Math.abs(liveGraphRange.min))) * 0.3;
const newMax = 10 - newMin;
const existingMin = 2;
const existingMax = -2;
return (
((newMax - newMin) * (value - existingMin)) / (existingMax - existingMin) + newMin
);
}

function updateDimensionLabels(data: number[]) {
for (let i = 0; i < 3; i++) {
labels[i].arrowHeight = (2.1 - axes[labels[i].id]) * 2.32;
const value = data[labels[i].id];
labels[i].arrowHeight = scale(value);
}
fixOverlappingLabels();
}
Expand Down Expand Up @@ -81,11 +93,11 @@
<div
class="absolute arrowLeft -m-3.5"
style="transform: translateY({dimension.arrowHeight +
0.75}rem) scale(1, 0.75); border-right-color: {dimension.color};" />
10 / 16 / 2}rem) scale(1, 0.75); border-right-color: {dimension.color};" />
<p
class="absolute ml-3 text-xl"
style="transform: translateY({dimension.labelHeight -
0.5}rem); color: {dimension.color};">
1.75 / 2}rem); color: {dimension.color};">
{dimension.label}
</p>
{/each}
Expand Down
13 changes: 7 additions & 6 deletions src/components/graphs/LiveGraph.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import { currentData, settings } from '../../script/stores/mlStore';
import { state } from '../../script/stores/uiStore';
import DimensionLabels from './DimensionLabels.svelte';
import { liveGraphRange } from './graph-utils';

// Updates width to ensure that the canvas fills the whole screen
export let width: number;
Expand All @@ -26,8 +27,8 @@
// On mount draw smoothieChart
onMount(() => {
chart = new SmoothieChart({
maxValue: 2.3,
minValue: -2,
maxValue: liveGraphRange.max,
minValue: liveGraphRange.min,
millisPerPixel: 7,
grid: {
fillStyle: '#ffffff00',
Expand Down Expand Up @@ -72,14 +73,14 @@
}

// Set start line
recordLines.append(new Date().getTime() - 1, -2, false);
recordLines.append(new Date().getTime(), 2.3, false);
recordLines.append(new Date().getTime() - 1, liveGraphRange.min, false);
recordLines.append(new Date().getTime(), liveGraphRange.max, false);

// Wait a second and set end line
blockRecordingStart = true;
setTimeout(() => {
recordLines.append(new Date().getTime() - 1, 2.3, false);
recordLines.append(new Date().getTime(), -2, false);
recordLines.append(new Date().getTime() - 1, liveGraphRange.max, false);
recordLines.append(new Date().getTime(), liveGraphRange.min, false);
blockRecordingStart = false;
}, get(settings).duration);
}
Expand Down
Loading