Source: finch.js

/*
 * finch.js - A library for the Birdbrain Technologies Finch Robot 1.0 using
 * the WebHID API.
 *
 * Based off of the official Python library for the same robot and is mostly
 * compatible, however values in this library are generally from 0-255 rather
 * than 0-1. See this link for more on the original Python library:
 * https://learn.birdbraintechnologies.com/finch1/python/install
 *
 * Copyright 2023 Michael M.
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

/*
 * ===========================================================================
 * PRIVATE API
 * ===========================================================================
 */

// Asynchronously ends the command cmd to the robot along with the parameters
// msg. Leave msg empty for commands to receive from the Finch, and msg=[0] for
// other commands that take no parameters.
async function send(cmd, msg) {
  const arr = new Uint8Array(8);
  arr[0] = cmd.charCodeAt(0);
  let i;
  for (i = 0; i < msg.length + 1; i++)
    arr[i+1] = msg[i];
  for (; i < arr.length; i++)
    arr[i] = 0;
  console.log('sending', arr)
  window.device.sendReport(0, arr);
}

// Asynchronously receives data from the robot and returns the buffer received
// as a Uint8Array.
function recv() {
  return new Promise(res => {
    device.addEventListener('inputreport', event => {
      res(new Uint8Array(event.data.buffer));
    }, {once: true});
  });
}

/*
 * ===========================================================================
 * PUBLIC API
 * ===========================================================================
 */

/**
 * Returns a promise that resolves after sec seconds.
 * 
 * @param {number} sec - Time in seconds to resolve after
 *
 * @async
 *
 * @example
 * // Print the second string one second after the first;
 * console.log('string 1');
 * await wait(1);
 * console.log('string 2')
 */
export function wait(sec) {
  return new Promise(res => setTimeout(res, sec * 1000));
}

/**
 * A callback to be run from run()
 *
 * @callback run_callback
 * @async
 */

/**
 * Wraps an async function to assure the Finch robot is connected before and
 * disconnected after any code runs. Because of browser security restrictions,
 * this function call only be called in response to a user gesture. For
 * example, in the `click` event handler of a button.
 *
 * @param {run_callback} prog - An asynchronous function callback.
 *
 * @example
 * // Running both wheels at full speed for one second, then close the
 * // connection.
 * run(async function() {
 *   await wheels(255, 255);
 *   await wait(1);
 * });
 */
export function run(prog) {
  (async function() {
    // Get device permissions & connect
    if (!window.device) {
      const devices = await navigator.hid.requestDevice({ filters: [] });
      window.device = devices[0];
    }
    if (!device.opened) await device.open();
   
    // Get the command ID. Unused but useful for debugging.
    await send('z', []);
    const data = new Uint8Array(await recv());
    window.cmdId = data[0];
   
    // Run user program
    await prog();
   
    // Put in idle mode and close connection
    await send('R', [0]);
    await device.close();
  })();
}

/**
 * Asynchronously sets speed of each wheel. 0 is not moving, while 255 is full
 * speed.
 *
 * @param {number} left - Speed of the left wheel from 0 to 255.
 * @param {number} right - Speed of the right wheel from 0 to 255.
 *
 * @async
 *
 * @example
 * // Turn clockwise by setting the left wheel to full speed.
 * await wheels(255, 0);
 */
export function wheels(left, right) {
  return send('M', [0, left, 0, right]);
}

/**
 * Asynchronously runs the robot's buzzer at freq hertz for sec seconds. 
 *
 * @param {number} sec - Time in seconds.
 * @param {number} freq - Frequency in hertz.
 *
 * @async
 *
 * @example
 * // Play an A4 note (440 Hz) for one second.
 * await buzz(1, 440);
 */
export async function buzz(sec, freq) {
  await send('B', [((sec * 1000) & 0xFF00) >> 8, (sec * 1000) & 0x00FF,
    (freq & 0xFF00) >> 8], freq && 0xFF);
  await wait(sec);
}

/**
 * Sets the LED light to the color with the RGB value passed. Each component of
 * the RGB value is in the range of 0-255.
 *
 * @param {number} r - Red component of the RGB value.
 * @param {number} g - Green component of the RGB value.
 * @param {number} b - Blue component of the RGB value.
 *
 * @async
 *
 * @example
 * // Set the LED to magenta (RGB value 255, 0, 255).
 * await led(255, 0, 255);
 */
export function led(r, g, b) {
  return send('O', [r, g, b]);  
}

/**
 * Asynchronously halts the robot and stops all commands currently running on
 * the robot.
 *
 * @async
 *
 * @example
 * // Move at full speed forward for one second.
 * await wheels(255, 255);
 * await wait(1);
 * await halt();
 */
export function halt() {
  return send('X', [0]);
}

/**
 * Returns a promise that resolves with the status array of the obstacle
 * sensors. The first element is the status of the left obstacle sensor and the
 * second element is the status of the right obstacle sensor. Each value is
 * either false (no obstacle) or true (obstacle)
 *
 * @async
 * @returns {Promise<boolean[]>} The status array of the obstacle sensors.
 *
 * @example
 * // Turn the LED red if there is an obstacle, otherwise green.
 * const obstacles = await get_obstacles();
 * if (obstacles.every(x => x)) {
 *   await led(0, 255, 0);
 * } else {
 *   await led(255, 0, 0);
 * }
 */
export async function get_obstacles() {
  await send('I', []);
  const data = await recv();
  return data.slice(0, 2).map(x => x === 1);
}

/**
 * Returns a promise that resolves with the status of the acceleration sensor.
 * as an object.
 *
 * @async
 * @returns {Promise<Object>} Status of the acceleration sensor.
 *
 * @example
 * // Move forward if the robot is being tapped.
 * const accel = await get_acceleration();
 * if (accel.tap) {
 *   await wheels(255, 255);
 * }
 */
export async function get_acceleration() {
  // convert finch readings to Gs
  function convAccel(a) {
    if (a > 31) a -= 64
    return a * 1.6 / 32.0
  }
 
  await send('A', []);
  const data = await recv();
  const accel = data.slice(0, 3).map(convAccel);
  return {
    x: accel[0], y: accel[1], z: accel[2],
    tap: (data[4] & 0x20) !== 0, shake: (data[4] & 0x80) !== 0
  };
}

/**
 * Returns a promise that resolves with the temperature detected by the robot
 * in degrees Celsius.
 *
 * @async
 * @returns {Promise<number>} Temperature in degrees Celsius
 *
 * @example
 * // Print the temperature in degrees Fahrenheit.
 * const temp_c = await get_temperature();
 * console.log(temp_c * 1.8 + 32);
 */
export async function get_temperature() {
  await send('T', []);
  const data = await recv();
  return (data[0] - 127) / 2.4 + 25;
}

/**
 * Returns a promise that resolves with the status array of the light sensors.
 * The first element is the intensity of the light on the left sensor, and the
 * second element is the intensity of the light on the right sensor. Each
 * value is from 0 (full darkness) to 255 (full brightness).
 *
 * @async
 * @returns {Promise<number[]>} Status array of the light sensors.
 */
export async function get_light() {
  await send('L', []);
  const data = await recv();
  return data.slice(0, 2);
}