浏览代码

Implement rudimentary TUI

Thomas Dy 7 年之前
父节点
当前提交
a7c948b26e
共有 3 个文件被更改,包括 201 次插入54 次删除
  1. 118 0
      device.js
  2. 80 53
      index.js
  3. 3 1
      package.json

+ 118 - 0
device.js

@@ -0,0 +1,118 @@
+let noble = require('noble');
+let Rx = require('rx');
+
+const ACCEL_OFF = Buffer.from([0x00]);
+const ACCEL_ON = Buffer.from([0x01]);
+
+// MAGIC CONSTANTS FOLLOW
+// these might be different for other devices, I wouldn't know
+const IDENTIFIER = 'CHSLEEV_00';
+const DEVICE_INFO_UUID = '180a';
+const ACCEL_UUID = 'ffa0';
+const RATIO = 14;
+// MAGIC CONSTANTS END
+
+noble.on('stateChange', function(state) {
+  if(state === 'poweredOn') {
+    noble.startScanning();
+  }
+  else {
+    noble.stopScanning();
+  }
+});
+
+let button$ = new Rx.Subject();
+
+let discover$ = Rx.Observable.fromEvent(noble, 'discover');
+
+let peripheral$ = discover$
+  .filter(p => p.advertisement.localName === IDENTIFIER)
+  .do(_ => noble.stopScanning())
+  .take(1)
+  .shareReplay(1)
+
+let connection$ = peripheral$
+  .flatMapFirst(peripheral => {
+    let disconnect$ = Rx.Observable.fromEvent(peripheral, 'disconnect')
+      .map(_ => { return {state: 'disconnected'}})
+      .take(1);
+    let connect$ = Rx.Observable.fromEvent(peripheral, 'connect')
+      .map(_ => { return {state: 'connected', peripheral}})
+      .take(1);
+    peripheral.connect();
+    return disconnect$.merge(connect$);
+  })
+  .repeat()
+  .share()
+
+let services$ = connection$
+  .filter(s => s && s.state === 'connected')
+  .flatMap(s => {
+    let peripheral = s.peripheral;
+    let discoverServices = Rx.Observable.fromNodeCallback(
+      peripheral.discoverSomeServicesAndCharacteristics,
+      peripheral
+    );
+    return discoverServices([DEVICE_INFO_UUID, ACCEL_UUID], []);
+  })
+  .filter(s => s.length == 2 && s[0].length == 2)
+  .map(s => s[0])
+  .share()
+
+let serial$ = services$
+  .flatMap(s => {
+    let info = s[0].characteristics[2];
+    return Rx.Observable.fromNodeCallback(info.read, info)();
+  })
+
+let connStatus$ = connection$.combineLatest(serial$, (conn, serial) => {
+  if(conn.state == 'connected') {
+    return Object.assign({serial}, conn);
+  }
+  else {
+    return conn;
+  }
+}).share()
+
+let xAccel$ = services$
+  .do(s => {
+    s[1].characteristics[0].write(ACCEL_ON);
+    s[1].characteristics[2].subscribe();
+  })
+  .flatMap(s => {
+    return Rx.Observable.fromEvent(s[1].characteristics[2], 'data');
+  })
+  .map(d => d[1] * 256 + d[0])
+  .share()
+
+function item(type, value) {
+  return { type, value }
+}
+
+let tareFromButton$ = button$
+  .withLatestFrom(xAccel$, (_, accel) => item('reset', accel))
+
+let tareFromXAccel$ = xAccel$
+  .map(accel => item('value', accel))
+
+let tare$ = tareFromButton$.merge(tareFromXAccel$)
+  .scan((acc, item) => {
+    if(item.type === 'reset') {
+      return item.value;
+    }
+    else {
+      return Math.max(acc, item.value);
+    }
+  }, 0)
+
+let weight$ = xAccel$
+  .combineLatest(tare$, (accel, tare) => {
+    return (tare - accel) / RATIO;
+  })
+  .map(Math.floor)
+
+module.exports = {
+  button$,
+  connStatus$,
+  weight$
+}

+ 80 - 53
index.js

