<commit_before>const { Endpoint } = require('westfield-endpoint')

const WireMessageUtil = require('./WireMessageUtil')
const CompositorInterceptor = require('./wire/CompositorInterceptor')

class NativeClientSession {
  /**
   * @param {Object}wlClient
   * @param {NativeCompositorSession}compositorSession
   * @returns {NativeClientSession}
   */
  static create (wlClient, compositorSession) {
    const dataChannel = compositorSession.rtcClient.peerConnection.createDataChannel()
    const nativeClientSession = new NativeClientSession(wlClient, compositorSession, dataChannel)
    nativeClientSession.onDestroy().then(() => {
      if (dataChannel.readyState === 'open' || dataChannel.readyState === 'connecting') {
        dataChannel.close()
      }
    })

    Endpoint.setClientDestroyedCallback(wlClient, () => nativeClientSession.destroy())
    Endpoint.setRegistryCreatedCallback(wlClient, (wlRegistry, registryId) => nativeClientSession._onRegistryCreated(wlRegistry, registryId))
    Endpoint.setWireMessageCallback(wlClient, (wlClient, message, objectId, opcode, hasNativeResource) => nativeClientSession._onWireMessageRequest(wlClient, message, objectId, opcode, hasNativeResource))
    Endpoint.setWireMessageEndCallback(wlClient, (wlClient, fdsIn) => nativeClientSession._onWireMessageEnd(wlClient, fdsIn))

    dataChannel.onerror = () => nativeClientSession.destroy()
    dataChannel.onclose = event => nativeClientSession._onClose(event)
    dataChannel.onmessage = event => nativeClientSession._onMessage(event)

    // TODO data channel options from config
    dataChannel.onopen = () => {
      dataChannel.onerror = event => nativeClientSession._onError(event)
      // flush out any requests that came in while we were waiting for the data channel to open.
      process.env.DEBUG && console.log(`[app-endpoint-${nativeClientSession._nativeCompositorSession.rtcClient.appEndpointCompositorPair.appEndpointSessionId}] Native client session: RTC data channel to browser is open.`)
      nativeClientSession._flushOutboundMessage()
    }

    return nativeClientSession
  }

  /**
   * @param {number}objectId
   * @param {number}opcode
   * @returns {boolean}
   * @private
   */
  static _isGetRegistry (objectId, opcode) {
    const displayId = 1
    const getRegistryOpcode = 1
    return objectId === displayId && opcode === getRegistryOpcode
  }

  /**
   * @param {number}objectId
   * @param {number}opcode
   * @returns {boolean}
   * @private
   */
  static _isSync (objectId, opcode) {
    const displayId = 1
    const syncOpcode = 0
    return objectId === displayId && opcode === syncOpcode
  }

  /**
   * @param {Object}wlClient
   * @param {NativeCompositorSession}nativeCompositorSession
   * @param {RTCDataChannel}dataChannel
   */
  constructor (wlClient, nativeCompositorSession, dataChannel) {
    /**
     * @type {Object}
     */
    this.wlClient = wlClient
    /**
     * @type {NativeCompositorSession}
     * @private
     */
    this._nativeCompositorSession = nativeCompositorSession
    /**
     * @type {function():void}
     * @private
     */
    this._destroyResolve = null
    /**
     * @type {Promise<void>}
     * @private
     */
    this._destroyPromise = new Promise(resolve => { this._destroyResolve = resolve })
    /**
     * @type {Array<ArrayBuffer>}
     * @private
     */
    this._pendingWireMessages = []
    /**
     * @type {number}
     * @private
     */
    this._pendingMessageBufferSize = 0
    /**
     * @type {Array<ArrayBuffer>}
     * @private
     */
    this._outboundMessages = []
    /**
     * @type {Object.<number,WireMessageInterceptor>}
     * @private
     */
    this._interceptors = {}
    /**
     * @type {Object.<number, Object>}
     * @private
     */
    this._wlRegistries = {}
    /**
     * @type {RTCDataChannel}
     * @private
     */
    this._dataChannel = dataChannel
  }

  /**
   * @param {ArrayBuffer}buffer
   * @private
   */
  _onWireMessageEvents (buffer) {
    // receive from data channel
    process.env.DEBUG && console.log(`[app-endpoint-${this._nativeCompositorSession.rtcClient.appEndpointCompositorPair.appEndpointSessionId}] Native client session: received wire messages from browser.`)

    const receiveBuffer = new Uint32Array(buffer)
    let readOffset = 0
    let localGlobalsEmitted = false
    while (readOffset < receiveBuffer.length) {
      const fdsCount = receiveBuffer[readOffset++]
      const fdsBuffer = receiveBuffer.subarray(readOffset, readOffset + fdsCount)
      readOffset += fdsCount
      const sizeOpcode = receiveBuffer[readOffset + 1]
      const size = sizeOpcode >>> 16
      const length = size / Uint32Array.BYTES_PER_ELEMENT
      const messageBuffer = receiveBuffer.subarray(readOffset, readOffset + length)
      readOffset += length

      if (!localGlobalsEmitted) {
        // check if browser compositor is emitting globals, if so, emit the local globals as well.
        localGlobalsEmitted = this._emitLocalGlobals(messageBuffer)
      }

      Endpoint.sendEvents(
        this.wlClient,
        messageBuffer.buffer.slice(messageBuffer.byteOffset, messageBuffer.byteOffset + messageBuffer.byteLength),
        fdsBuffer.buffer.slice(fdsBuffer.byteOffset, fdsBuffer.byteOffset + fdsBuffer.byteLength)
      )
    }

    Endpoint.flush(this.wlClient)
  }

  _flushOutboundMessage () {
    process.env.DEBUG && console.log(`[app-endpoint-${this._nativeCompositorSession.rtcClient.appEndpointCompositorPair.appEndpointSessionId}] Native client session: sending ${this._outboundMessages.length} queued outbound messages.`)
    while (this._outboundMessages.length) {
      this._dataChannel.send(this._outboundMessages.shift())
    }
  }

  /**
   * @param {Uint32Array}wireMessageBuffer
   * @returns {boolean}
   * @private
   */
  _emitLocalGlobals (wireMessageBuffer) {
    const id = wireMessageBuffer[0]
    const wlRegistry = this._wlRegistries[id]
    if (wlRegistry) {
      const sizeOpcode = wireMessageBuffer[1]
      const messageOpcode = sizeOpcode & 0x0000FFFF
      const globalOpcode = 0
      if (messageOpcode === globalOpcode) {
        process.env.DEBUG && console.log(`[app-endpoint-${this._nativeCompositorSession.rtcClient.appEndpointCompositorPair.appEndpointSessionId}] Native client session: received globals emit wire message from browser. Will emit local globals as well.`)
        Endpoint.emitGlobals(wlRegistry)
        return true
      }
    }
    return false
  }

  /**
   *
   * @param {number}id
   * @param {number}opcode
   * @param {ArrayBuffer}message
   * @private
   */
  _isRemoteGlobalBind (id, opcode, message) {
    const wlRegistry = this._wlRegistries[id]
    let isRemoteGlobal = false
    if (wlRegistry) {
      const bindOpcode = 0
      if (opcode === bindOpcode) {
        const args = this.parse('usun', message)
        const globalName = args[0]
        this._trackCompositorGlobal(...args)
        isRemoteGlobal = !this._nativeCompositorSession.localGlobalNames.includes(globalName)
      }
    }
    return isRemoteGlobal
  }

  /**
   * @param {Object}wlClient
   * @param {ArrayBuffer}message
   * @param {number}objectId
   * @param {number}opcode
   * @param {boolean}hasNativeResource
   * @returns {number}
   * @private
   */
  _onWireMessageRequest (wlClient, message, objectId, opcode, hasNativeResource) {
    // - If the resource does not exist locally, then the request should only happen remotely.
    // - If the resource exist locally, then the request should only happen locally.
    //  Except:
    //  - If the resource is 'display' and the request is 'get_registry', then the request should be happen both locally & remotely.
    //  - If the resource is 'display' and the request is 'sync', then the request should be happen only remotely.
    //  - If the resource is 'registry' and the request is 'bind' for a locally created global, then the request should only happen locally.
    //  - If the resource is 'registry' and the request is 'bind' for a remotely created global, then the request should only happen remotely.

    // by default, always consume the message & delegate to browser
    let consumed = 1

    // if there is a native resource and the request is not a bind to a remote global or a sync, then there are some special cases that apply.
    if (hasNativeResource &&
      !NativeClientSession._isSync(objectId, opcode) &&
      !this._isRemoteGlobalBind(objectId, opcode, message)) {
      if (NativeClientSession._isGetRegistry(objectId, opcode)) {
        // if the resource is 'display' and the request is 'get_registry', then the request should be happen
        // both locally & in the browser.
        consumed = 0
      } else {
        // if none of the above conditions apply, then let the local resource implementation handle the request
        return 0
      }
    }

    if (consumed) {
      const interceptor = this._interceptors[objectId]
      if (interceptor) {
        interceptor.intercept(message, opcode)
      }
    }

    this._pendingMessageBufferSize += message.byteLength
    this._pendingWireMessages.push(message)

    return consumed
  }

  /**
   * @param {Object}wlClient
   * @param {ArrayBuffer}fdsIn
   */
  _onWireMessageEnd (wlClient, fdsIn) {
    const fdsBufferSize = Uint32Array.BYTES_PER_ELEMENT + (fdsIn ? fdsIn.byteLength : 0)
    const sendBuffer = new Uint32Array(new ArrayBuffer(fdsBufferSize + this._pendingMessageBufferSize))
    let offset = 0
    sendBuffer[0] = fdsIn ? fdsIn.byteLength / Uint32Array.BYTES_PER_ELEMENT : 0
    offset += 1

    if (fdsIn) {
      const fdsArray = new Uint32Array(fdsIn)
      sendBuffer.set(fdsArray)
      offset += fdsArray.length
    }

    this._pendingWireMessages.forEach(value => {
      const wireMessage = new Uint32Array(value)
      sendBuffer.set(wireMessage, offset)
      offset += wireMessage.length
    })

    if (this._dataChannel.readyState === 'open') {
      process.env.DEBUG && console.log(`[app-endpoint-${this._nativeCompositorSession.rtcClient.appEndpointCompositorPair.appEndpointSessionId}] Native client session: forwarding wire messages to browser.`)
      this._dataChannel.send(sendBuffer.buffer)
    } else {
      // queue up data until the channel is open
      process.env.DEBUG && console.log(`[app-endpoint-${this._nativeCompositorSession.rtcClient.appEndpointCompositorPair.appEndpointSessionId}] Native client session: not forwarding wire messages to browser. Channel not open. Queueing.`)
      this._outboundMessages.push(sendBuffer.buffer)
    }
  }

  /**
   *
   * @param {string}signature
   * @param {ArrayBuffer}wireMessageBuffer
   * @returns {null|Array<*>}
   */
  parse (signature, wireMessageBuffer) {
    return WireMessageUtil.unmarshallArgs({
      buffer: wireMessageBuffer,
      bufferOffset: 8,
      consumed: 8,
      fds: [],
      size: new Uint32Array(wireMessageBuffer)[1] >>> 16
    }, signature)
  }

  /**
   * @returns {Promise<void>}
   */
  onDestroy () {
    return this._destroyPromise
  }

  destroy () {
    this._destroyResolve()
  }

  /**
   * @param {MessageEvent}event
   * @private
   */
  _onMessage (event) {
    const receiveBuffer = /** @type {ArrayBuffer} */event.data
    this._onWireMessageEvents(receiveBuffer)
  }

