Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
3 changes: 1 addition & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,7 @@ build:
### Test suite ############################################
.PHONY: test
test: build
npm install
npm test
cd test && npm install && npm test


### Formatting ############################################
Expand Down
21 changes: 3 additions & 18 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,22 +100,7 @@ Data from Senso can be recorded using the [`recorder`](src/dividat-driver/record

Like Senso data, but with `make record-flex`.

### Data replayer
### Mock Senso

Recorded data can be replayed for debugging purposes.

For default settings: `npm run replay`

To replay an other recording: `npm run replay -- rec/senso/simple.dat`

To change the replay speed: `npm run replay -- --speed=0.5 rec/senso/simple.dat`

To run without looping: `npm run replay -- --once`

#### Senso replay

The Senso replayer will appear as a Senso network device, so both driver and replayer should be running at the same time.

#### Senso Flex replay

The Senso Flex replayer (`npm run replay-flex`) supports the same parameters as the Senso replayer. It mocks the driver with respect to the `/flex` WebSocket resource, so the driver can not be running at the same time.
The Senso mocking tool can be used to simulate a Senso, including replaying data made with the recorder.
Run `mock-senso --help` for documentation and usage examples.
3 changes: 3 additions & 0 deletions mock-senso/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
tabWidth: 2
singleQuote: true
semi: false
120 changes: 120 additions & 0 deletions mock-senso/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import * as path from 'path'
import * as Replay from './lib/replay'
import * as Senso from './lib/senso'
import * as Flex from './lib/flex'
import * as commander from 'commander'
import * as Profile from './lib/senso/profile'

const parseVersion = (value: string): Profile.Version => {
const match = value.match(/^(\d+)\.(\d+)\.(\d+)$/)
if (match) {
const [major, minor, patch] = match
.slice(1)
.map((i) => parseInt(i, 10)) as any
return {
...Profile.defaultVersion,
major,
minor,
patch,
}
}
throw new commander.InvalidArgumentError(`Invalid semver string: ${value}`)
}

const _parseFloat = (value: string) => {
const p = parseFloat(value)
if (isNaN(p)) {
throw new commander.InvalidArgumentError(`"${value}" is not a number.`)
}
return p
}

const defaultRecordings = {
flex: 'recordings/flex/zero.dat',
senso: 'recordings/senso/zero.dat',
}

const description = `
Simulates a Senso or a Senso Flex device for testing purposes.

By default, the program simulates a Senso. The mock Senso will appear as a network
device, so the driver has to be running at the same time. The Senso can simulate
receiving firmware update commands, booting to DFU mode and receiving firmware.

When the --flex option is used, the program will simulate a Senso Flex. When using
this mode, the application mocks the driver at the /flex endpoint, so ensure the
actual driver is not running to avoid conflicts.

Sensor data is simulated by playing back recordings made with the Driver's recording
tool. Use the recording file argument to choose the recording being replayed.
`.trim()

const program = new commander.Command()
program
.name('mock-senso')
.description(description)
.usage('[options] [recording file]')
.option('[SENSO-OPTIONS]', '(Only in Senso mode)')
.option('--dfu', 'Boot in DFU mode', false)
.option(
'--fw <semver>',
'Specify the firmware version',
parseVersion,
'3.3.3' as any,
)
.option('\n[MODE]', '')
.option(
'--flex',
'Mock a Senso Flex. Note: Ensure the actual driver is not running concurrently when using this option.',
false,
)
.option('\n[PLAYBACK-OPTIONS]', '(Both in Senso and Flex mode)')
.option('--no-loop', 'Turn off looping of the recording')
.option(
'--speed <number>',
'Speed of replaying the recording.',
_parseFloat,
1.0,
)
.option('\n[OTHER]', '')
.argument(
'[recording file]',
`Path to the recording file to be replayed. Defaults to "${defaultRecordings.senso}" for Senso mode and
"${defaultRecordings.flex}" for Senso Flex mode if not specified.`,
)
.addHelpText(
'after',
`
Examples:
mock-senso --no-loop --speed=0.5 Simulate a Senso, replaying data at half speed, without looping.
mock-senso --dfu Simulate a Senso, starting in DFU mode.
mock-senso --flex recording.dat Simulate a Senso Flex using a specific recording.
`,
)
.action((recordingFile, options) => {
const recording =
recordingFile ||
(() => {
const r = options.flex
? defaultRecordings.flex
: defaultRecordings.senso
console.log('Using default recording:', r)
return path.resolve(__dirname, r)
})()

const replayOpts: Replay.Opts = {
loop: options.loop,
speed: options.speed,
recording: recording,
}

if (options.flex) {
return Flex.mock(replayOpts)
}
const sensoOpts: Senso.Opts = {
startInDfu: options.dfu,
firmwareVersion: options.fw,
}
Senso.mock(replayOpts, sensoOpts)
})
.parse()
29 changes: 29 additions & 0 deletions mock-senso/lib/flex/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as http from 'http'
import * as ws from 'ws'
import * as Replay from '../replay'

const PORT = 8382 // Driver port

export function mock(replayOpts: Replay.Opts) {
const server = http.createServer()
const wss = new ws.Server({ noServer: true })

wss.on('connection', function connection(ws) {
const dataStream = Replay.start(replayOpts)
dataStream.on('data', (data) => ws.send(data))
})

server.on('upgrade', function upgrade(request, socket, head) {
if (request.url === '/flex') {
wss.handleUpgrade(request, socket, head, function done(ws) {
wss.emit('connection', ws, request)
})
} else {
socket.destroy()
}
})

server.listen(PORT, () => {
console.log(`Mocking flex driver at localhost:${PORT}/flex`)
})
}
45 changes: 45 additions & 0 deletions mock-senso/lib/replay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import * as fs from 'fs'
import split from 'binary-split'
import { PassThrough, Transform } from 'stream'

export interface Opts {
recording: string
loop: boolean
speed: number
}

const playback = (speed: number) =>
new Transform({
transform(chunk, _, callback) {
const data = chunk.toString()
const items = data.split(',')

const [timeout, msg] =
items.length === 2 ? [parseInt(items[0]), items[1]] : [20, items[0]]

const buf = Buffer.from(msg, 'base64')

setTimeout(() => {
this.push(buf)
callback()
}, timeout / speed)
},
})

export function start(opts: Opts) {
const passthrough = new PassThrough()
;(function loop() {
fs.createReadStream(opts.recording)
.pipe(split())
.pipe(playback(opts.speed))
.on('end', () => {
if (opts.loop) {
loop()
} else {
passthrough.end()
}
})
.pipe(passthrough, { end: false })
})()
return passthrough
}
84 changes: 84 additions & 0 deletions mock-senso/lib/senso/application/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import net from 'net'
import respond from './response'
import { EventEmitter } from 'stream'
import { promisify } from 'util'
import * as Profile from '../profile'
import * as MDNS from '../mdns'
import * as Replay from '../../replay'

const TCP_PORT = 55567
const DATA_TYPE_SENSOR = 0x80

const server = net.createServer({ noDelay: true })
server.maxConnections = 1
const closeServer = promisify(server.close.bind(server))
const startListening = promisify<number, string>(server.listen.bind(server))

const eventEmitter = new EventEmitter<{ EnterDFU: [] }>()

const mdnsService: MDNS.Service = MDNS.service({
name: 'Mock Senso',
type: 'sensoControl',
protocol: 'tcp',
txt: {
ser_no: Profile.serial.device,
mode: 'Application',
},
port: TCP_PORT,
})

export function setup(host: string, replayOpts: Replay.Opts) {
const replay = Replay.start(replayOpts)
let socket: net.Socket | undefined

server.on('connection', (socket) => {
console.log(`Driver connected`)
socket = socket

replay.on('data', (buffer: Buffer) => {
// Only forward sensor data from replay
const dataType = buffer.subarray(8).readUInt8(2)
if (dataType == DATA_TYPE_SENSOR) {
socket.write(buffer)
}
})

socket.on('data', (buffer) => {
const responses = respond(buffer)
switch (responses) {
case 'EnterDFU': {
replay.removeAllListeners()
return eventEmitter.emit('EnterDFU')
}
default:
return responses.forEach((r) => socket.write(r))
}
})

socket.on('error', (e) => {
console.log(e)
})

socket.on('close', (hadError) => {
console.log(`Driver disconnected ${(hadError && 'with error') || ''}`)
})
})

return {
on: eventEmitter.on.bind(eventEmitter),
start: async () => {
console.log('Starting control mode')
await startListening(TCP_PORT, host)
mdnsService.publish()
replay.resume()
console.log(`Senso in control mode at ${host}:${TCP_PORT}`)
},
stop: async () => {
console.log('Stopping control mode')
mdnsService.unpublish()
socket?.destroy()
await closeServer()
replay.pause()
},
}
}
Loading