diff --git a/jacdacSensorMap.ts b/jacdacSensorMap.ts index b45f496..5d44be1 100644 --- a/jacdacSensorMap.ts +++ b/jacdacSensorMap.ts @@ -5,9 +5,7 @@ namespace sensors { * This is looked up by a SimpleSensorClient.serviceClass: * The metadata therein can be used to make a Sensor(), see getJacdacSensor() and wrapJacdacSensor() */ - type SimpleSensorsMap = { - /** This service class key is in decimal, since that's what jacdac.SimpleSensorClient.serviceClass returns */ - [serviceClass: number]: { + type SimpleSensorMetaData = { /** A unique name starting with 'Jacdac', e.g: JacdacButton not the same as the client specific name 'button1'. */ name: string, /** A unique shortened name starting with 'JD', e.g: JDB. This is useful is you want to transmit over radio. */ @@ -28,9 +26,12 @@ namespace sensors { */ stateFormat: string } - } - + type SimpleSensorsMap = { + /** This service class key is in decimal, since that's what jacdac.SimpleSensorClient.serviceClass returns */ + [serviceClass: number]: SimpleSensorMetaData + } + /** * Supported Jacdac Simple Sensors. * Maps a Jacdac sensor name to its service class in decimal. @@ -517,5 +518,14 @@ namespace sensors { return Object.keys(__jacdacSensorMap).map(k => parseInt(k)) as JacdacSensorSrvs[]; } + export function getSimpleSensorMetaData(srv: JacdacSensorSrvs): SimpleSensorMetaData { + const s = __jacdacSensorMap[srv]; + + if (!s) + throw "Error: Invalid serviceClass: that Jacdac Client is not supported. Please use a SimpleSensorClient." + + return s; + } + export const numberOfSupportedJacdacSensors: number = Object.keys(__jacdacSensorMap).length; } diff --git a/pxt.json b/pxt.json index dcd6f4a..194bbde 100644 --- a/pxt.json +++ b/pxt.json @@ -1,6 +1,6 @@ { "name": "sensors", - "version": "0.0.21", + "version": "0.0.22", "dependencies": { "core": "*", "radio": "*", diff --git a/ui/.github/workflows/makecode.yml b/ui/.github/workflows/makecode.yml new file mode 100644 index 0000000..9497b8f --- /dev/null +++ b/ui/.github/workflows/makecode.yml @@ -0,0 +1,13 @@ +name: MakeCode Build +on: + push: + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + - run: npx makecode diff --git a/ui/.prettierrc b/ui/.prettierrc new file mode 100644 index 0000000..292912b --- /dev/null +++ b/ui/.prettierrc @@ -0,0 +1 @@ +{"arrowParens":"avoid","semi":false,"tabWidth":4} \ No newline at end of file diff --git a/ui/main.ts b/ui/main.ts new file mode 100644 index 0000000..bd37ba3 --- /dev/null +++ b/ui/main.ts @@ -0,0 +1,152 @@ +jacdac.start() + +// TODO: +// efficiency +// - make it so that the widget updates on sensor change, not just on a loop +// - make it so that the widget only updates the text, not the whole thing +// - need a way for user to dismiss the widget, maybe a button on the widget itself? +// - need to handle user inputs (buttons, etc), user outputs, and actuators (motors, etc) in general, not just sensors +// - what happens when devices disconnect? optional dialog for this? +// - storing the RoleInfo in settings + +input.lightLevel() +input.temperature() + +// wait until the client is bound then create the widget +input.onButtonPressed(Button.A, () => { + console.log(`starting servers...`) + servers.start({ + touchP0: true, + touchP1: true, + touchP2: true, + soundLevel: true, + lightLevel: true, + temperature: true, + forceSimulators: true + }) +}) + + +namespace microgui { +import Button = user_interface_base.Button + +control.singleSimulator(); +const app = new App(); + +function convertToPercent(sensor: sensors.Sensor) { + if (sensor.unitName === "percent") { + return Math.roundWithPrecision(sensor.reading*100, 2)+" %" + } + return `${Math.roundWithPrecision(sensor.reading,3)} ${sensor.unitName}` +} + +function getTextComponent(role: string, sensor: sensors.Sensor) { + const simpleTextComponent = new TextBox({ + alignment: GUIComponentAlignment.CENTRE, + isActive: false, + title: sensor.name, + text: [ + convertToPercent(sensor), + `min: ${sensor.minimum.toString()}`, + `max: ${sensor.maximum.toString()}`, + `role: ${role}` + ], // optional arg + colour: 6, // optional arg + xScaling: 1.7, // optional arg + }) + return simpleTextComponent +} + +let sensorsToProcess: string[] = [] +let devicesServiceFound: string[] = [] + +type RoleInfo = { + rName: string, + deviceId: string, + serviceIndex: number +} + +type ServiceToRoleInfo = { + [serviceClass: number]: RoleInfo[] +} + +type ServiceToCount = { + [serviceClass: number]: number +} + +const serviceToRoleInfo: ServiceToRoleInfo = {} +const serviceToCount: ServiceToCount = {} +const roleToSensor: { [rName: string]: sensors.Sensor } = {} + +function getRoleInfoForService(serviceClass: number, deviceId: string, serviceIndex: number) : RoleInfo | undefined{ + try { + const sensorData = sensors.getSimpleSensorMetaData(serviceClass) + if (!serviceToRoleInfo[serviceClass]) { + serviceToRoleInfo[serviceClass] = [] + serviceToCount[serviceClass] = 0 + } + const rName = `${sensorData.rName}${serviceToCount[serviceClass]}` + const roleInfo = { + rName, + deviceId, + serviceIndex + } + serviceToRoleInfo[serviceClass].push(roleInfo) + serviceToCount[serviceClass]++ + return roleInfo + } catch (e) { + return undefined + } + +} + +jacdac.bus.on(jacdac.DEVICE_ANNOUNCE, (dev: jacdac.Device) => { + console.log(`device connected: ${dev.deviceId} with ${dev.serviceClassLength} services `) + for (let i = 1; i < dev.serviceClassLength; i++) { + const serviceClass = dev.serviceClassAt(i) + const devService = `${dev.deviceId}:${serviceClass}:${i}` + if (devicesServiceFound.find(d => d === devService)) { + continue + } + devicesServiceFound.push(devService) + const roleInfo = getRoleInfoForService(serviceClass, dev.deviceId, i) + if (!roleInfo) { + console.log(`service class ${serviceClass} not recognized as a sensor, skipping`) + continue + } + const sensor = sensors.getJacdacSensor(serviceClass, roleInfo.rName) + roleToSensor[roleInfo.rName] = sensor + sensorsToProcess.push(roleInfo.rName) + } +}) + +let currentRole: string | undefined= undefined +let gcs : GUIComponentScene | undefined = undefined + +context.onEvent(ControllerButtonEvent.Pressed, controller.B.id, + () => { + if (gcs) { + app.popScene() + gcs = undefined + currentRole = undefined + } + } +) + + +basic.forever(() => { + if (sensorsToProcess.length > 0 && !gcs) { + console.log(`processing sensor ${sensorsToProcess[0]}...`) + currentRole = sensorsToProcess.pop() + } + if (currentRole) { + const textComponent = getTextComponent(currentRole, roleToSensor[currentRole]) + app.popScene() + gcs = new GUIComponentScene({ app, + components: [textComponent] }) + gcs.showAllComponents() + app.pushScene(gcs) + } +}) + +} diff --git a/ui/mkc.json b/ui/mkc.json new file mode 100644 index 0000000..20ce172 --- /dev/null +++ b/ui/mkc.json @@ -0,0 +1,4 @@ +{ + "targetWebsite": "https://makecode.microbit.org/beta", + "links": {} +} \ No newline at end of file diff --git a/ui/pxt.json b/ui/pxt.json new file mode 100644 index 0000000..3416680 --- /dev/null +++ b/ui/pxt.json @@ -0,0 +1,19 @@ +{ + "name": "my-project", + "version": "0.0.22", + "files": [ + "main.ts" + ], + "supportedTargets": [ + "microbit" + ], + "dependencies": { + "core": "*", + "radio": "*", + "microphone": "*", + "microgui": "github:microbit-apps/microgui#v0.0.41", + "sensors": "github:microbit-apps/sensors#198ddbe594c953d81499e164dce58d457dfda325", + "jacdac-microbit": "github:jacdac/pxt-jacdac/devices/microbit" + }, + "testDependencies": {} +} diff --git a/ui/tsconfig.json b/ui/tsconfig.json new file mode 100644 index 0000000..1f12644 --- /dev/null +++ b/ui/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES5", + "noImplicitAny": true, + "outDir": "built", + "rootDir": "." + }, + "include": [ + "**/*.ts" + ], + "exclude": [ + "built/**", + "pxt_modules/**/*test.ts" + ] +} \ No newline at end of file