  /**
   * @param {RTCErrorEvent}event
   * @private
   */
  _onError (event) {
    // TODO log error
    process.env.DEBUG && console.log(`[app-endpoint-${this._nativeCompositorSession.rtcClient.appEndpointCompositorPair.appEndpointSessionId}] Native client session: RTC data channel is in error ${JSON.stringify(event.error)}.`)
  }

  /**
   * @param {Event}event
   * @private
   */
  _onClose (event) {
    process.env.DEBUG && console.log(`[app-endpoint-${this._nativeCompositorSession.rtcClient.appEndpointCompositorPair.appEndpointSessionId}] Native client session: RTC data channel is closed.`)
    this.destroy()
  }

  /**
   * @param {Object}wlRegistry
   * @param {number}registryId
   * @private
   */
  _onRegistryCreated (wlRegistry, registryId) {
    this._wlRegistries[registryId] = wlRegistry
  }

  /**
   * @param {number}uniqueName
   * @param {string}interfaceName
   * @param {number}version
   * @param {number}objectId
   * @private
   */
  _trackCompositorGlobal (uniqueName, interfaceName, version, objectId) {
    // TODO track compositor resource destruction
    if (interfaceName === 'wl_compositor') {
      this._interceptors[objectId] = CompositorInterceptor.create(this._interceptors)
    }
  }
}

module.exports = NativeClientSession
<commit_msg>clear pending wire messages after they've been send<commit_after>const { Endpoint } = require('westfield-endpoint')

const WireMessageUtil = require('./WireMessageUtil')
const CompositorInterceptor = require('./wire/CompositorInterceptor')

class NativeClientSession {
  /**
   * @param {Object}wlClient
   * @param {NativeCompositorSession}compositorSession
   * @returns {NativeClientSession}
   */
  static create (wlClient, compositorSession) {
    const dataChannel = compositorSession.rtcClient.peerConnection.createDataChannel()
    const nativeClientSession = new NativeClientSession(wlClient, compositorSession, dataChannel)
    nativeClientSession.onDestroy().then(() => {
      if (dataChannel.readyState === 'open' || dataChannel.readyState === 'connecting') {
        dataChannel.close()
      }
    })

    Endpoint.setClientDestroyedCallback(wlClient, () => nativeClientSession.destroy())
    Endpoint.setRegistryCreatedCallback(wlClient, (wlRegistry, registryId) => nativeClientSession._onRegistryCreated(wlRegistry, registryId))
    Endpoint.setWireMessageCallback(wlClient, (wlClient, message, objectId, opcode, hasNativeResource) => nativeClientSession._onWireMessageRequest(wlClient, message, objectId, opcode, hasNativeResource))
    Endpoint.setWireMessageEndCallback(wlClient, (wlClient, fdsIn) => nativeClientSession._onWireMessageEnd(wlClient, fdsIn))

    dataChannel.onerror = () => nativeClientSession.destroy()
    dataChannel.onclose = event => nativeClientSession._onClose(event)
    dataChannel.onmessage = event => nativeClientSession._onMessage(event)

    // TODO data channel options from config
    dataChannel.onopen = () => {
      dataChannel.onerror = event => nativeClientSession._onError(event)
      // flush out any requests that came in while we were waiting for the data channel to open.
      process.env.DEBUG && console.log(`[app-endpoint-${nativeClientSession._nativeCompositorSession.rtcClient.appEndpointCompositorPair.appEndpointSessionId}] Native client session: RTC data channel to browser is open.`)
      nativeClientSession._flushOutboundMessage()
    }

    return nativeClientSession
  }

  /**
   * @param {number}objectId
   * @param {number}opcode
   * @returns {boolean}
   * @private
   */
  static _isGetRegistry (objectId, opcode) {
    const displayId = 1
    const getRegistryOpcode = 1
    return objectId === displayId && opcode === getRegistryOpcode
  }

  /**
   * @param {number}objectId
   * @param {number}opcode
   * @returns {boolean}
   * @private
   */
  static _isSync (objectId, opcode) {
    const displayId = 1
    const syncOpcode = 0
    return objectId === displayId && opcode === syncOpcode
  }

  /**
   * @param {Object}wlClient
   * @param {NativeCompositorSession}nativeCompositorSession
   * @param {RTCDataChannel}dataChannel
   */
  constructor (wlClient, nativeCompositorSession, dataChannel) {
    /**
     * @type {Object}
     */
    this.wlClient = wlClient
    /**
     * @type {NativeCompositorSession}
     * @private
     */
    this._nativeCompositorSession = nativeCompositorSession
    /**
     * @type {function():void}
     * @private
     */
    this._destroyResolve = null
    /**
     * @type {Promise<void>}
     * @private
     */
    this._destroyPromise = new Promise(resolve => { this._destroyResolve = resolve })
    /**
     * @type {Array<ArrayBuffer>}
     * @private
     */
    this._pendingWireMessages = []
    /**
     * @type {number}
     * @private
     */
    this._pendingMessageBufferSize = 0
    /**
     * @type {Array<ArrayBuffer>}
     * @private
     */
    this._outboundMessages = []
    /**
     * @type {Object.<number,WireMessageInterceptor>}
     * @private
     */
    this._interceptors = {}
    /**
     * @type {Object.<number, Object>}
     * @private
     */
    this._wlRegistries = {}
    /**
     * @type {RTCDataChannel}
     * @private
     */
    this._dataChannel = dataChannel
  }

  /**
   * @param {ArrayBuffer}buffer
   * @private
   */
  _onWireMessageEvents (buffer) {
    // receive from data channel
    process.env.DEBUG && console.log(`[app-endpoint-${this._nativeCompositorSession.rtcClient.appEndpointCompositorPair.appEndpointSessionId}] Native client session: received wire messages from browser.`)

    const receiveBuffer = new Uint32Array(buffer)
    let readOffset = 0
    let localGlobalsEmitted = false
    while (readOffset < receiveBuffer.length) {
      const fdsCount = receiveBuffer[readOffset++]
      const fdsBuffer = receiveBuffer.subarray(readOffset, readOffset + fdsCount)
      readOffset += fdsCount
      const sizeOpcode = receiveBuffer[readOffset + 1]
      const size = sizeOpcode >>> 16
      const length = size / Uint32Array.BYTES_PER_ELEMENT
      const messageBuffer = receiveBuffer.subarray(readOffset, readOffset + length)
      readOffset += length

      if (!localGlobalsEmitted) {
        // check if browser compositor is emitting globals, if so, emit the local globals as well.
        localGlobalsEmitted = this._emitLocalGlobals(messageBuffer)
      }

      Endpoint.sendEvents(
        this.wlClient,
        messageBuffer.buffer.slice(messageBuffer.byteOffset, messageBuffer.byteOffset + messageBuffer.byteLength),
        fdsBuffer.buffer.slice(fdsBuffer.byteOffset, fdsBuffer.byteOffset + fdsBuffer.byteLength)
      )
    }

    Endpoint.flush(this.wlClient)
  }

  _flushOutboundMessage () {
    process.env.DEBUG && console.log(`[app-endpoint-${this._nativeCompositorSession.rtcClient.appEndpointCompositorPair.appEndpointSessionId}] Native client session: sending ${this._outboundMessages.length} queued outbound messages.`)
    while (this._outboundMessages.length) {
      this._dataChannel.send(this._outboundMessages.shift())
    }
  }

  /**
   * @param {Uint32Array}wireMessageBuffer
   * @returns {boolean}
   * @private
   */
  _emitLocalGlobals (wireMessageBuffer) {
    const id = wireMessageBuffer[0]
    const wlRegistry = this._wlRegistries[id]
    if (wlRegistry) {
      const sizeOpcode = wireMessageBuffer[1]
      const messageOpcode = sizeOpcode & 0x0000FFFF
      const globalOpcode = 0
      if (messageOpcode === globalOpcode) {
        process.env.DEBUG && console.log(`[app-endpoint-${this._nativeCompositorSession.rtcClient.appEndpointCompositorPair.appEndpointSessionId}] Native client session: received globals emit wire message from browser. Will emit local globals as well.`)
        Endpoint.emitGlobals(wlRegistry)
        return true
      }
    }
    return false
  }

  /**
   *
   * @param {number}id
   * @param {number}opcode
   * @param {ArrayBuffer}message
   * @private
   */
  _isRemoteGlobalBind (id, opcode, message) {
    const wlRegistry = this._wlRegistries[id]
    let isRemoteGlobal = false
    if (wlRegistry) {
      const bindOpcode = 0
      if (opcode === bindOpcode) {
        const args = this.parse('usun', message)
        const globalName = args[0]
        this._trackCompositorGlobal(...args)
        isRemoteGlobal = !this._nativeCompositorSession.localGlobalNames.includes(globalName)
      }
    }
    return isRemoteGlobal
  }

  /**
   * @param {Object}wlClient
   * @param {ArrayBuffer}message
   * @param {number}objectId
   * @param {number}opcode
   * @param {boolean}hasNativeResource
   * @returns {number}
   * @private
   */
  _onWireMessageRequest (wlClient, message, objectId, opcode, hasNativeResource) {
    // - If the resource does not exist locally, then the request should only happen remotely.
    // - If the resource exist locally, then the request should only happen locally.
    //  Except:
    //  - If the resource is 'display' and the request is 'get_registry', then the request should be happen both locally & remotely.
    //  - If the resource is 'display' and the request is 'sync', then the request should be happen only remotely.
    //  - If the resource is 'registry' and the request is 'bind' for a locally created global, then the request should only happen locally.
    //  - If the resource is 'registry' and the request is 'bind' for a remotely created global, then the request should only happen remotely.

    // by default, always consume the message & delegate to browser
    let consumed = 1

    // if there is a native resource and the request is not a bind to a remote global or a sync, then there are some special cases that apply.
    if (hasNativeResource &&
      !NativeClientSession._isSync(objectId, opcode) &&
      !this._isRemoteGlobalBind(objectId, opcode, message)) {
      if (NativeClientSession._isGetRegistry(objectId, opcode)) {
        // if the resource is 'display' and the request is 'get_registry', then the request should be happen
        // both locally & in the browser.
        consumed = 0
      } else {
        // if none of the above conditions apply, then let the local resource implementation handle the request
        return 0
      }
    }

    if (consumed) {
      const interceptor = this._interceptors[objectId]
      if (interceptor) {
        interceptor.intercept(message, opcode)
      }
    }

    this._pendingMessageBufferSize += message.byteLength
    this._pendingWireMessages.push(message)

    return consumed
  }

  /**
   * @param {Object}wlClient
   * @param {ArrayBuffer}fdsIn
   */
  _onWireMessageEnd (wlClient, fdsIn) {
    const fdsBufferSize = Uint32Array.BYTES_PER_ELEMENT + (fdsIn ? fdsIn.byteLength : 0)
    const sendBuffer = new Uint32Array(new ArrayBuffer(fdsBufferSize + this._pendingMessageBufferSize))
    let offset = 0
    sendBuffer[0] = fdsIn ? fdsIn.byteLength / Uint32Array.BYTES_PER_ELEMENT : 0
    offset += 1

    if (fdsIn) {
      const fdsArray = new Uint32Array(fdsIn)
      sendBuffer.set(fdsArray)
      offset += fdsArray.length
    }

    this._pendingWireMessages.forEach(value => {
      const wireMessage = new Uint32Array(value)
      sendBuffer.set(wireMessage, offset)
      offset += wireMessage.length
    })

    if (this._dataChannel.readyState === 'open') {
      process.env.DEBUG && console.log(`[app-endpoint-${this._nativeCompositorSession.rtcClient.appEndpointCompositorPair.appEndpointSessionId}] Native client session: forwarding wire messages to browser.`)
      this._dataChannel.send(sendBuffer.buffer)
    } else {
      // queue up data until the channel is open
      process.env.DEBUG && console.log(`[app-endpoint-${this._nativeCompositorSession.rtcClient.appEndpointCompositorPair.appEndpointSessionId}] Native client session: not forwarding wire messages to browser. Channel not open. Queueing.`)
      this._outboundMessages.push(sendBuffer.buffer)
    }

    this._pendingMessageBufferSize = 0
    this._pendingWireMessages = []
  }