@@ -1,59 +1,86 @@
-let noble = require('noble');
-
-const ACCEL_OFF = Buffer.from([0x00]);
-const ACCEL_ON = Buffer.from([0x01]);
-
-// MAGIC CONSTANTS FOLLOW
-// these might be different for other devices, I wouldn't know
-const IDENTIFIER = 'CHSLEEV_00';
-const DEVICE_INFO_UUID = '180a';
-const ACCEL_UUID = 'ffa0';
-const TARE = 41690;
-const RATIO = 14;
-// MAGIC CONSTANTS END
-
-noble.on('stateChange', function(state) {
-  if(state === 'poweredOn') {
-    noble.startScanning();
+let device = require('./device');
+let blessed = require('blessed');
+
+let screen = blessed.screen({
+  smartCSR: true
+});
+
+screen.title = 'OpenPrepPad';
+
+let container = blessed.box({
+  top: 'center',
+  left: 'center',
+  width: 50,
+  height: 17,
+  border: {
+    type: 'line'
+  },
+  style: {
+    bg: 'cyan-bg'
   }
-  else {
-    noble.stopScanning();
+})
+
+let box = blessed.BigText({
+  top: 0,
+  right: 0,
+  width: 'shrink',
+  height: 'shrink',
+  tags: true,
+  style: {
+    fg: 'white'
   }
 });
 
-noble.on('discover', function(peripheral) {
-  if(peripheral.advertisement.localName === 'CHSLEEV_00') {
-    noble.stopScanning();
-
-    console.log('Prep Pad found with address '+peripheral.address);
-
-    peripheral.on('disconnect', function() {
-      console.log('Disconnecting from Prep Pad '+peripheral.address);
-      process.exit(0);
-    });
-
-    peripheral.connect(function(error) {
-      console.log('Connected to Prep Pad');
-      peripheral.discoverSomeServicesAndCharacteristics([DEVICE_INFO_UUID, ACCEL_UUID], [], function(error, services) {
-        if(services.length != 2) {
-          console.log('Could not find the relevant services. This might not actually be a Prep Pad');
-          peripheral.disconnect();
-        }
-        else {
-          let information = services[0];
-          let accelerometer = services[1];
-          information.characteristics[2].read(function(error, data) {
-            console.log('Serial Number: '+data);
-          });
-          accelerometer.characteristics[0].write(ACCEL_ON);
-          accelerometer.characteristics[2].subscribe();
-          accelerometer.characteristics[2].on('data', function(data, isNotification) {
-            let value = data[1] * 256 + data[0];
-            let grams = Math.floor((TARE - value) / RATIO);
-            console.log(grams+'g');
-          });
-        }
-      });
-    });
+container.append(box);
+screen.append(container);
+
+let statusLine = blessed.Text({
+  bottom: 0,
+  width: '100%',
+  height: 'shrink',
+  content: 'Connecting...',
+  style: {
+    fg: 'white',
+    bg: 'blue'
   }
 });
+
+screen.append(statusLine);
+
+let helpBox = blessed.box({
+  top: 0,
+  left: 0,
+  width: 'shrink',
+  height: 'shrink',
+  content: 'q - Quit\nz - Zero/Tare'
+});
+
+screen.append(helpBox);
+
+screen.key(['escape', 'q', 'C-c'], function(ch, key) {
+  process.exit(0);
+});
+screen.key(['z'], function(ch, key) {
+  device.button$.onNext('');
+});
+
+// Focus our element.
+box.focus();
+
+// Render the screen.
+screen.render();
+
+device.weight$.subscribe(w => {
+  box.setContent(w+'g');
+  screen.render();
+})
+
+device.connStatus$.subscribe(s => {
+  if(s.state == 'connected') {
+    statusLine.setContent('Connected to Prep Pad '+s.serial);
+  }
+  else {
+    statusLine.setContent('Connecting...');
+  }
+  screen.render();
+})

+ 3 - 1
package.json

@@ -4,8 +4,10 @@
   "description": "TUI for Prep Pad",
   "main": "index.js",
   "dependencies": {
+    "blessed": "^0.1.81",
     "noble": "^1.7.0",
-  ,
+    "rx": "^4.1.0"
+  },
   "devDependencies": {},
   "scripts": {
     "test": "echo \"Error: no test specified\" && exit 1"