Skip to content

Commit

Permalink
TCP transport - implementation skeleton (cf. uhppoted/uhppote-core#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
twystd committed Jun 4, 2024
1 parent d101e39 commit 497e045
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 35 deletions.
46 changes: 38 additions & 8 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,44 @@

### IN PROGRESS

- [x] [`restore-default-parameters`](https://github.com/uhppoted/uhppoted/issues/48)
- [x] node
- [x] example
- [x] unit test
- [x] integration test
- [x] README
- [x] CHANGELOG

- [ ] TCP/IP protocol (cf. https://github.com/uhppoted/uhppote-core/issues/17)
- [ ] uhppoted
- [ ] uhppoted-get-device
- [ ] uhppoted-set-ip
- [ ] uhppoted-get-time
- [ ] uhppoted-set-time
- [ ] uhppoted-get-door-control
- [ ] uhppoted-set-door-control
- [ ] uhppoted-get-listener
- [ ] uhppoted-set-listener
- [ ] uhppoted-record-special-events
- [ ] uhppoted-get-status
- [ ] uhppoted-get-cards
- [ ] uhppoted-get-card
- [ ] uhppoted-get-card-by-index
- [ ] uhppoted-put-card
- [ ] uhppoted-delete-card
- [ ] uhppoted-delete-all-cards
- [ ] uhppoted-get-time-profile
- [ ] uhppoted-set-time-profile
- [ ] uhppoted-clear-time-profiles
- [ ] uhppoted-add-task
- [ ] uhppoted-clear-task-list
- [ ] uhppoted-refresh-task-list
- [ ] uhppoted-get-event-index
- [ ] uhppoted-set-event-index
- [ ] uhppoted-get-event
- [ ] uhppoted-open-door
- [ ] uhppoted-set-pc-control
- [ ] uhppoted-set-interlock
- [ ] uhppoted-activate-keypads
- [ ] uhppoted-set-door-passcodes
- [ ] uhppoted-restore-default-parameters
- [ ] examples
- [ ] integration-tests
- [ ] documentation
- [ ] CHANGELOG
- [ ] README

## TODO

Expand Down
4 changes: 4 additions & 0 deletions nodes/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,9 @@ module.exports = {
error: function (node, err) {
node.status({ fill: 'red', shape: 'dot', text: 'node-red:common.status.error' })
node.warn('uhppoted::' + err)
},

resolve: function(controller) {
return { controller:controller, address: null, protocol: 'udp'}
}
}
145 changes: 118 additions & 27 deletions nodes/uhppoted.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,20 @@ module.exports = {
* 'get' and 'set' are functionally identical but are defined separately for
* semantic clarity.
*
* @param {object} ctx Configuration, internationalisation translation and logger
* @param {number} deviceId The serial number for the target access controller
* @param {byte} op Operation code from 'opcode' module
* @param {object} request Operation parameters for use by codec.encode
* @param {object} ctx Configuration, internationalisation translation and logger
* @param {number} controller The serial number for the target access controller
* @param {byte} op Operation code from 'opcode' module
* @param {object} request Operation parameters for use by codec.encode
* @param {string} dest Optional controller IPv4 address. Defaults to UDP broadcast.
* @param {string} protocol Optional connection protocol ('udp' or 'tcp'). Defaults to
* 'udp' unless 'tcp'
*
* @param {object} Decoded reply containing the received information
*
* @exports
*/
get: async function (ctx, deviceId, op, request) {
const c = context(deviceId, ctx.config, ctx.logger)
get: async function (ctx, controller, op, request, dest = null, protocol = 'udp') {
const c = context(controller, ctx.config, ctx.logger)
const receiver = receiveAny(c.timeout)

try {
Expand All @@ -33,10 +36,14 @@ module.exports = {
}
}

throw new Error(`no reply from ${deviceId}`)
throw new Error(`no reply from ${controller}`)
}

return exec(c, op, request, receiver).then(decode)
if (protocol === 'tcp' && dest != null) {
return tcp(c, dest, op, request, receiver).then(decode)
} else {
return udp(c, op, request, receiver).then(decode)
}
} finally {
receiver.cancel()
}
Expand All @@ -47,17 +54,20 @@ module.exports = {
* 'get' and 'set' are functionally identical but are defined separately for
* semantic clarity.
*
* @param {object} ctx Configuration, internationalisation translation and logger
* @param {number} deviceId The serial number for the target access controller
* @param {byte} op Operation code from 'opcode' module
* @param {object} request Operation parameters for use by codec.encode
* @param {object} ctx Configuration, internationalisation translation and logger
* @param {number} controller The serial number for the target access controller
* @param {byte} op Operation code from 'opcode' module
* @param {object} request Operation parameters for use by codec.encode
* @param {string} dest Optional controller IPv4 address. Defaults to UDP broadcast.
* @param {string} protocol Optional connection protocol ('udp' or 'tcp'). Defaults to
* 'udp' unless 'tcp'
*
* @param {object} Decoded result of the operation
*
* @exports
*/
set: async function (ctx, deviceId, op, request) {
const c = context(deviceId, ctx.config, ctx.logger)
set: async function (ctx, controller, op, request, dest = null, protocol = 'udp') {
const c = context(controller, ctx.config, ctx.logger)
const receiver = receiveAny(c.timeout)

try {
Expand All @@ -69,10 +79,14 @@ module.exports = {
}
}

throw new Error(`no reply from ${deviceId}`)
throw new Error(`no reply from ${controller}`)
}

return exec(c, op, request, receiver).then(decode)
if (protocol === 'tcp' && dest != null) {
return tcp(c, dest, op, request, receiver).then(decode)
} else {
return udp(c, op, request, receiver).then(decode)
}
} finally {
receiver.cancel()
}
Expand All @@ -83,14 +97,17 @@ module.exports = {
* expecting a reply. Used solely by the 'set-ip' node - the UHPPOTE access controller
* does not reply to the set IP command.
*
* @param {object} ctx Configuration, internationalisation translation and logger
* @param {number} deviceId The serial number for the target access controller
* @param {byte} op Operation code from 'opcode' module
* @param {object} request Operation parameters for use by codec.encode
* @param {object} ctx Configuration, internationalisation translation and logger
* @param {number} controller The serial number for the target access controller
* @param {byte} op Operation code from 'opcode' module
* @param {object} request Operation parameters for use by codec.encode
* @param {string} dest Optional controller IPv4 address. Defaults to UDP broadcast.
* @param {string} protocol Optional connection protocol ('udp' or 'tcp'). Defaults to
* 'udp' unless 'tcp'
*
*/
send: async function (ctx, deviceId, op, request) {
const c = context(deviceId, ctx.config, ctx.logger)
send: async function (ctx, controller, op, request, dest = null, protocol = 'udp') {
const c = context(controller, ctx.config, ctx.logger)

const receiver = new Promise((resolve, reject) => {
resolve()
Expand All @@ -102,7 +119,11 @@ module.exports = {

receiver.received = (message) => {}

return exec(c, op, request, receiver).then(decode)
if (protocol === 'tcp' && dest != null) {
return tcp(c, dest, op, request, receiver).then(decode)
} else {
return udp(c, op, request, receiver).then(decode)
}
},

/**
Expand Down Expand Up @@ -151,7 +172,7 @@ module.exports = {
replies.push(new Uint8Array(message))
}

return exec(c, op, request, receiver).then(decode)
return udp(c, op, request, receiver).then(decode)
},

/**
Expand Down Expand Up @@ -208,7 +229,7 @@ module.exports = {
* @return {object} Decoded reply from access controller
*
*/
async function exec (ctx, op, request, receive) {
async function udp (ctx, op, request, receive) {
const sock = dgram.createSocket(opts)
const rq = codec.encode(op, ctx.deviceId, request)

Expand Down Expand Up @@ -259,11 +280,81 @@ async function exec (ctx, op, request, receive) {
throw new Error('no reply to request')
}

/**
* Sends a UDP command to a UHPPOTE access controller and returns the decoded
* reply, for use by 'get' and 'set'.
*
* configuration to receive events from UHPPOTE access controllers configured
* to send events to this host:port. Received events are forwarded to the
* supplied handler for dispatch to the application.
*
* @param {object} context Addresses, logger, debug, etc.
* @param {object} dest Destination { address,port }
* @param {byte} op Operation code from 'opcode' module
* @param {object} request Operation parameters for use by codec.encode
* @param {function} receive Handler for received messages
*
* @return {object} Decoded reply from access controller
*
*/
async function tcp (ctx, dest, op, request, receive) {
// const sock = dgram.createSocket(opts)
// const rq = codec.encode(op, ctx.deviceId, request)
//
// const onerror = new Promise((resolve, reject) => {
// sock.on('error', (err) => {
// reject(err)
// })
// })
//
// const send = new Promise((resolve, reject) => {
// sock.on('listening', () => {
// if (ctx.forceBroadcast || isBroadcast(ctx.addr.address)) {
// sock.setBroadcast(true)
// }
//
// sock.send(new Uint8Array(rq), 0, 64, ctx.addr.port, ctx.addr.address, (err, bytes) => {
// if (err) {
// reject(err)
// } else {
// log(ctx.debug, 'sent', rq, ctx.addr)
// resolve(bytes)
// }
// })
// })
//
// sock.bind({
// address: ctx.bind,
// port: 0
// })
// })
//
// sock.on('message', (message, rinfo) => {
// log(ctx.debug, 'received', message, rinfo)
//
// receive.received(new Uint8Array(message))
// })
//
// try {
// const result = await Promise.race([onerror, Promise.all([receive, send])])
//
// if (result && result.length === 2) {
// return result[0]
// }
// } finally {
// sock.close()
// }
//
// throw new Error('no reply to request')

throw new Error('** NOT IMPLEMENTED **')
}

/**
* Utility function to reconcile supplied configuration against the default
* values. Returns a working 'exec' context with valid:
* - UDP bind address:port
* - UDP destination address:port
* - bind address:port
* - destination address:port
* - timeout
* - debug enabled
*
Expand Down
23 changes: 23 additions & 0 deletions test/common_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const describe = require('mocha').describe
const assert = require('chai').assert
const it = require('mocha').it
const expect = require('chai').expect
const resolve = require('../nodes/common.js').resolve

describe('common', function () {
describe('resolve controller ID', function () {
it('should resolve a uint32 controller argument', function () {
const { controller, address, protocol } = resolve(405419896)

expect(controller).to.equal(405419896)
assert.isNull(address)
expect(protocol).to.equal('udp')
})

// it('should resolve an object controller ID', function () {
// const { controller, _address, _protocol } = resolve({ controller: 405419896 })
//
// expect(controller).to.equal(405419896)
// })
})
})

0 comments on commit 497e045

Please sign in to comment.