  /**
   *
   * @param {string}signature
   * @param {ArrayBuffer}wireMessageBuffer
   * @returns {null|Array<*>}
   */
  parse (signature, wireMessageBuffer) {
    return WireMessageUtil.unmarshallArgs({
      buffer: wireMessageBuffer,
      bufferOffset: 8,
      consumed: 8,
      fds: [],
      size: new Uint32Array(wireMessageBuffer)[1] >>> 16
    }, signature)
  }

  /**
   * @returns {Promise<void>}
   */
  onDestroy () {
    return this._destroyPromise
  }

  destroy () {
    this._destroyResolve()
  }

  /**
   * @param {MessageEvent}event
   * @private
   */
  _onMessage (event) {
    const receiveBuffer = /** @type {ArrayBuffer} */event.data
    this._onWireMessageEvents(receiveBuffer)
  }

  /**
   * @param {RTCErrorEvent}event
   * @private
   */
  _onError (event) {
    // TODO log error
    process.env.DEBUG && console.log(`[app-endpoint-${this._nativeCompositorSession.rtcClient.appEndpointCompositorPair.appEndpointSessionId}] Native client session: RTC data channel is in error ${JSON.stringify(event.error)}.`)
  }

  /**
   * @param {Event}event
   * @private
   */
  _onClose (event) {
    process.env.DEBUG && console.log(`[app-endpoint-${this._nativeCompositorSession.rtcClient.appEndpointCompositorPair.appEndpointSessionId}] Native client session: RTC data channel is closed.`)
    this.destroy()
  }

  /**
   * @param {Object}wlRegistry
   * @param {number}registryId
   * @private
   */
  _onRegistryCreated (wlRegistry, registryId) {
    this._wlRegistries[registryId] = wlRegistry
  }

  /**
   * @param {number}uniqueName
   * @param {string}interfaceName
   * @param {number}version
   * @param {number}objectId
   * @private
   */
  _trackCompositorGlobal (uniqueName, interfaceName, version, objectId) {
    // TODO track compositor resource destruction
    if (interfaceName === 'wl_compositor') {
      this._interceptors[objectId] = CompositorInterceptor.create(this._interceptors)
    }
  }
}

module.exports = NativeClientSession
<|endoftext|>


<commit_before>'use strict';

const http = require('http');
const gpio = require('pi-gpio');

//  Our times
const secTimer = sec(1);
const minTimer = min(15);

console.log('sec', secTimer, 'mainMin', minTimer);

gpio.BCM_GPIO = true;

function cycleOff (pin) {
  gpio.write(pin, 0, () => {
    cycleOn();
  });
};

function cycleOn (pin) {
  setTimout(() => {
    gpio.write(pin, 1, () => {
      done();
    });
  }, 0);
};

function done () {
  conesole.info('Station reset', new Date());
}

const getWeather = () => {
  http.get('http://10.0.0.70', (res, err) => {
      res.setEncoding('utf8');
      res.on('err', (err) => {
          console.log(err.message);
      });

      res.on('data', (data) => {
          let status = JSON.parse(data);
          status = status.connected;

          if (status !== true) {
            cycleOff();
          } else {
              console.log('ok....')
          }
      });
  });
}

const mainTimer = () => {
  const timing = setTimeout(getWeather, mainMin)
}
<commit_msg>finished up script. I think...<commit_after>'use strict';

const http = require('http');
const gpio = require('pi-gpio');

//  Our times
const secTimer = sec(1);
const minTimer = min(15);

const pin = 16;

console.log('sec', secTimer, 'mainMin', minTimer);

function cycleOff () {
  gpio.open(pin, 'output', (err) => {
    gpio.write(pin, 0, () => {
      console.info(`pin ${pin} is off...`);
      gpio.close(pin);
    });
  });
};

function cycleOn () {
  gpio.open(pin, 'output', (err) => {
    gpio.write(pin, 1, () => {
      console.info(`pin ${pin} is on...`);
      gpio.close(pin);
    });
  });
};

const areYouAwake = () => {
  http.get('http://10.0.0.70', (res) => {
    const code = res.statusCode;

    res.setEncoding('utf8');

    if (code !== 200) {
      cycleOff();
      setTimeout(cycleOn, secTimer);
      return;
    }
    console.info(`Station currently up! ${new Date()}`)
  });
}
<|endoftext|>


<commit_before>/**
 * @fileoverview Functions for facilitating the creation of waveforms using the
 *	accompanying waveform.html file.
 *
 * Copyright (c) 2013 Daniel P. Seemuth
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 * @author Daniel P. Seemuth
 * @version 0.1.0
 **/

var PROJ_NAME = 'Waveform Editor';
var VERSION = 'v0.2.0';

var START_SIGNALS = 4;
var START_COLS = 8;

var COLOR_SELECT = 'cyan';

var signals = 0;
var cols = 0;
var table;

var selected = [];

/**
 * Find an object within an array.
 * @param {array} arr Array to search.
 * @param {object} seek Object for which to search.
 * @return {number} Index of found object, or -1 if not found.
 */
function indexOf(arr, seek)
{
    for (var i = 0; i < arr.length; i++) {
	if (arr[i] === seek) {
	    return i;
	}
    }

    return -1;
}

/**
 * @private
 * Create a TextNode with the given string.
 * @param {string} str Text contents for new node.
 * @return {TextNode} Node containing str.
 */
function text_(str)
{
    return document.createTextNode(str);
}

/**
 * @private
 * Generate the default contents for a new cell.
 * @param {number} rowIndex Zero-based table row index of the cell.
 * @param {number} colIndex Zero-based table col index of the cell.
 * @return {string} Text contents for the cell.
 */
function cellContents_(rowIndex, colIndex)
{
    if (rowIndex == 0) {
	/* Header row: show column index except for name column. */
	if (colIndex == 0) {
	    return ' ';
	} else {
	    return colIndex.toString();
	}

    } else if ((rowIndex % 2) == 1) {
	/* Separator row: blank cells. */
	return ' ';

    } else if (colIndex == 0) {
	/* Signal name column. */
	return 'SIG';

    }

    /* Signal data cell. */
    return ' ';
}

/**
 * @private
 * Set the event callbacks for a new cell.
 * @param {cell} cell Set callbacks for this cell.
 * @param {number} rowIndex Zero-based table row index of the cell.
 * @param {number} colIndex Zero-based table col index of the cell.
 */
function setCellEventCallbacks_(cell, rowIndex, colIndex)
{
    if (rowIndex == 0) {
	/* Header row. */

	cell.onclick = cell_click;
	cell.ondblclick = cell_dblclick;

    } else if (((rowIndex - 1) % 2) == 1) {
	/* Signal row. */
	cell.onclick = cell_click;
	cell.ondblclick = cell_dblclick;
    }
}

/**
 * @private
 * Set up a new cell (contents, callbacks, style, etc.).
 * @param {cell} cell Set callbacks for this cell.
 * @param {number} rowIndex Zero-based table row index of the cell.
 * @param {number} colIndex Zero-based table col index of the cell.
 */
function setUpCell_(cell, rowIndex, colIndex)
{
    cell.innerHTML = cellContents_(rowIndex, colIndex);

    setCellEventCallbacks_(cell, rowIndex, colIndex);

    cell.style.borderRight = 'thin dotted black';
}

/**
 * @private
 * Add table row at the given row index.
 * @param {number} rowIndex Add row at this index (0 <= rowIndex <= # rows).
 */
function addRow_(rowIndex)
{
    if (rowIndex < 0) {
	throw 'rowIndex too low';
    }

    var row = table.insertRow(rowIndex);
    row.appendChild(text_('\n'));

    /* Add newline before this row. */
    table.tBodies[0].insertBefore(text_('\n'), row);

    /* Add newline after this row. */
    var nextrow = row.nextSibling;
    if (nextrow == null) {
	table.tBodies[0].appendChild(text_('\n'));
    } else {
	table.tBodies[0].insertBefore(text_('\n'), nextrow);
    }

    /* Add columns. */
    for (var c = 0; c < cols; c++) {
	addCell_(rowIndex, c);
    }
}

/**
 * @private
 * Add table cell at the given row and column indices.
 * @param {number} rowIndex Add cell in this row (0 <= rowIndex < # rows).
 * @param {number} colIndex Add cell in this col (0 <= colIndex <= # cells).
 * @return {cell} Cell that was added to the table.
 */
function addCell_(rowIndex, colIndex)
{
    if (rowIndex < 0) {
	throw 'rowIndex too low';
    }

    if (colIndex < 0) {
	throw 'colIndex too low';
    }

    var row = table.rows[rowIndex];
    var cell = row.insertCell(colIndex);

    setUpCell_(cell, rowIndex, colIndex);

    row.appendChild(text_('\n'));

    return cell;
}

/**
 * @private
 * Convert signal index to table row index.
 * @param {number} sigIndex Zero-based signal index.
 * @return {number} Zero-based table row index.
 */
function sigIndexToRowIndex_(sigIndex)
{
    if (sigIndex < 0) {
	throw 'sigIndex too low';
    }

    /* Row 0 is header row, and 2 rows per signal. */
    return 1 + (sigIndex * 2);
}

/**
 * @private
 * Convert table row index to signal index.
 * @param {number} rowIndex Zero-based table row index.
 * @return {number} Zero-based signal index.
 */
function rowIndexToSigIndex_(rowIndex)
{
    if (rowIndex < 1) {
	throw 'rowIndex too low';
    }

    /* Row 0 is header row, and 2 rows per signal. */
    return Math.floor((rowIndex - 1) / 2);
}

/**
 * @private
 * Find the column index for a cell object.
 * @param {cell} cell Cell whose index to find.
 * @return {number} Zero-based column index.
 */
function cellToColIndex_(cell)
{
    var parent_row = cell.parentNode;

    for (var i = 0; i < parent_row.cells.length; i++) {
	if (cell === parent_row.cells[i]) {
	    return i;
	}
    }
    return -1;
}

/**
 * @private
 * Find the row index for a row object.
 * @param {row} row Row whose index to find.
 * @return {number} Zero-based row index.
 */
function rowToRowIndex_(row)
{
    var parent_table = row.parentNode;

    for (var i = 0; i < parent_table.rows.length; i++) {
	if (row === parent_table.rows[i]) {
	    return i;
	}
    }
    return -1;
}

/**
 * @private
 * Find the cell at the given table coordinates.
 * @param {number} rowIndex Zero-based row index.
 * @param {number} colIndex Zero-based column index.
 * @return {cell} Cell at the given coordinates.
 */
function tableCoordsToCell_(rowIndex, colIndex)
{
    return table.rows[rowIndex].cells[colIndex];
}

/**
 * Add signal at the given index.
 * @param {number} index Add signal at this index (negative means last).
 */
function addSignal(index)
{
    if (index < 0) {
	index = signals;
    }

    var rowIndex = sigIndexToRowIndex_(index);

    /* Add separator row and signal row. */
    for (var i = 0; i < 2; i++) {
	addRow_(rowIndex + i);
    }

    signals++;
}

/**
 * Delete the signal at the given index.
 * @param {number} index Delete signal at this index (negative means last).
 */
function delSignal(index)
{
    if (signals < 1) {
	/* No signals to delete! */
	return;
    }

    signals--;

    if (index < 0) {
	index = signals;
    }

    var rowIndex = sigIndexToRowIndex_(index);

    for (var i = 0; i < 2; i++) {
	table.deleteRow(rowIndex);
    }
}

/**
 * Add column at the given index.
 * @param {number} index Add column at this index (negative means last).
 */
function addCol(index)
{
    if (index > cols) {
	throw 'index too high';
    }

    if (index < 0) {
	index = cols;
    }

    var regenHeader = (index < cols);	/* Need to regenerate header? */

    var colIndex = index;

    for (var r = 0; r < table.rows.length; r++) {
	addCell_(r, colIndex);
    }

    cols++;

    if (regenHeader) {
	var row = table.rows[0];

	for (var c = 1; c < cols; c++) {
	    row.cells[c].innerHTML = cellContents_(0, c);
	}
    }
}

/**
 * Delete the column at the given index.
 * @param {number} index Delete column at this index (negative means last).
 */
function delCol(index)
{
    if (cols <= 1) {
	/* No non-name columns to delete! */
	return;
    }

    if (index >= cols) {
	throw 'index too high';
    }

    var regenHeader = (index < (cols - 1)); /* Need to regenerate header? */

    cols--;

    if (index < 0) {
	index = cols;
    }

    var colIndex = index;

    for (var r = 0; r < table.rows.length; r++) {
	var row = table.rows[r];

	row.deleteCell(colIndex);
    }

    if (regenHeader) {
	var row = table.rows[0];

	for (var c = 1; c < cols; c++) {
	    row.cells[c].innerHTML = cellContents_(0, c);
	}
    }
}

/**
 * @private
 * Enable/show or disable/hide signal editing buttons.
 * @param {bool} enable True to enable, false to disable.
 */
function enableSigEdit_(enable)
{
    var sigEdit = document.getElementById('sigEdit');

    for (var i = 0; i < sigEdit.childNodes.length; i++) {
	var n = sigEdit.childNodes[i];

	if (n.nodeName.toLowerCase() == 'button') {
	    n.disabled = ! enable;
	}
    }

    sigEdit.style.visibility = (enable) ? 'visible' : 'hidden';
}

/**
 * Clear selection and unhighlight cells.
 */
function clearSelection_()
{
    for (var i = 0; i < selected.length; i++) {
	var parts = selected[i].split('x');
	var rowIndex = new Number(parts[0]);
	var colIndex = new Number(parts[1]);
	var cell = tableCoordsToCell_(rowIndex, colIndex);

	cell.style.backgroundColor = '';
    }

    selected = [];
    enableSigEdit_(false);
}

/**
 * @private
 * Toggle cell selection.
 * @param {number} rowIndex Zero-based row index.
 * @param {number} colIndex Zero-based column index.
 * @param {string} mode Set/clear/toggle selection.
 */
function setCellSelection_(rowIndex, colIndex, mode)
{
    var cellKey = rowIndex.toString().concat('x', colIndex.toString());
    var index = indexOf(selected, cellKey);
    var cell = tableCoordsToCell_(rowIndex, colIndex);

    var sel_set = false;
    var sel_clear = false;

    mode = mode.trim().charAt(0);

    if (mode == 's') {
	sel_set = true;

    } else if (mode == 'c') {
	sel_clear = true;

    } else if (mode == 't') {
	sel_set = (index < 0);
	sel_clear = ! sel_set;

    } else {
	throw 'invalid mode';
    }

    if (sel_set) {
	/* Select this cell if it isn't already selected. */
	if (index < 0) {
	    selected.push(cellKey);

	    cell.style.backgroundColor = COLOR_SELECT;
	}
    }

    if (sel_clear) {
	/* Deselect this cell if it is selected. */
	if (index >= 0) {
	    selected.splice(index, 1);

	    cell.style.backgroundColor = '';
	}
    }

    enableSigEdit_(selected.length > 0);
}

/**
 * Initialize the editor and waveform grid.
 */
function init()
{
    var version_paragraph = document.getElementById('version');

    version_paragraph.innerHTML = PROJ_NAME.concat(' ', VERSION);

    table = document.getElementById('wftable');

    /* Pre-populate the table. */

    if (table.tBodies.length < 1) {
	table.appendChild(document.createElement('tbody'));
    }

    for (var i = 0; i < START_COLS; i++) {
	addCol(-1);
    }

    /* Add header row. */
    addRow_(0);

    for (var i = 0; i < START_SIGNALS; i++) {
	addSignal(-1);
    }
}

/**
 * Copy the table's HTML into the I/O textarea so the user can copy/paste.
 */
function exportHTML()
{
    var io = document.getElementById('io');
    var text = table.innerHTML.trim().replace(/\n+/g, '\n');
    io.value = text;
}

/**
 * Handle single-click event on a cell.
 */
function cell_click(event)
{
    var cell = event.currentTarget;
    var rowIndex = rowToRowIndex_(cell.parentNode);
    var colIndex = cellToColIndex_(cell);
    var modifier = (event.altKey || event.ctrlKey || event.shiftKey);

    if (! modifier) {
	/* Clear other selection and select the chosen cell(s). */
	clearSelection_();
    }

    if ((colIndex == 0) && (rowIndex == 0)) {
	/* Just clear selection. */
	return;

    } else if (colIndex == 0) {
	/* Select whole row. */

	for (var ci = 1; ci < cols; ci++) {
	    setCellSelection_(rowIndex, ci, 's');
	}

    } else if (rowIndex == 0) {
	/* Select whole column. */

	for (var si = 0; si < signals; si++) {
	    var ri = sigIndexToRowIndex_(si) + 1;
	    setCellSelection_(ri, colIndex, 's');
	}

    } else {
	/* Toggle this cell's selection. */
	setCellSelection_(rowIndex, colIndex, 't');
    }
}

/**
 * Handle double-click event on a cell.
 */
function cell_dblclick(event)
{
    var cell = event.currentTarget;

    if (cell.style.backgroundColor == 'blue') {
	cell.style.backgroundColor = '';
    } else {
	cell.style.backgroundColor = 'blue';
    }
}
<commit_msg>Clear selection when changing table size<commit_after>/**
 * @fileoverview Functions for facilitating the creation of waveforms using the
 *	accompanying waveform.html file.
 *
 * Copyright (c) 2013 Daniel P. Seemuth
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 * @author Daniel P. Seemuth
 * @version 0.1.0
 **/

var PROJ_NAME = 'Waveform Editor';
var VERSION = 'v0.2.0';

var START_SIGNALS = 4;
var START_COLS = 8;

var COLOR_SELECT = 'cyan';

var signals = 0;
var cols = 0;
var table;

var selected = [];

/**
 * Find an object within an array.
 * @param {array} arr Array to search.
 * @param {object} seek Object for which to search.
 * @return {number} Index of found object, or -1 if not found.
 */
function indexOf(arr, seek)
{
    for (var i = 0; i < arr.length; i++) {
	if (arr[i] === seek) {
	    return i;
	}
    }

    return -1;
}

/**
 * @private
 * Create a TextNode with the given string.
 * @param {string} str Text contents for new node.
 * @return {TextNode} Node containing str.
 */
function text_(str)
{
    return document.createTextNode(str);
}

/**
 * @private
 * Generate the default contents for a new cell.
 * @param {number} rowIndex Zero-based table row index of the cell.
 * @param {number} colIndex Zero-based table col index of the cell.
 * @return {string} Text contents for the cell.
 */
function cellContents_(rowIndex, colIndex)
{
    if (rowIndex == 0) {
	/* Header row: show column index except for name column. */
	if (colIndex == 0) {
	    return ' ';
	} else {
	    return colIndex.toString();
	}

    } else if ((rowIndex % 2) == 1) {
	/* Separator row: blank cells. */
	return ' ';

    } else if (colIndex == 0) {
	/* Signal name column. */
	return 'SIG';

    }

    /* Signal data cell. */
    return ' ';
}

/**
 * @private
 * Set the event callbacks for a new cell.
 * @param {cell} cell Set callbacks for this cell.
 * @param {number} rowIndex Zero-based table row index of the cell.
 * @param {number} colIndex Zero-based table col index of the cell.
 */
function setCellEventCallbacks_(cell, rowIndex, colIndex)
{
    if (rowIndex == 0) {
	/* Header row. */

	cell.onclick = cell_click;
	cell.ondblclick = cell_dblclick;

    } else if (((rowIndex - 1) % 2) == 1) {
	/* Signal row. */
	cell.onclick = cell_click;
	cell.ondblclick = cell_dblclick;
    }
}

/**
 * @private
 * Set up a new cell (contents, callbacks, style, etc.).
 * @param {cell} cell Set callbacks for this cell.
 * @param {number} rowIndex Zero-based table row index of the cell.
 * @param {number} colIndex Zero-based table col index of the cell.
 */
function setUpCell_(cell, rowIndex, colIndex)
{
    cell.innerHTML = cellContents_(rowIndex, colIndex);

    setCellEventCallbacks_(cell, rowIndex, colIndex);

    cell.style.borderRight = 'thin dotted black';
}

/**
 * @private
 * Add table row at the given row index.
 * @param {number} rowIndex Add row at this index (0 <= rowIndex <= # rows).
 */
function addRow_(rowIndex)
{
    if (rowIndex < 0) {
	throw 'rowIndex too low';
    }

    var row = table.insertRow(rowIndex);
    row.appendChild(text_('\n'));

    /* Add newline before this row. */
    table.tBodies[0].insertBefore(text_('\n'), row);

    /* Add newline after this row. */
    var nextrow = row.nextSibling;
    if (nextrow == null) {
	table.tBodies[0].appendChild(text_('\n'));
    } else {
	table.tBodies[0].insertBefore(text_('\n'), nextrow);
    }

    /* Add columns. */
    for (var c = 0; c < cols; c++) {
	addCell_(rowIndex, c);
    }
}

/**
 * @private
 * Add table cell at the given row and column indices.
 * @param {number} rowIndex Add cell in this row (0 <= rowIndex < # rows).
 * @param {number} colIndex Add cell in this col (0 <= colIndex <= # cells).
 * @return {cell} Cell that was added to the table.
 */
function addCell_(rowIndex, colIndex)
{
    if (rowIndex < 0) {
	throw 'rowIndex too low';
    }

    if (colIndex < 0) {
	throw 'colIndex too low';
    }

    var row = table.rows[rowIndex];
    var cell = row.insertCell(colIndex);

    setUpCell_(cell, rowIndex, colIndex);

    row.appendChild(text_('\n'));

    return cell;
}

/**
 * @private
 * Convert signal index to table row index.
 * @param {number} sigIndex Zero-based signal index.
 * @return {number} Zero-based table row index.
 */
function sigIndexToRowIndex_(sigIndex)
{
    if (sigIndex < 0) {
	throw 'sigIndex too low';
    }

    /* Row 0 is header row, and 2 rows per signal. */
    return 1 + (sigIndex * 2);
}

/**
 * @private
 * Convert table row index to signal index.
 * @param {number} rowIndex Zero-based table row index.
 * @return {number} Zero-based signal index.
 */
function rowIndexToSigIndex_(rowIndex)
{
    if (rowIndex < 1) {
	throw 'rowIndex too low';
    }

    /* Row 0 is header row, and 2 rows per signal. */
    return Math.floor((rowIndex - 1) / 2);
}

/**
 * @private
 * Find the column index for a cell object.
 * @param {cell} cell Cell whose index to find.
 * @return {number} Zero-based column index.
 */
function cellToColIndex_(cell)
{
    var parent_row = cell.parentNode;

    for (var i = 0; i < parent_row.cells.length; i++) {
	if (cell === parent_row.cells[i]) {
	    return i;
	}
    }
    return -1;
}

/**
 * @private
 * Find the row index for a row object.
 * @param {row} row Row whose index to find.
 * @return {number} Zero-based row index.
 */
function rowToRowIndex_(row)
{
    var parent_table = row.parentNode;

    for (var i = 0; i < parent_table.rows.length; i++) {
	if (row === parent_table.rows[i]) {
	    return i;
	}
    }
    return -1;
}

/**
 * @private
 * Find the cell at the given table coordinates.
 * @param {number} rowIndex Zero-based row index.
 * @param {number} colIndex Zero-based column index.
 * @return {cell} Cell at the given coordinates.
 */
function tableCoordsToCell_(rowIndex, colIndex)
{
    return table.rows[rowIndex].cells[colIndex];
}

/**
 * Add signal at the given index.
 * @param {number} index Add signal at this index (negative means last).
 */
function addSignal(index)
{
    clearSelection_();

    if (index < 0) {
	index = signals;
    }

    var rowIndex = sigIndexToRowIndex_(index);

    /* Add separator row and signal row. */
    for (var i = 0; i < 2; i++) {
	addRow_(rowIndex + i);
    }

    signals++;
}

/**
 * Delete the signal at the given index.
 * @param {number} index Delete signal at this index (negative means last).
 */
function delSignal(index)
{
    clearSelection_();

    if (signals < 1) {
	/* No signals to delete! */
	return;
    }

    signals--;

    if (index < 0) {
	index = signals;
    }

    var rowIndex = sigIndexToRowIndex_(index);

    for (var i = 0; i < 2; i++) {
	table.deleteRow(rowIndex);
    }
}

/**
 * Add column at the given index.
 * @param {number} index Add column at this index (negative means last).
 */
function addCol(index)
{
    clearSelection_();

    if (index > cols) {
	throw 'index too high';
    }

    if (index < 0) {
	index = cols;
    }

    var regenHeader = (index < cols);	/* Need to regenerate header? */

    var colIndex = index;

    for (var r = 0; r < table.rows.length; r++) {
	addCell_(r, colIndex);
    }

    cols++;

    if (regenHeader) {
	var row = table.rows[0];

	for (var c = 1; c < cols; c++) {
	    row.cells[c].innerHTML = cellContents_(0, c);
	}
    }
}

/**
 * Delete the column at the given index.
 * @param {number} index Delete column at this index (negative means last).
 */
function delCol(index)
{
    clearSelection_();

    if (cols <= 1) {
	/* No non-name columns to delete! */
	return;
    }

    if (index >= cols) {
	throw 'index too high';
    }

    var regenHeader = (index < (cols - 1)); /* Need to regenerate header? */

    cols--;

    if (index < 0) {
	index = cols;
    }

    var colIndex = index;

    for (var r = 0; r < table.rows.length; r++) {
	var row = table.rows[r];

	row.deleteCell(colIndex);
    }

    if (regenHeader) {
	var row = table.rows[0];

	for (var c = 1; c < cols; c++) {
	    row.cells[c].innerHTML = cellContents_(0, c);
	}
    }
}

/**
 * @private
 * Enable/show or disable/hide signal editing buttons.
 * @param {bool} enable True to enable, false to disable.
 */
function enableSigEdit_(enable)
{
    var sigEdit = document.getElementById('sigEdit');

    for (var i = 0; i < sigEdit.childNodes.length; i++) {
	var n = sigEdit.childNodes[i];

	if (n.nodeName.toLowerCase() == 'button') {
	    n.disabled = ! enable;
	}
    }

    sigEdit.style.visibility = (enable) ? 'visible' : 'hidden';
}

/**
 * Clear selection and unhighlight cells.
 */
function clearSelection_()
{
    for (var i = 0; i < selected.length; i++) {
	var parts = selected[i].split('x');
	var rowIndex = new Number(parts[0]);
	var colIndex = new Number(parts[1]);
	var cell = tableCoordsToCell_(rowIndex, colIndex);

	cell.style.backgroundColor = '';
    }

    selected = [];
    enableSigEdit_(false);
}

/**
 * @private
 * Toggle cell selection.
 * @param {number} rowIndex Zero-based row index.
 * @param {number} colIndex Zero-based column index.
 * @param {string} mode Set/clear/toggle selection.
 */
function setCellSelection_(rowIndex, colIndex, mode)
{
    var cellKey = rowIndex.toString().concat('x', colIndex.toString());
    var index = indexOf(selected, cellKey);
    var cell = tableCoordsToCell_(rowIndex, colIndex);

    var sel_set = false;
    var sel_clear = false;

    mode = mode.trim().charAt(0);

    if (mode == 's') {
	sel_set = true;

    } else if (mode == 'c') {
	sel_clear = true;

    } else if (mode == 't') {
	sel_set = (index < 0);
	sel_clear = ! sel_set;

    } else {
	throw 'invalid mode';
    }

    if (sel_set) {
	/* Select this cell if it isn't already selected. */
	if (index < 0) {
	    selected.push(cellKey);

	    cell.style.backgroundColor = COLOR_SELECT;
	}
    }

    if (sel_clear) {
	/* Deselect this cell if it is selected. */
	if (index >= 0) {
	    selected.splice(index, 1);

	    cell.style.backgroundColor = '';
	}
    }

    enableSigEdit_(selected.length > 0);
}

/**
 * Initialize the editor and waveform grid.
 */
function init()
{
    var version_paragraph = document.getElementById('version');

    version_paragraph.innerHTML = PROJ_NAME.concat(' ', VERSION);

    table = document.getElementById('wftable');

    /* Pre-populate the table. */

    if (table.tBodies.length < 1) {
	table.appendChild(document.createElement('tbody'));
    }

    for (var i = 0; i < START_COLS; i++) {
	addCol(-1);
    }

    /* Add header row. */
    addRow_(0);

    for (var i = 0; i < START_SIGNALS; i++) {
	addSignal(-1);
    }
}

/**
 * Copy the table's HTML into the I/O textarea so the user can copy/paste.
 */
function exportHTML()
{
    var io = document.getElementById('io');
    var text = table.innerHTML.trim().replace(/\n+/g, '\n');
    io.value = text;
}

/**
 * Handle single-click event on a cell.
 */
function cell_click(event)
{
    var cell = event.currentTarget;
    var rowIndex = rowToRowIndex_(cell.parentNode);
    var colIndex = cellToColIndex_(cell);
    var modifier = (event.altKey || event.ctrlKey || event.shiftKey);

    if (! modifier) {
	/* Clear other selection and select the chosen cell(s). */
	clearSelection_();
    }

    if ((colIndex == 0) && (rowIndex == 0)) {
	/* Just clear selection. */
	return;

    } else if (colIndex == 0) {
	/* Select whole row. */

	for (var ci = 1; ci < cols; ci++) {
	    setCellSelection_(rowIndex, ci, 's');
	}

    } else if (rowIndex == 0) {
	/* Select whole column. */

	for (var si = 0; si < signals; si++) {
	    var ri = sigIndexToRowIndex_(si) + 1;
	    setCellSelection_(ri, colIndex, 's');
	}

    } else {
	/* Toggle this cell's selection. */
	setCellSelection_(rowIndex, colIndex, 't');
    }
}

/**
 * Handle double-click event on a cell.
 */
function cell_dblclick(event)
{
    var cell = event.currentTarget;

    if (cell.style.backgroundColor == 'blue') {
	cell.style.backgroundColor = '';
    } else {
	cell.style.backgroundColor = 'blue';
    }
}
<|endoftext|>


<commit_before>app.run(function($rootScope) {
	$rootScope.user = {
		settings: {
			tts: {
				enabled: true,
				countdown: true,
				voice: 0
			}
		}
	};

	$rootScope.timers = [
		/*
		{
			id: String // Unique ID of this timer (used for URL slug)
			title: String // Human friendly name of the timer profile
			description: String // UNSUPPORTED Description of the routine
			routineTime: String // UNSUPPORTED Time the routine should be run for (e.g. '1 week')
			script: [ // Array of script items
				{
					title: String // The display title of the item
					time: Int // Time in seconds to display
					say: Bool|String // If true the title will be said via TTS, if a string it will be passed to TTS
					countdown: Bool // (Default: true), use TTS to announce the countdown
				{
			]
		}
		*/

		{
			id: 'tennisElbow',
			title: 'Tennis Elbow Session 1',
			description: 'Simple Tennis Elbow stretches with the aid of a rubber band. Designed to work with a arm at 90 degrees bend.',
			routineTime: '1 week',
			script: [
				{
					title: 'Preperation',
					time: 5 * 1000,
				},
				{
					title: 'Right arm stress #1',
					time: 60 * 1000,
				},
				{
					title: 'Rest',
					time: 60 * 1000,
					say: true
				},
				{
					title: 'Right arm stress #2',
					time: 60 * 1000,
				},
				{
					title: 'Rest',
					time: 60 * 1000,
					say: true
				},
				{
					title: 'Right arm stress #3',
					time: 60 * 1000,
				},
				{
					title: 'Rest',
					time: 60 * 1000,
					say: true
				},
				{
					title: 'Right arm stress #4',
					time: 60 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 10 * 1000,
					say: true
				},
				{
					title: 'Left arm stress #1',
					time: 60 * 1000,
				},
				{
					title: 'Rest',
					time: 60 * 1000,
					say: true
				},
				{
					title: 'Left arm stress #2',
					time: 60 * 1000,
				},
				{
					title: 'Rest',
					time: 60 * 1000,
					say: true
				},
				{
					title: 'Left arm stress #3',
					time: 60 * 1000,
				},
				{
					title: 'Rest',
					time: 60 * 1000,
					say: true
				},
				{
					title: 'Left arm stress #4',
					time: 60 * 1000,
				},
				{
					title: 'Finished',
					say: true,
					countdown: false
				}
			]
		},
		{
			id: 'tennisElbow1Compressed',
			title: 'Tennis Elbow Session 1 (Alternating)',
			description: 'Alternating Tennis Elbow stretches desgigned to reduce the time of the routine. Designed to work with a arm at 90 degrees bend.',
			routineTime: '1 week',
			script: [
				{
					title: 'Preperation',
					time: 5 * 1000,
				},
				{
					title: 'Right arm stress #1',
					time: 60 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 10 * 1000,
					say: true
				},
				{
					title: 'Left arm stress #1',
					time: 60 * 1000,
				},
				{
					title: 'Switch to right arm',
					time: 10 * 1000,
					say: true
				},
				{
					title: 'Right arm stress #2',
					time: 60 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 10 * 1000,
					say: true
				},
				{
					title: 'Left arm stress #2',
					time: 60 * 1000,
				},
				{
					title: 'Switch to right arm',
					time: 10 * 1000,
					say: true
				},
				{
					title: 'Right arm stress #3',
					time: 60 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 10 * 1000,
					say: true
				},
				{
					title: 'Left arm stress #3',
					time: 60 * 1000,
				},
				{
					title: 'Switch to right arm',
					time: 10 * 1000,
					say: true
				},
				{
					title: 'Right arm stress #4',
					time: 60 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 10 * 1000,
					say: true
				},
				{
					title: 'Left arm stress #4',
					time: 60 * 1000,
				},
				{
					title: 'Finished',
					say: true,
					countdown: false
				}
			]
		},
		{
			id: 'tennisElbow2',
			title: 'Tennis Elbow Session 2',
			description: 'Straight arm based Tennis Elbow exsorsizes. Stretch arm in \'stopping motion\'.',
			routineTime: '10 days',
			script: [
				{
					title: 'Preperation',
					time: 5 * 1000,
				},
				{
					title: 'Right arm stress #1',
					time: 45 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 10 * 1000,
					say: true
				},
				{
					title: 'Left arm stress #1',
					time: 45 * 1000,
				},
				{
					title: 'Switch to right arm',
					time: 10 * 1000,
					say: true
				},
				{
					title: 'Right arm stress #2',
					time: 45 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 10 * 1000,
					say: true
				},
				{
					title: 'Left arm stress #2',
					time: 45 * 1000,
				},
				{
					title: 'Switch to right arm',
					time: 10 * 1000,
					say: true
				},
				{
					title: 'Right arm stress #3',
					time: 45 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 10 * 1000,
					say: true
				},
				{
					title: 'Left arm stress #3',
					time: 45 * 1000,
				},
				{
					title: 'Switch to right arm',
					time: 10 * 1000,
					say: true
				},
				{
					title: 'Right arm stress #4',
					time: 45 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 10 * 1000,
					say: true
				},
				{
					title: 'Left arm stress #4',
					time: 45 * 1000,
				},
				{
					title: 'Finished',
					say: true,
					countdown: false
				}
			]
		},
		{
			id: 'tennisElbow3',
			title: 'Tennis Elbow Session 3 (60s)',
			description: '5kg Weight holding exorcizes, twice a day',
			routineTime: '14 days',
			script: [
				{
					title: 'Preperation',
					time: 5 * 1000,
				},
				{
					title: 'Right arm weights #1',
					time: 60 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 5 * 1000,
					say: true
				},
				{
					title: 'Left arm weights #1',
					time: 60 * 1000,
				},
				{
					title: 'Switch to right arm',
					time: 5 * 1000,
					say: true
				},
				{
					title: 'Right arm weights #2',
					time: 60 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 5 * 1000,
					say: true
				},
				{
					title: 'Left arm weights #2',
					time: 60 * 1000,
				},
				{
					title: 'Switch to right arm',
					time: 5 * 1000,
					say: true
				},
				{
					title: 'Right arm weights #3',
					time: 60 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 5 * 1000,
					say: true
				},
				{
					title: 'Left arm weights #3',
					time: 60 * 1000,
				},
				{
					title: 'Switch to right arm',
					time: 5 * 1000,
					say: true
				},
				{
					title: 'Right arm weights #4',
					time: 60 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 5 * 1000,
					say: true
				},
				{
					title: 'Left arm weights #4',
					time: 60 * 1000,
				},
				{
					title: 'Finished',
					say: true,
					countdown: false
				}
			]
		},
		{
			id: 'tennisElbow3-45s',
			title: 'Tennis Elbow Session 3 (45s)',
			description: '5kg Weight holding exorcizes, twice a day with lighter 45 second intervals',
			routineTime: '14 days',
			script: [
				{
					title: 'Preperation',
					time: 5 * 1000,
				},
				{
					title: 'Right arm weights #1',
					time: 45 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 5 * 1000,
					say: true
				},
				{
					title: 'Left arm weights #1',
					time: 45 * 1000,
				},
				{
					title: 'Switch to right arm',
					time: 5 * 1000,
					say: true
				},
				{
					title: 'Right arm weights #2',
					time: 45 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 5 * 1000,
					say: true
				},
				{
					title: 'Left arm weights #2',
					time: 45 * 1000,
				},
				{
					title: 'Switch to right arm',
					time: 5 * 1000,
					say: true
				},
				{
					title: 'Right arm weights #3',
					time: 45 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 5 * 1000,
					say: true
				},
				{
					title: 'Left arm weights #3',
					time: 45 * 1000,
				},
				{
					title: 'Switch to right arm',
					time: 5 * 1000,
					say: true
				},
				{
					title: 'Right arm weights #4',
					time: 45 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 5 * 1000,
					say: true
				},
				{
					title: 'Left arm weights #4',
					time: 45 * 1000,
				},
				{
					title: 'Finished',
					say: true,
					countdown: false
				}
			]
		}
	];
});
<commit_msg>Added 60s weights<commit_after>app.run(function($rootScope) {
	$rootScope.user = {
		settings: {
			tts: {
				enabled: true,
				countdown: true,
				voice: 0
			}
		}
	};

	$rootScope.timers = [
		/*
		{
			id: String // Unique ID of this timer (used for URL slug)
			title: String // Human friendly name of the timer profile
			description: String // UNSUPPORTED Description of the routine
			routineTime: String // UNSUPPORTED Time the routine should be run for (e.g. '1 week')
			script: [ // Array of script items
				{
					title: String // The display title of the item
					time: Int // Time in seconds to display
					say: Bool|String // If true the title will be said via TTS, if a string it will be passed to TTS
					countdown: Bool // (Default: true), use TTS to announce the countdown
				{
			]
		}
		*/

		{
			id: 'tennisElbow',
			title: 'Tennis Elbow Session 1',
			description: 'Simple Tennis Elbow stretches with the aid of a rubber band. Designed to work with a arm at 90 degrees bend.',
			routineTime: '1 week',
			script: [
				{
					title: 'Preperation',
					time: 5 * 1000,
				},
				{
					title: 'Right arm stress #1',
					time: 60 * 1000,
				},
				{
					title: 'Rest',
					time: 60 * 1000,
					say: true
				},
				{
					title: 'Right arm stress #2',
					time: 60 * 1000,
				},
				{
					title: 'Rest',
					time: 60 * 1000,
					say: true
				},
				{
					title: 'Right arm stress #3',
					time: 60 * 1000,
				},
				{
					title: 'Rest',
					time: 60 * 1000,
					say: true
				},
				{
					title: 'Right arm stress #4',
					time: 60 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 10 * 1000,
					say: true
				},
				{
					title: 'Left arm stress #1',
					time: 60 * 1000,
				},
				{
					title: 'Rest',
					time: 60 * 1000,
					say: true
				},
				{
					title: 'Left arm stress #2',
					time: 60 * 1000,
				},
				{
					title: 'Rest',
					time: 60 * 1000,
					say: true
				},
				{
					title: 'Left arm stress #3',
					time: 60 * 1000,
				},
				{
					title: 'Rest',
					time: 60 * 1000,
					say: true
				},
				{
					title: 'Left arm stress #4',
					time: 60 * 1000,
				},
				{
					title: 'Finished',
					say: true,
					countdown: false
				}
			]
		},
		{
			id: 'tennisElbow1Compressed',
			title: 'Tennis Elbow Session 1 (Alternating)',
			description: 'Alternating Tennis Elbow stretches desgigned to reduce the time of the routine. Designed to work with a arm at 90 degrees bend.',
			routineTime: '1 week',
			script: [
				{
					title: 'Preperation',
					time: 5 * 1000,
				},
				{
					title: 'Right arm stress #1',
					time: 60 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 10 * 1000,
					say: true
				},
				{
					title: 'Left arm stress #1',
					time: 60 * 1000,
				},
				{
					title: 'Switch to right arm',
					time: 10 * 1000,
					say: true
				},
				{
					title: 'Right arm stress #2',
					time: 60 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 10 * 1000,
					say: true
				},
				{
					title: 'Left arm stress #2',
					time: 60 * 1000,
				},
				{
					title: 'Switch to right arm',
					time: 10 * 1000,
					say: true
				},
				{
					title: 'Right arm stress #3',
					time: 60 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 10 * 1000,
					say: true
				},
				{
					title: 'Left arm stress #3',
					time: 60 * 1000,
				},
				{
					title: 'Switch to right arm',
					time: 10 * 1000,
					say: true
				},
				{
					title: 'Right arm stress #4',
					time: 60 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 10 * 1000,
					say: true
				},
				{
					title: 'Left arm stress #4',
					time: 60 * 1000,
				},
				{
					title: 'Finished',
					say: true,
					countdown: false
				}
			]
		},
		{
			id: 'tennisElbow2',
			title: 'Tennis Elbow Session 2',
			description: 'Straight arm based Tennis Elbow exsorsizes. Stretch arm in \'stopping motion\'.',
			routineTime: '10 days',
			script: [
				{
					title: 'Preperation',
					time: 5 * 1000,
				},
				{
					title: 'Right arm stress #1',
					time: 45 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 10 * 1000,
					say: true
				},
				{
					title: 'Left arm stress #1',
					time: 45 * 1000,
				},
				{
					title: 'Switch to right arm',
					time: 10 * 1000,
					say: true
				},
				{
					title: 'Right arm stress #2',
					time: 45 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 10 * 1000,
					say: true
				},
				{
					title: 'Left arm stress #2',
					time: 45 * 1000,
				},
				{
					title: 'Switch to right arm',
					time: 10 * 1000,
					say: true
				},
				{
					title: 'Right arm stress #3',
					time: 45 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 10 * 1000,
					say: true
				},
				{
					title: 'Left arm stress #3',
					time: 45 * 1000,
				},
				{
					title: 'Switch to right arm',
					time: 10 * 1000,
					say: true
				},
				{
					title: 'Right arm stress #4',
					time: 45 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 10 * 1000,
					say: true
				},
				{
					title: 'Left arm stress #4',
					time: 45 * 1000,
				},
				{
					title: 'Finished',
					say: true,
					countdown: false
				}
			]
		},
		{
			id: 'tennisElbow3',
			title: 'Tennis Elbow Session 3 (60s)',
			description: '5kg Weight holding exorcizes, twice a day',
			routineTime: '14 days',
			script: [
				{
					title: 'Preperation',
					time: 5 * 1000,
				},
				{
					title: 'Right arm weights #1',
					time: 60 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 5 * 1000,
					say: true
				},
				{
					title: 'Left arm weights #1',
					time: 60 * 1000,
				},
				{
					title: 'Switch to right arm',
					time: 5 * 1000,
					say: true
				},
				{
					title: 'Right arm weights #2',
					time: 60 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 5 * 1000,
					say: true
				},
				{
					title: 'Left arm weights #2',
					time: 60 * 1000,
				},
				{
					title: 'Switch to right arm',
					time: 5 * 1000,
					say: true
				},
				{
					title: 'Right arm weights #3',
					time: 60 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 5 * 1000,
					say: true
				},
				{
					title: 'Left arm weights #3',
					time: 60 * 1000,
				},
				{
					title: 'Switch to right arm',
					time: 5 * 1000,
					say: true
				},
				{
					title: 'Right arm weights #4',
					time: 60 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 5 * 1000,
					say: true
				},
				{
					title: 'Left arm weights #4',
					time: 60 * 1000,
				},
				{
					title: 'Finished',
					say: true,
					countdown: false
				}
			]
		},
		{
			id: 'tennisElbow3-45s',
			title: 'Tennis Elbow Session 3 (45s)',
			description: '5kg Weight holding exorcizes, twice a day with lighter 45 second intervals',
			routineTime: '14 days',
			script: [
				{
					title: 'Preperation',
					time: 5 * 1000,
				},
				{
					title: 'Right arm weights #1',
					time: 45 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 5 * 1000,
					say: true
				},
				{
					title: 'Left arm weights #1',
					time: 45 * 1000,
				},
				{
					title: 'Switch to right arm',
					time: 5 * 1000,
					say: true
				},
				{
					title: 'Right arm weights #2',
					time: 45 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 5 * 1000,
					say: true
				},
				{
					title: 'Left arm weights #2',
					time: 45 * 1000,
				},
				{
					title: 'Switch to right arm',
					time: 5 * 1000,
					say: true
				},
				{
					title: 'Right arm weights #3',
					time: 45 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 5 * 1000,
					say: true
				},
				{
					title: 'Left arm weights #3',
					time: 45 * 1000,
				},
				{
					title: 'Switch to right arm',
					time: 5 * 1000,
					say: true
				},
				{
					title: 'Right arm weights #4',
					time: 45 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 5 * 1000,
					say: true
				},
				{
					title: 'Left arm weights #4',
					time: 45 * 1000,
				},
				{
					title: 'Finished',
					say: true,
					countdown: false
				}
			]
		},
		{
			id: 'tennisElbow3-60s',
			title: 'Tennis Elbow Session 3 (60s)',
			description: '5kg Weight holding exorcizes, twice a day with 60 second intervals',
			routineTime: '14 days',
			script: [
				{
					title: 'Preperation',
					time: 5 * 1000,
				},
				{
					title: 'Right arm weights #1',
					time: 60 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 5 * 1000,
					say: true
				},
				{
					title: 'Left arm weights #1',
					time: 60 * 1000,
				},
				{
					title: 'Switch to right arm',
					time: 5 * 1000,
					say: true
				},
				{
					title: 'Right arm weights #2',
					time: 60 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 5 * 1000,
					say: true
				},
				{
					title: 'Left arm weights #2',
					time: 60 * 1000,
				},
				{
					title: 'Switch to right arm',
					time: 5 * 1000,
					say: true
				},
				{
					title: 'Right arm weights #3',
					time: 60 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 5 * 1000,
					say: true
				},
				{
					title: 'Left arm weights #3',
					time: 60 * 1000,
				},
				{
					title: 'Switch to right arm',
					time: 5 * 1000,
					say: true
				},
				{
					title: 'Right arm weights #4',
					time: 60 * 1000,
				},
				{
					title: 'Switch to left arm',
					time: 5 * 1000,
					say: true
				},
				{
					title: 'Left arm weights #4',
					time: 60 * 1000,
				},
				{
					title: 'Finished',
					say: true,
					countdown: false
				}
			]
		}
	];
});
<|endoftext|>


<commit_before>var Feather = require('feather-ble');
var _ = require('underscore');

var CONSTANTS = require('./constants.js');

var Wearable = function(peripheral){

	/*
		VARIABLES
	*/
	// Reference to "this"
	var _self = this;

	// Noble Peripheral Object
	this._feather = new Feather({
		peripheral: peripheral,
		verbose: CONSTANTS.START_FEATHER_IN_VERBOSE_MODE,
		rssi: CONSTANTS.REQUEST_RSSI_UPDATES,
		rssi_update_rate: CONSTANTS.OVERRIDE_REQUEST_RSSI_UPDATE_RATE
	});

	// Wearable's UserID
	this._userID;

	// Flag for if wearable is ready for use
	this._ready = false;

	// Timestamp for when feather was ready
	this._start;

	// Timestamp for when feather was disconnected
	this._end;

	// If the wearable likes this exhibit
	this._likesExhibit;

	// Listener Event callbacks
	this._listeners = {
		// When wearable is connected and ready
		"ready": [],

		// When a user "likes"
		"like": [],

		// When a user dismisses
		"dismiss": [],

		// When rssi changes
		"rssi": [],

		// When a user disconnects
		"disconnect": []
	};

	/*
		METHODS
	*/
	// Adds Event Listener
	this.on = function(event, callback){
		_self._listeners[event].push(callback);
	};

	// Adds Event Listeners to feather. Tells Feather to setup. Grabs userID from feather.
	this.setup = function(){
		_self._feather.on("ready", onFeatherReady);

		_self._feather.on("disconnect", onFeatherDisconnected);

		_self._feather.on("message", onMessageRecieved);

		_self._feather.on("rssi", onRssiUpdate);

		_self._feather.setup();

		// Triggers callbacks of type 'e' passing along err
		function triggerSimpleCallbacks(e, err){
			_.each(_self._listeners[e], function(callback){
				callback(err);
			});
		}

		// Callback for when feather is ready
		//   Requests UserID from feather
		function onFeatherReady(err){
			if (err) {
				triggerSimpleCallbacks("ready", err);
				return;
			}

			_self._start = new Date();

			// var requestUserIDMessage = JSON.stringify({
			// 	msgType: "UserID",
			// 	request: "GET"
			// });

			// _self._feather.sendMessage(requestUserIDMessage);

			_self.sendMessage("UserID", {
				request: "GET"
			});
		}

		// Callback for when feather is disconnected
		//   Trigger disconnect callbacks
		function onFeatherDisconnected(){
			_self._end = new Date();
			triggerSimpleCallbacks("disconnect", null);
		}

		// Parse message and call appropriate callbacks
		function onMessageRecieved(msg){
			// console.log("Incoming Message: " + msg);

			if (isJsonString(msg)){
				msg = JSON.parse(msg);
				// console.log("Converted: ", msg);
			}
			else {
				// Message was not acceptable JSON
				// Unable to tell what im returning an error to at this point soooo.... doing nothing for now?
				return;
			}

			switch(msg.msgType){
				case "UserID":
					userIDRecieved(msg);
					break;
				case "Like":
					likeRecieved(msg);
					break;
				case "Dismiss":
					dismissedRecieved(msg);
					break;
				default:
					// Unknown message type
					break;
			}

			function userIDRecieved(message){
				_self._userID = message.userID;

				if (_self._userID && _self._userID.trim() != "") {
					_self._ready = true;
					triggerSimpleCallbacks("ready", null);
				}
				else {
					// Trigger with error
					var err = new Error("Did not retrieve userID from wearable");
					triggerSimpleCallbacks("ready", err);
				}
			}

			function likeRecieved(message){
				triggerSimpleCallbacks("like", null);
			}

			function dismissedRecieved(message){
				triggerSimpleCallbacks("dismiss", null);
			}

			function isJsonString(str) {
				try {
					JSON.parse(str);
				} catch (e) {
					return false;
				}
				return true;
			}
		}

		// Callback for when RSSI updates
		//   Call RSSI updated callbacks
		//     Passes along callback function that takes signal strength to send to feather
		function onRssiUpdate(err, rssi){

			if (! _self._ready){
				// We're not ready, don't try to update RSSI / Signal Strength
				return;
			}

			if (err) {
				// Trigger RSSI callbacks with error
				_.each(_self._listeners.rssi, function(callback){
					callback(err);
				});
				return;
			}

			// Trigger RSSI callbacks
			_.each(_self._listeners.rssi, function(callback){
				callback(err, rssi, function(strength){

					// var signalStrengthMessage = JSON.stringify({
					// 	msgType: "SignalStrength",
					// 	signalStrengthValue: strength
					// });

					// _self._feather.sendMessage(signalStrengthMessage);

					_self.sendMessage("SignalStrength", strength);
				});
			});
		}
	};

	// Sends a haptic signal to the wearable to vibrate x times (optional, defaults to 1)
	this.sendHaptic = function(times) {
		var data = times || 1;
		_self.sendMessage("Haptic", data);
	};

	// Send a message to set the color of the LED ring
	this.setColor = function(red, green, blue) {
		_self.sendMessage("SetLights", {
			color: {
				R: red,
				G: green,
				B: blue
			}
		});
	};

	// Sends a message to the wearable
	this.sendMessage = function(msgType, data) {
		var message = JSON.stringify({
			msgType: msgType,
			data: data
		});

		_self._feather.sendMessage(message);
	};

	// Disconnect from the feather
	this.disconnect = function(){
		_self._feather.disconnect();
	};

	// Returns if the noble peripheral is a wearable-acceptable peripheral
	this.isWearable = function(peripheral){
		return new Feather().isFeather(peripheral);
	};
};

module.exports = Wearable;<commit_msg>Dont send message if not connected<commit_after>var Feather = require('feather-ble');
var _ = require('underscore');

var CONSTANTS = require('./constants.js');

var Wearable = function(peripheral){

	/*
		VARIABLES
	*/
	// Reference to "this"
	var _self = this;

	// Noble Peripheral Object
	this._feather = new Feather({
		peripheral: peripheral,
		verbose: CONSTANTS.START_FEATHER_IN_VERBOSE_MODE,
		rssi: CONSTANTS.REQUEST_RSSI_UPDATES,
		rssi_update_rate: CONSTANTS.OVERRIDE_REQUEST_RSSI_UPDATE_RATE
	});

	// Wearable's UserID
	this._userID;

	// Flag for if wearable is ready for use
	this._ready = false;

	// Timestamp for when feather was ready
	this._start;

	// Timestamp for when feather was disconnected
	this._end;

	// If the wearable likes this exhibit
	this._likesExhibit;

	// Listener Event callbacks
	this._listeners = {
		// When wearable is connected and ready
		"ready": [],

		// When a user "likes"
		"like": [],

		// When a user dismisses
		"dismiss": [],

		// When rssi changes
		"rssi": [],

		// When a user disconnects
		"disconnect": []
	};

	/*
		METHODS
	*/
	// Adds Event Listener
	this.on = function(event, callback){
		_self._listeners[event].push(callback);
	};

	// Adds Event Listeners to feather. Tells Feather to setup. Grabs userID from feather.
	this.setup = function(){
		_self._feather.on("ready", onFeatherReady);

		_self._feather.on("disconnect", onFeatherDisconnected);

		_self._feather.on("message", onMessageRecieved);

		_self._feather.on("rssi", onRssiUpdate);

		_self._feather.setup();

		// Triggers callbacks of type 'e' passing along err
		function triggerSimpleCallbacks(e, err){
			_.each(_self._listeners[e], function(callback){
				callback(err);
			});
		}

		// Callback for when feather is ready
		//   Requests UserID from feather
		function onFeatherReady(err){
			if (err) {
				triggerSimpleCallbacks("ready", err);
				return;
			}

			_self._start = new Date();

			// var requestUserIDMessage = JSON.stringify({
			// 	msgType: "UserID",
			// 	request: "GET"
			// });

			// _self._feather.sendMessage(requestUserIDMessage);

			_self.sendMessage("UserID", {
				request: "GET"
			});
		}

		// Callback for when feather is disconnected
		//   Trigger disconnect callbacks
		function onFeatherDisconnected(){
			_self._end = new Date();
			triggerSimpleCallbacks("disconnect", null);
		}

		// Parse message and call appropriate callbacks
		function onMessageRecieved(msg){
			// console.log("Incoming Message: " + msg);

			if (isJsonString(msg)){
				msg = JSON.parse(msg);
				// console.log("Converted: ", msg);
			}
			else {
				// Message was not acceptable JSON
				// Unable to tell what im returning an error to at this point soooo.... doing nothing for now?
				return;
			}

			switch(msg.msgType){
				case "UserID":
					userIDRecieved(msg);
					break;
				case "Like":
					likeRecieved(msg);
					break;
				case "Dismiss":
					dismissedRecieved(msg);
					break;
				default:
					// Unknown message type
					break;
			}

			function userIDRecieved(message){
				_self._userID = message.userID;

				if (_self._userID && _self._userID.trim() != "") {
					_self._ready = true;
					triggerSimpleCallbacks("ready", null);
				}
				else {
					// Trigger with error
					var err = new Error("Did not retrieve userID from wearable");
					triggerSimpleCallbacks("ready", err);
				}
			}

			function likeRecieved(message){
				triggerSimpleCallbacks("like", null);
			}

			function dismissedRecieved(message){
				triggerSimpleCallbacks("dismiss", null);
			}

			function isJsonString(str) {
				try {
					JSON.parse(str);
				} catch (e) {
					return false;
				}
				return true;
			}
		}

		// Callback for when RSSI updates
		//   Call RSSI updated callbacks
		//     Passes along callback function that takes signal strength to send to feather
		function onRssiUpdate(err, rssi){

			if (! _self._ready){
				// We're not ready, don't try to update RSSI / Signal Strength
				return;
			}

			if (err) {
				// Trigger RSSI callbacks with error
				_.each(_self._listeners.rssi, function(callback){
					callback(err);
				});
				return;
			}

			// Trigger RSSI callbacks
			_.each(_self._listeners.rssi, function(callback){
				callback(err, rssi, function(strength){

					// var signalStrengthMessage = JSON.stringify({
					// 	msgType: "SignalStrength",
					// 	signalStrengthValue: strength
					// });

					// _self._feather.sendMessage(signalStrengthMessage);

					_self.sendMessage("SignalStrength", strength);
				});
			});
		}
	};

	// Sends a haptic signal to the wearable to vibrate x times (optional, defaults to 1)
	this.sendHaptic = function(times) {
		var data = times || 1;
		_self.sendMessage("Haptic", data);
	};

	// Send a message to set the color of the LED ring
	this.setColor = function(red, green, blue) {
		_self.sendMessage("SetLights", {
			color: {
				R: red,
				G: green,
				B: blue
			}
		});
	};

	// Sends a message to the wearable
	this.sendMessage = function(msgType, data) {
		var message = JSON.stringify({
			msgType: msgType,
			data: data
		});

		if (! _self._feather._connected) {
			// We're not connected, don't try to send message
			return;
		}

		_self._feather.sendMessage(message);
	};

	// Disconnect from the feather
	this.disconnect = function(){
		_self._feather.disconnect();
	};

	// Returns if the noble peripheral is a wearable-acceptable peripheral
	this.isWearable = function(peripheral){
		return new Feather().isFeather(peripheral);
	};
};

module.exports = Wearable;<|endoftext|>


<commit_before>var diva = (function() {
	var evts = [];
	var self = this;
	window.onscroll = onScroll;

	function onScroll(e) {
		for (var i in evts) {
			var evt = evts[i];
			if ((evt.total == -1 || evt.count < evt.total)) {
				if (elementIsInView(evt.element, evt.offset)) {
					if (!evt.triggered) {
						evt.callback.call(self,evt);
						evt.triggered = true;
						evt.count++;
					}
				} else {
					if (evt.triggered) {
						evt.triggered = false;
					}
				}
			} else {
				removeEvent(i);
			}
		}
	}

	function elementIsInView(element, offset) {
		var el = getElementPosition(element), percentOffset = false;
		el.width = element.offsetWidth;
		el.height = element.offsetHeight;

		var windowHeight = Math.min(window.screen.height, window.innerHeight, window.outerHeight);
		var windowWidth = Math.min(window.screen.width, window.innerWidth, window.innerHeight);
		offset = computeBoundsOffset(offset, windowHeight, windowWidth);
		var screenUpperBound = window.scrollY + offset;
		var screenLowerBound = window.scrollY + windowHeight - offset;
		var elementUpperBound = el.top;
		var elementLowerBound = el.top + el.height;
		var screenLeftBound = window.scrollX + offset;
		var screenRightBound = window.scrollX + windowWidth - offset;
		var elementLeftBound = el.left;
		var elementRightBound = el.left + el.width;

		return ((elementUpperBound <= screenUpperBound && elementUpperBound < screenLowerBound && elementLowerBound > screenLowerBound && elementLowerBound > screenUpperBound) || (elementUpperBound >= screenUpperBound && elementUpperBound < screenLowerBound && elementLowerBound > screenLowerBound) || (elementLowerBound >= screenUpperBound && elementLowerBound < screenLowerBound && elementUpperBound < screenUpperBound)) &&
				((elementLeftBound <= screenLeftBound && elementLeftBound < screenRightBound && elementRightBound > screenRightBound && elementRightBound > screenLeftBound) || (elementLeftBound >= screenLeftBound && elementLeftBound < screenRightBound && elementRightBound > screenRightBound) || (elementRightBound >= screenLeftBound && elementRightBound < screenRightBound && elementLeftBound < screenLeftBound));
	}

	function computeBoundsOffset(offset, windowHeight) {
		if (!offset) return 0;
		if (typeof offset === 'number') return offset;
		if (typeof offset !== 'string') return 0;
		if (offset.indexOf('%') > -1) percentOffset = true;
		else percentOffset = false;

		offset = parseFloat(offset.replace(/%|px/g, ''));
		if (isNaN(offset)) offset = 0;
		if (percentOffset) offset = windowHeight*offset;
		if (offset > 100) offset = 100;
		if (offset < 0) offset = 0;
		return offset;
	}

	function getElementPosition(el) {
	    var el2 = el;
	    var curtop = 0;
	    var curleft = 0;
	    if (document.getElementById || document.all) {
	        do  {
	            curleft += el.offsetLeft-el.scrollLeft;
	            curtop += el.offsetTop-el.scrollTop;
	            el = el.offsetParent;
	            el2 = el2.parentNode;
	            while (el2 != el) {
	                curleft -= el2.scrollLeft;
	                curtop -= el2.scrollTop;
	                el2 = el2.parentNode;
	            }
	        } while (el.offsetParent);

	    } else if (document.layers) {
	        curtop += el.y;
	        curleft += el.x;
	    }
	    return {top:curtop, left:curleft};
	}

	function addEvent(selector, opts, callback) {
		if (typeof opts === 'function') {
			callback = opts;
			opts = {};
		}

		var element = selector;
		if (typeof element === 'string') {
			if (element.indexOf('#') > -1 || element.indexOf('.') == -1) {
				element = element.replace(/\.|#/g, '');
				element = document.getElementById(element);
			} else if (element.indexOf('.') > -1) {
				element = element.replace(/\.|#/g, '');
				element = document.getElementsByClassName(element)[0];
			}
		}
		if (!element || element === null || typeof callback !== 'function') return;
		evts.push({
			selector: typeof selector === 'string' ? selector : element,
			element: element,
			count: 0,
			offset: opts.offset ? opts.offset : 0,
			total: opts.count ? opts.count : -1,
			callback: callback,
			triggered: false
		});
		onScroll();
	}

	function removeEvent(at) {
		if (typeof at === 'number') {
			evts.splice(index, 1);
		} else if (typeof at === 'string') {
			for (var i in evts) {
				if (evts[i].selector === at) {
					evts.splice(i, 1);
				}
			}
		} else if (typeof at === 'object') {
			for (var j in evts) {
				if (evts[j].element === at) {
					evts.splice(j, 1);
				}
			}
		}
	}

	return {
		events: evts,
		once: function(selector, opts, callback) {
			addEvent(selector, opts, callback);
		},
		always: function(selector, opts, callback) {
			addEvent(selector, opts, callback);
		},
		sometimes: function(selector, opts, callback) {
			addEvent(selector, opts, callback);
		},
		destroy: function(element) {
			if (typeof element === 'string' || typeof element === 'object') removeEvent(element);
		}
	};
})();<commit_msg>Now using module.exports<commit_after>module.exports = (function() {
	var evts = [];
	var self = this;
	window.onscroll = onScroll;

	function onScroll(e) {
		for (var i in evts) {
			var evt = evts[i];
			if ((evt.total == -1 || evt.count < evt.total)) {
				if (elementIsInView(evt.element, evt.offset)) {
					if (!evt.triggered) {
						evt.callback.call(self,evt);
						evt.triggered = true;
						evt.count++;
					}
				} else {
					if (evt.triggered) {
						evt.triggered = false;
					}
				}
			} else {
				removeEvent(i);
			}
		}
	}

	function elementIsInView(element, offset) {
		var el = getElementPosition(element), percentOffset = false;
		el.width = element.offsetWidth;
		el.height = element.offsetHeight;

		var windowHeight = Math.min(window.screen.height, window.innerHeight, window.outerHeight);
		var windowWidth = Math.min(window.screen.width, window.innerWidth, window.innerHeight);
		offset = computeBoundsOffset(offset, windowHeight, windowWidth);
		var screenUpperBound = window.scrollY + offset;
		var screenLowerBound = window.scrollY + windowHeight - offset;
		var elementUpperBound = el.top;
		var elementLowerBound = el.top + el.height;
		var screenLeftBound = window.scrollX + offset;
		var screenRightBound = window.scrollX + windowWidth - offset;
		var elementLeftBound = el.left;
		var elementRightBound = el.left + el.width;

		return ((elementUpperBound <= screenUpperBound && elementUpperBound < screenLowerBound && elementLowerBound > screenLowerBound && elementLowerBound > screenUpperBound) || (elementUpperBound >= screenUpperBound && elementUpperBound < screenLowerBound && elementLowerBound > screenLowerBound) || (elementLowerBound >= screenUpperBound && elementLowerBound < screenLowerBound && elementUpperBound < screenUpperBound)) &&
				((elementLeftBound <= screenLeftBound && elementLeftBound < screenRightBound && elementRightBound > screenRightBound && elementRightBound > screenLeftBound) || (elementLeftBound >= screenLeftBound && elementLeftBound < screenRightBound && elementRightBound > screenRightBound) || (elementRightBound >= screenLeftBound && elementRightBound < screenRightBound && elementLeftBound < screenLeftBound));
	}

	function computeBoundsOffset(offset, windowHeight) {
		if (!offset) return 0;
		if (typeof offset === 'number') return offset;
		if (typeof offset !== 'string') return 0;
		if (offset.indexOf('%') > -1) percentOffset = true;
		else percentOffset = false;

		offset = parseFloat(offset.replace(/%|px/g, ''));
		if (isNaN(offset)) offset = 0;
		if (percentOffset) offset = windowHeight*offset;
		if (offset > 100) offset = 100;
		if (offset < 0) offset = 0;
		return offset;
	}

	function getElementPosition(el) {
	    var el2 = el;
	    var curtop = 0;
	    var curleft = 0;
	    if (document.getElementById || document.all) {
	        do  {
	            curleft += el.offsetLeft-el.scrollLeft;
	            curtop += el.offsetTop-el.scrollTop;
	            el = el.offsetParent;
	            el2 = el2.parentNode;
	            while (el2 != el) {
	                curleft -= el2.scrollLeft;
	                curtop -= el2.scrollTop;
	                el2 = el2.parentNode;
	            }
	        } while (el.offsetParent);

	    } else if (document.layers) {
	        curtop += el.y;
	        curleft += el.x;
	    }
	    return {top:curtop, left:curleft};
	}

	function addEvent(selector, opts, callback) {
		if (typeof opts === 'function') {
			callback = opts;
			opts = {};
		}

		var element = selector;
		if (typeof element === 'string') {
			if (element.indexOf('#') > -1 || element.indexOf('.') == -1) {
				element = element.replace(/\.|#/g, '');
				element = document.getElementById(element);
			} else if (element.indexOf('.') > -1) {
				element = element.replace(/\.|#/g, '');
				element = document.getElementsByClassName(element)[0];
			}
		}
		if (!element || element === null || typeof callback !== 'function') return;
		evts.push({
			selector: typeof selector === 'string' ? selector : element,
			element: element,
			count: 0,
			offset: opts.offset ? opts.offset : 0,
			total: opts.count ? opts.count : -1,
			callback: callback,
			triggered: false
		});
		onScroll();
	}

	function removeEvent(at) {
		if (typeof at === 'number') {
			evts.splice(index, 1);
		} else if (typeof at === 'string') {
			for (var i in evts) {
				if (evts[i].selector === at) {
					evts.splice(i, 1);
				}
			}
		} else if (typeof at === 'object') {
			for (var j in evts) {
				if (evts[j].element === at) {
					evts.splice(j, 1);
				}
			}
		}
	}

	return {
		events: evts,
		once: function(selector, opts, callback) {
			addEvent(selector, opts, callback);
		},
		always: function(selector, opts, callback) {
			addEvent(selector, opts, callback);
		},
		sometimes: function(selector, opts, callback) {
			addEvent(selector, opts, callback);
		},
		destroy: function(element) {
			if (typeof element === 'string' || typeof element === 'object') removeEvent(element);
		}
	};
})();<|endoftext|>