BLEセントラルコンソール

このエントリーをはてなブックマークに追加

How it works

BLEのセントラルとして使えるWebアプリです。
obnizの近くにあるBLEを探して接続できます。

Program

<!-- HTML Example -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"
      integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">


  <script src="https://code.jquery.com/jquery-3.2.1.min.js"
          integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js"
          integrity="sha384-vFJXuSJphROIrBnz7yo7oB41mKfc8JzQZiCq4NCceLEaO4IHwicKwpJf9c9IpFgh"
          crossorigin="anonymous"></script>
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"
          integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl"
          crossorigin="anonymous"></script>
  <script src="https://rawgit.com/sylvain-hamel/safarimobile-multiline-select/master/src/safarimobile-multiline-select.js" crossorigin="anonymous"></script>

  <script src="https://unpkg.com/obniz@1.9.1/obniz.js"></script>

  <style>
    select {
      width: 100%;
    }

    .row .col-4 {
      margin-bottom: 10px;
    }

    .row .round {
      border: 1px solid #ccc;
      border-radius: 5px;
      margin-left: 10px;
      margin-right: 10px;
      overflow-y: scroll;
      word-break: normal;
      height: 300px;
    }

    .row .round::-webkit-scrollbar {
      background:#eee;
      width:10px;
    }
    .row .round::-webkit-scrollbar-thumb {
      background:#aaa;
    }

    ul .input-group{
      padding-right: 40px;

    }

    /*for mobile safari*/
    ul.multilineselect{
      width:100%
    }
    ul.multilineselect::-webkit-scrollbar {
      background:#ccc;
      width:10px;
    }
    ul.multilineselect::-webkit-scrollbar-thumb {
      background:#aaa;
    }

  </style>


  
</head>

<body>
<div id="obniz-debug"></div>
<br>
<div class="text-center">
  <h1> Ble Central </h1>
</div>
<div class="container">
  <div class="row">
    <div class="col text-center">
    </div>
  </div>
  <div class="row">
    <div class="col-md-4 col-sm-6 col-6">
      <h3>Devices</h3>
      <select name="device" id="deviceSelector" class="form-control" size="8">
      </select><br/>
      <button type="button" id="device-clear" class="btn btn-secondary">clear</button>
      <button type="button" id="device-connect" class="btn btn-primary">Connect</button>
      <button type="button" id="device-disconnect" class="btn btn-danger" style="display:none">Disconnect</button>
    </div>
    <div class="col-md-4 col-sm-6 col-6">
      <h3>Services</h3>
      <select name="service" id="serviceSelector" class="form-control" size="8">
      </select>
    </div>
    <div class="col-md-4 col-sm-6 col-6">
      <h3>Characteristics</h3>
      <select name="characteristic" id="characteristicSelector"  class="form-control" size="8">
      </select>
    </div>
  </div>
  <div class="row">
    <div class="col-md-6 col-sm-12">
      <h3>detail</h3>
      <div class="round">
        <div id="device-detail" style="display:none">
          <ul>
            <li>device address</li>
            <span class="device_address"> </span>
            <li>rssi</li>
            <span class="device_rssi"> </span>
            <li>advertise data raw</li>
            <span class="adv_raw"> </span>
            <li>scan response data raw</li>
            <span class="scan_resp_raw"> </span>
            <li>advertise / scan response data meaning</li>
            <div class="meaning"> </div>
          </ul>
        </div>

        <div id="service-detail" style="display:none">
          <ul>
            <li>service uuid</li>
            <span class="service_uuid"> </span>
            <div class="service_name_wrapper">
              <li>uuid name (defined by <a href="https://www.bluetooth.com/specifications/gatt/services">Bluetooth specification</a>)</li>
              <span class="service_name"> </span>
            </div>
          </ul>
        </div>

        <div id="characteristic-detail" style="display:none">
          <ul>
            <li>characteristic uuid</li>
            <span class="characteristic_uuid"> </span>
            <div class="characteristic_name_wrapper">
              <li>uuid name (defined by <a href="https://www.bluetooth.com/specifications/gatt/characteristics">Bluetooth specification</a>)</li>
              <span class="characteristic_name_name"> </span>
            </div>
            <li>value type</li>
            <div class="btn-group btn-group-toggle" data-toggle="buttons">
              <label class="btn  btn-outline-primary active">
                <input type="radio" name="characteristic_type" autocomplete="off" value="binary" checked> binary
              </label>
              <label class="btn btn-outline-primary">
                <input type="radio" name="characteristic_type" autocomplete="off" value="text" > text
              </label>
            </div>
            <li>value</li>
            <span class="characteristic_value"> </span><br/>
            <button class="btn btn-primary mb-3" type="button" id="characteristic_read">read</button>
            <div class="input-group mb-3">
              <input type="text" class="form-control" id="characteristic_write_value" placeholder="hex string (ex: f94c8c...) " />
              <div class="input-group-append">
              <button class="btn btn-primary" type="button" id="characteristic_write">write</button>
            </div>
            </div>
          </ul>
        </div>
      </div>
    </div>
    <div class="col-md-6 col-sm-12">
      <h3>Log</h3>
      <div id="log" class="round">
        <div id="log-content">

        </div>
      </div>

    </div>
  </div>
</div>

<script>


  let isConnected = false;
  let devices = [];
  let deviceSelector =$("#deviceSelector");
  let serviceSelector =$("#serviceSelector");
  let characteristicSelector =$("#characteristicSelector");

  let deviceDetailTag = $("#device-detail");
  let serviceDetailTag = $("#service-detail");
  let characteristicDetailTag = $("#characteristic-detail");

  let logWrapperDom = $("#log");
  let logDom = $("#log-content");
  let currentPeripheral = null;


  serviceSelector.prop("disabled", true);
  characteristicSelector.prop("disabled", true);

  deviceDetailTag.hide();
  serviceDetailTag.hide();
  characteristicDetailTag.hide();


  $().ready(function () {
    if((navigator.userAgent.match(/Android/) && navigator.userAgent.match(/Mobile/))){
      $("select").fixForSafariMobile(true);

    }else{
      $("select").fixForSafariMobile();
    }
  });

  /* This will be over written on obniz.io webapp page */
  let obniz = new Obniz("OBNIZ_ID_HERE");


  /** BEFORE CONNECT **/
  obniz.onconnect = async function () {
    log("obniz connected.");

    log("start ble scan repeatly");
    startScanRepeatly();
  };

  function startScanRepeatly() {
    obniz.ble.scan.end();
    if (!isConnected) {
      log("scan repeating");
      obniz.ble.scan.start({duration: 30});
      setTimeout(startScanRepeatly, 35 * 1000);
    }
  }


  obniz.ble.scan.onfind = function (peripheral) {
    if (undefined === devices.find((elm) => {
          return elm.address === peripheral.address
        })) {
      let address = splitByLength(peripheral.address, 2).join(":");
      log("find new peripheral : " + address + (peripheral.localName? "(" + peripheral.localName + ")":""));
  
      devices.push(peripheral);
      let optionTag = '<option value="' + peripheral.address + '">' + address +(peripheral.localName? "(" + peripheral.localName + ")":"")  + '</option>';
      deviceSelector.append(optionTag);
      deviceSelector.trigger("item-added");
    }
  };


  $("#device-clear").on("click", () => {
    if(isConnected)return;
    obniz.ble.scan.end();
    devices = [];
    deviceSelector.empty();
    deviceSelector.trigger("item-added");

    log("clear all device data and rescan");
    obniz.ble.scan.start({duration: 30});
  });

  $("#device-disconnect").on("click", () => {
    currentPeripheral.disconnect();
  });

  $("#device-connect").on("click", () => {
    let device = getDevice(deviceSelector.val());
    device.onconnect = ()=>{
        log("connected to " + splitByLength(device.address, 2).join(":"));
      currentPeripheral = device;
      $("#device-connect").hide();
      $("#device-disconnect").show();

      serviceSelector.prop("disabled", false);
      characteristicSelector.prop("disabled", false);
      findService();
    };

    device.ondisconnect = ()=>{
      log("disconnected from " + splitByLength(device.address, 2).join(":"));
      isConnected = false;
      serviceSelector.prop("disabled", true);
      characteristicSelector.prop("disabled", true);
      deviceSelector.prop("disabled", false);

      $("#device-connect").prop("disabled", false);
      $("#device-clear").prop("disabled", false);

      currentPeripheral = null;
      $("#device-connect").show();
      $("#device-disconnect").hide();

      log("start ble scan repeatly");
      startScanRepeatly();
    };

    $("#device-connect").prop("disabled", true);
    $("#device-clear").prop("disabled", true);
    deviceSelector.prop("disabled", true);
    obniz.ble.scan.end();
    device.connect();
    log("connecting to " + splitByLength(device.address, 2).join(":"));
    isConnected = true;

  });

  deviceSelector.change(()=>{
    let device = getDevice(deviceSelector.val());
    showDetailDevice(device);
    serviceSelector.empty();
    serviceSelector.trigger("item-added");
    characteristicSelector.empty();
    characteristicSelector.trigger("item-added");
  });


  /** AFTER CONNECT **/


  function findService(){
    log("discovering services on device(" + splitByLength(currentPeripheral.address, 2).join(":") + ")");
    serviceSelector.empty();
    serviceSelector.trigger("item-added");
    currentPeripheral.discoverAllServices();
    currentPeripheral.ondiscoverservice = (service) =>{
      let optionTag = '<option value="' + service.uuid + '">' + service.uuid + '</option>';
      serviceSelector.append(optionTag);
      serviceSelector.trigger("item-added");
    };
  }

  serviceSelector.change(()=>{
    let service = currentPeripheral.getService(serviceSelector.val());
    showDetailService(service);
    characteristicSelector.empty();
    characteristicSelector.trigger("item-added");
    findCharacteristics(service);
  });

  function findCharacteristics(service){
    log("discovering characteristics on service("+service.uuid+")");
    characteristicSelector.empty();
    characteristicSelector.trigger("item-added");
    service.discoverAllCharacteristics();
    service.ondiscovercharacteristic = (chara)=>{
      let optionTag = '<option value="' + chara.uuid + '">' + chara.uuid + '</option>';
      characteristicSelector.append(optionTag);
      characteristicSelector.trigger("item-added");
    }
  }


  characteristicSelector.change(()=>{
    let chara = currentPeripheral.getService(serviceSelector.val()).getCharacteristic(characteristicSelector.val());
    showDetailCharacteristic(chara);

    chara.read();
    characteristicDetailTag.find(".characteristic_value").html();
    log("read value on charactaristic ("+chara.uuid+")");

    chara.onread = (data) =>{
      let currentChara = currentPeripheral.getService(serviceSelector.val()).getCharacteristic(characteristicSelector.val());
      if(chara === currentChara ){

        let type = $("[name=characteristic_type]:checked").val();
        let str;
        if(type === "binary") {
          str = "0x" + data.map((elm) => {
            return elm.toString(16).padStart(2, "0")
          }).join("");
          if (str.length === 2) {
            str = "null";
          }
        }else{
          str = String.fromCharCode.apply(null, data);
          if(str.length === 0){
            str = "null";
          }else{
            str = '"' + str + '"';
          }
        }
        characteristicDetailTag.find(".characteristic_value").html(str);
      }
    }
  });

  $("#characteristic_read").on('click', ()=>{
    let chara = currentPeripheral.getService(serviceSelector.val()).getCharacteristic(characteristicSelector.val())
    log("read value on charactaristic ("+chara.uuid+")");
    chara.read();
  });

  $("#characteristic_write").on('click', ()=>{
    let chara = currentPeripheral.getService(serviceSelector.val()).getCharacteristic(characteristicSelector.val())
    log("write value on charactaristic ("+chara.uuid+")");
    let valString = $("#characteristic_write_value").val();
    let type = $("input[name=characteristic_type]:checked").val();
    let data;
    if(type === "binary") {
      data = splitByLength(valString, 2).map((elm) => {
        return parseInt(elm, 16)
      });
    }else{
      data = [];
      for(let i =0; i< valString.length; i++){
        data.push(valString.charCodeAt(i));
      }
    }
    chara.write(data);

    chara.onwrite = (results)=>{
      chara.read();
    }
  });

  $( 'input[name=characteristic_type]:radio' ).change( ()=> {
    let chara = currentPeripheral.getService(serviceSelector.val()).getCharacteristic(characteristicSelector.val())
    chara.read();
    let type = $("input[name=characteristic_type]:checked").val();
    let data = {
      binary : "hex string (ex: f94c8c...)",
      text : "string (ex: hello world. )",
    }
    $("#characteristic_write_value").val("");
    $("#characteristic_write_value").prop("placeholder",data[type] );
    characteristicDetailTag.find(".characteristic_value").html();
  });

    /** DISPLAY **/

  function showDetailDevice(peripheral){

    deviceDetailTag.find(".device_address").html(splitByLength(peripheral.address, 2).join(":"));
    deviceDetailTag.find(".device_rssi").html(peripheral.rssi);
    deviceDetailTag.find(".adv_raw").html(array2string(peripheral.adv_data));
    deviceDetailTag.find(".scan_resp_raw").html(array2string(peripheral.scan_resp));

    peripheral.analyseAdvertisement();
    let meanings = [];
    meanings.push("<ul>");
    for( let row of peripheral.advertise_data_rows){

      let data = advDataAnalyze(row);
      meanings.push("<li>"+data.title+"</li>" + data.infomations.join("<br/>"));
    }
    meanings.push("</ul>");

    deviceDetailTag.find(".meaning").html(meanings.join(""));

    deviceDetailTag.show();
    serviceDetailTag.hide();
    characteristicDetailTag.hide();
  }


  function showDetailService(service) {
    const serviceUuidList = {
      0x1800: "Generic Access",
      0x1811: "Alert Notification Service",
      0x1815: "Automation IO",
      0x180F: "Battery Service",
      0x1810: "Blood Pressure",
      0x181B: "Body Composition",
      0x181E: "Bond Management Service",
      0x181F: "Continuous Glucose Monitoring",
      0x1805: "Current Time Service",
      0x1818: "Cycling Power",
      0x1816: "Cycling Speed and Cadence",
      0x180A: "Device Information",
      0x181A: "Environmental Sensing",
      0x1826: "Fitness Machine",
      0x1801: "Generic Attribute",
      0x1808: "Glucose",
      0x1809: "Health Thermometer",
      0x180D: "Heart Rate",
      0x1823: "HTTP Proxy",
      0x1812: "Human Interface Device",
      0x1802: "Immediate Alert",
      0x1821: "Indoor Positioning",
      0x1820: "Internet Protocol Support Service",
      0x1803: "Link Loss",
      0x1819: "Location and Navigation",
      0x1827: "Mesh Provisioning Service",
      0x1828: "Mesh Proxy Service",
      0x1807: "Next DST Change Service",
      0x1825: "Object Transfer Service",
      0x180E: "Phone Alert Status Service",
      0x1822: "Pulse Oximeter Service",
      0x1829: "Reconnection Configuration",
      0x1806: "Reference Time Update Service",
      0x1814: "Running Speed and Cadence",
      0x1813: "Scan Parameters",
      0x1824: "Transport Discovery",
      0x1804: "Tx Power",
      0x181C: "User Data",
      0x181D: "Weight Scale",
    };

    serviceDetailTag.find(".service_uuid").html(service.uuid);

    let name = serviceUuidList[service.uuid];
    if (name) {
      serviceDetailTag.find(".service_name_wrapper").show();
      serviceDetailTag.find(".service_name").html(name);
    } else {
      serviceDetailTag.find(".service_name_wrapper").hide();
      serviceDetailTag.find(".service_name").html("");
    }
    deviceDetailTag.hide();
    serviceDetailTag.show();
    characteristicDetailTag.hide();
  }


  function showDetailCharacteristic(chara) {
    const characteristicUuidList = {
      0x2A7E:"Aerobic Heart Rate Lower Limit",
      0x2A84:"Aerobic Heart Rate Upper Limit",
      0x2A7F:"Aerobic Threshold",
      0x2A80:"Age",
      0x2A5A:"Aggregate",
      0x2A43:"Alert Category ID",
      0x2A42:"Alert Category ID Bit Mask",
      0x2A06:"Alert Level",
      0x2A44:"Alert Notification Control Point",
      0x2A3F:"Alert Status",
      0x2AB3:"Altitude",
      0x2A81:"Anaerobic Heart Rate Lower Limit",
      0x2A82:"Anaerobic Heart Rate Upper Limit",
      0x2A83:"Anaerobic Threshold",
      0x2A58:"Analog",
      0x2A59:"Analog Output",
      0x2A73:"Apparent Wind Direction",
      0x2A72:"Apparent Wind Speed",
      0x2A01:"Appearance",
      0x2AA3:"Barometric Pressure Trend",
      0x2A19:"Battery Level",
      0x2A1B:"Battery Level State",
      0x2A1A:"Battery Power State",
      0x2A49:"Blood Pressure Feature",
      0x2A35:"Blood Pressure Measurement",
      0x2A9B:"Body Composition Feature",
      0x2A9C:"Body Composition Measurement",
      0x2A38:"Body Sensor Location",
      0x2AA4:"Bond Management Control Point",
      0x2AA5:"Bond Management Features",
      0x2A22:"Boot Keyboard Input Report",
      0x2A32:"Boot Keyboard Output Report",
      0x2A33:"Boot Mouse Input Report",
      0x2AA6:"Central Address Resolution",
      0x2AA8:"CGM Feature",
      0x2AA7:"CGM Measurement",
      0x2AAB:"CGM Session Run Time",
      0x2AAA:"CGM Session Start Time",
      0x2AAC:"CGM Specific Ops Control Point",
      0x2AA9:"CGM Status",
      0x2ACE:"Cross Trainer Data",
      0x2A5C:"CSC Feature",
      0x2A5B:"CSC Measurement",
      0x2A2B:"Current Time",
      0x2A66:"Cycling Power Control Point",
      0x2A65:"Cycling Power Feature",
      0x2A63:"Cycling Power Measurement",
      0x2A64:"Cycling Power Vector",
      0x2A99:"Database Change Increment",
      0x2A85:"Date of Birth",
      0x2A86:"Date of Threshold Assessment",
      0x2A08:"Date Time",
      0x2A0A:"Day Date Time",
      0x2A09:"Day of Week",
      0x2A7D:"Descriptor Value Changed",
      0x2A00:"Device Name",
      0x2A7B:"Dew Point",
      0x2A56:"Digital",
      0x2A57:"Digital Output",
      0x2A0D:"DST Offset",
      0x2A6C:"Elevation",
      0x2A87:"Email Address",
      0x2A0B:"Exact Time 100",
      0x2A0C:"Exact Time 256",
      0x2A88:"Fat Burn Heart Rate Lower Limit",
      0x2A89:"Fat Burn Heart Rate Upper Limit",
      0x2A26:"Firmware Revision String",
      0x2A8A:"First Name",
      0x2AD9:"Fitness Machine Control Point",
      0x2ACC:"Fitness Machine Feature",
      0x2ADA:"Fitness Machine Status",
      0x2A8B:"Five Zone Heart Rate Limits",
      0x2AB2:"Floor Number",
      0x2A8C:"Gender",
      0x2A51:"Glucose Feature",
      0x2A18:"Glucose Measurement",
      0x2A34:"Glucose Measurement Context",
      0x2A74:"Gust Factor",
      0x2A27:"Hardware Revision String",
      0x2A39:"Heart Rate Control Point",
      0x2A8D:"Heart Rate Max",
      0x2A37:"Heart Rate Measurement",
      0x2A7A:"Heat Index",
      0x2A8E:"Height",
      0x2A4C:"HID Control Point",
      0x2A4A:"HID Information",
      0x2A8F:"Hip Circumference",
      0x2ABA:"HTTP Control Point",
      0x2AB9:"HTTP Entity Body",
      0x2AB7:"HTTP Headers",
      0x2AB8:"HTTP Status Code",
      0x2ABB:"HTTPS Security",
      0x2A6F:"Humidity",
      0x2A2A:"IEEE 11073-20601 Regulatory Certification Data List",
      0x2AD2:"Indoor Bike Data",
      0x2AAD:"Indoor Positioning Configuration",
      0x2A36:"Intermediate Cuff Pressure",
      0x2A1E:"Intermediate Temperature",
      0x2A77:"Irradiance",
      0x2AA2:"Language",
      0x2A90:"Last Name",
      0x2AAE:"Latitude",
      0x2A6B:"LN Control Point",
      0x2A6A:"LN Feature",
      0x2AB1:"Local East Coordinate",
      0x2AB0:"Local North Coordinate",
      0x2A0F:"Local Time Information",
      0x2A67:"Location and Speed Characteristic",
      0x2AB5:"Location Name",
      0x2AAF:"Longitude",
      0x2A2C:"Magnetic Declination",
      0x2AA0:"Magnetic Flux Density - 2D",
      0x2AA1:"Magnetic Flux Density - 3D",
      0x2A29:"Manufacturer Name String",
      0x2A91:"Maximum Recommended Heart Rate",
      0x2A21:"Measurement Interval",
      0x2A24:"Model Number String",
      0x2A68:"Navigation",
      0x2A3E:"Network Availability",
      0x2A46:"New Alert",
      0x2AC5:"Object Action Control Point",
      0x2AC8:"Object Changed",
      0x2AC1:"Object First-Created",
      0x2AC3:"Object ID",
      0x2AC2:"Object Last-Modified",
      0x2AC6:"Object List Control Point",
      0x2AC7:"Object List Filter",
      0x2ABE:"Object Name",
      0x2AC4:"Object Properties",
      0x2AC0:"Object Size",
      0x2ABF:"Object Type",
      0x2ABD:"OTS Feature",
      0x2A04:"Peripheral Preferred Connection Parameters",
      0x2A02:"Peripheral Privacy Flag",
      0x2A5F:"PLX Continuous Measurement Characteristic",
      0x2A60:"PLX Features",
      0x2A5E:"PLX Spot-Check Measurement",
      0x2A50:"PnP ID",
      0x2A75:"Pollen Concentration",
      0x2A2F:"Position 2D",
      0x2A30:"Position 3D",
      0x2A69:"Position Quality",
      0x2A6D:"Pressure",
      0x2A4E:"Protocol Mode",
      0x2A62:"Pulse Oximetry Control Point",
      0x2A78:"Rainfall",
      0x2B1D:"RC Feature",
      0x2B1E:"RC Settings",
      0x2A03:"Reconnection Address",
      0x2B1F:"Reconnection Configuration Control Point",
      0x2A52:"Record Access Control Point",
      0x2A14:"Reference Time Information",
      0x2A3A:"Removable",
      0x2A4D:"Report",
      0x2A4B:"Report Map",
      0x2AC9:"Resolvable Private Address Only",
      0x2A92:"Resting Heart Rate",
      0x2A40:"Ringer Control point",
      0x2A41:"Ringer Setting",
      0x2AD1:"Rower Data",
      0x2A54:"RSC Feature",
      0x2A53:"RSC Measurement",
      0x2A55:"SC Control Point",
      0x2A4F:"Scan Interval Window",
      0x2A31:"Scan Refresh",
      0x2A3C:"Scientific Temperature Celsius",
      0x2A10:"Secondary Time Zone",
      0x2A5D:"Sensor Location",
      0x2A25:"Serial Number String",
      0x2A05:"Service Changed",
      0x2A3B:"Service Required",
      0x2A28:"Software Revision String",
      0x2A93:"Sport Type for Aerobic and Anaerobic Thresholds",
      0x2AD0:"Stair Climber Data",
      0x2ACF:"Step Climber Data",
      0x2A3D:"String",
      0x2AD7:"Supported Heart Rate Range",
      0x2AD5:"Supported Inclination Range",
      0x2A47:"Supported New Alert Category",
      0x2AD8:"Supported Power Range",
      0x2AD6:"Supported Resistance Level Range",
      0x2AD4:"Supported Speed Range",
      0x2A48:"Supported Unread Alert Category",
      0x2A23:"System ID",
      0x2ABC:"TDS Control Point",
      0x2A6E:"Temperature",
      0x2A1F:"Temperature Celsius",
      0x2A20:"Temperature Fahrenheit",
      0x2A1C:"Temperature Measurement",
      0x2A1D:"Temperature Type",
      0x2A94:"Three Zone Heart Rate Limits",
      0x2A12:"Time Accuracy",
      0x2A15:"Time Broadcast",
      0x2A13:"Time Source",
      0x2A16:"Time Update Control Point",
      0x2A17:"Time Update State",
      0x2A11:"Time with DST",
      0x2A0E:"Time Zone",
      0x2AD3:"Training Status",
      0x2ACD:"Treadmill Data",
      0x2A71:"True Wind Direction",
      0x2A70:"True Wind Speed",
      0x2A95:"Two Zone Heart Rate Limit",
      0x2A07:"Tx Power Level",
      0x2AB4:"Uncertainty",
      0x2A45:"Unread Alert Status",
      0x2AB6:"URI",
      0x2A9F:"User Control Point",
      0x2A9A:"User Index",
      0x2A76:"UV Index",
      0x2A96:"VO2 Max",
      0x2A97:"Waist Circumference",
      0x2A98:"Weight",
      0x2A9D:"Weight Measurement",
      0x2A9E:"Weight Scale Feature",
      0x2A79:"Wind Chill",
    };

    characteristicDetailTag.find(".characteristic_uuid").html(chara.uuid);

    let name = characteristicUuidList[chara.uuid];
    if (name) {
      characteristicDetailTag.find(".characteristic_name_wrapper").show();
      characteristicDetailTag.find(".characteristic_name").html(name);
    } else {
      characteristicDetailTag.find(".characteristic_name_wrapper").hide();
      characteristicDetailTag.find(".characteristic_name_wrapper").html("");
    }
    characteristicDetailTag.find('input[name=characteristic_type]:eq(0)').prop('checked', true);

    deviceDetailTag.hide();
    serviceDetailTag.hide();
    characteristicDetailTag.show();
  }
  /** UTIL **/

  function getDevice(address){
    return devices.filter((elm)=>{
      return elm.address === address;
    }).pop();
  }

  function log(msg) {
    let docHeight = logDom.outerHeight(); //ドキュメントの高さ
    let windowHeight = logWrapperDom.innerHeight(); //ウィンドウの高さ
    let pageBottom = docHeight - windowHeight; //ドキュメントの高さ - ウィンドウの高さ
    let needToBottomScroll = (pageBottom <= logWrapperDom.scrollTop());


    let before = logDom.html().trim();
    logDom.html(before.length > 0 ? before + "<br/>" + msg : msg);

    if (needToBottomScroll) {
      logWrapperDom.scrollTop(logDom.innerHeight());
    }


  }

  function splitByLength(str, length) {
    let resultArr = [];
    if (!str || !length || length < 1) {
      return resultArr;
    }
    let index = 0;
    let start = index;
    let end = start + length;
    while (start < str.length) {
      resultArr[index] = str.substring(start, end);
      index++;
      start = end;
      end = start + length;
    }
    return resultArr;
  }

  function array2string(arr){
    if(!arr || !Array.isArray(arr) ){
      return "undefined";
    }
    if(arr.length === 0 ){
      return "[ ]";
    }
    return "[" + arr.map((elm)=>{
      return "0x" + parseInt(elm).toString(16).padStart(2,"0");
    }).join(", ") + "]";
  }

  function advDataAnalyze(row){
    let title;
    let infomations = [];
    let bytes = row.slice(1);
    switch(row[0]) {
      case 0x01:
        title = "Flags";
        let data = {
          0x01:"LE Limited Discoverable Mode",
          0x02:"LE General Discoverable Mode",
          0x04:"BR/EDR Not Supported ",
          0x08:"Simultaneous LE and BR/EDR to Same Device Capa- ble (Controller)",
          0x10:"Simultaneous LE and BR/EDR to Same Device Capa- ble (Host)",
          0x20:"unknown flag - 0x20",
          0x40:"unknown flag - 0x40",
          0x80:"unknown flag - 0x80",
        };
        for(let key in data){
          if(parseInt(key) & bytes[0]){
            infomations.push(data[key]);
          }
        }
        break;
      case 0x02: // Incomplete List of 16-bit Service Class UUID
      case 0x03: // Complete List of 16-bit Service Class UUIDs
        title = "16-bit Service UUIDs";
        for (let j = 0; j < bytes.length; j += 2) {
          let uuid = bytes.slice(j, j + 2).toString('hex').match(/.{1,2}/g).reverse().join('');
          infomations.push("uuid - " + uuid);
        }
        break;

      case 0x06: // Incomplete List of 128-bit Service Class UUIDs
      case 0x07: // Complete List of 128-bit Service Class UUIDs
        title = "128-bit Service UUIDs";
        for (let j = 0; j < bytes.length; j += 16) {
          let uuid = bytes.slice(j, j + 16).toString('hex').match(/.{1,2}/g).reverse().join('');
          infomations.push("uuid - " + uuid);
        }
        break;

      case 0x08: // Shortened Local Name
      case 0x09: // Complete Local Name»
        title = "Local Name";
        infomations.push(String.fromCharCode.apply(null, bytes));
        break;

      case 0x0a: // Tx Power Level
        title = "Tx Power Level";
        infomations.push(bytes[0]);
        break;

      case  0x14: // List of 16 bit solicitation UUIDs
        title = "16-bit solicitation UUIDs";
        for (let j = 0; j < bytes.length; j += 2) {
          let uuid = bytes.slice(j, j + 2).toString('hex').match(/.{1,2}/g).reverse().join('');
          infomations.push("uuid - " + uuid);
        }

        break;

      case  0x15: // List of 128 bit solicitation UUIDs
        title = "128-bit solicitation UUIDs";
        for (let j = 0; j < bytes.length; j += 16) {
          let uuid = bytes.slice(j, j + 16).toString('hex').match(/.{1,2}/g).reverse().join('');
          infomations.push("uuid - " + uuid);
        }
        break;

      case 0x16: // 16-bit Service Data, there can be multiple occurences
        title = "16-bit Service Data";
        let serviceDataUuid = bytes.slice(0, 2).toString('hex').match(/.{1,2}/g).reverse().join('');
        let serviceData = bytes.slice(2, bytes.length);
        infomations.push("uuid - " + serviceDataUuid );
        infomations.push("serviceData - " + array2string(serviceData));
        break;

      case 0x20: // 32-bit Service Data, there can be multiple occurences
        title = "32-bit Service Data";
        let serviceData32Uuid = bytes.slice(0, 4).toString('hex').match(/.{1,2}/g).reverse().join('');
        let serviceData32 = bytes.slice(4, bytes.length);
        infomations.push("uuid - " + serviceData32Uuid + "<br/>serviceData - " + array2string(serviceData32));

        break;

      case 0x21: // 128-bit Service Data, there can be multiple occurences
        title = "128-bit Service Data";
        let serviceData128Uuid = bytes.slice(0, 16).toString('hex').match(/.{1,2}/g).reverse().join('');
        let serviceData128 = bytes.slice(16, bytes.length);
        infomations.push("uuid - " + serviceData128Uuid + "<br/>serviceData - " + array2string(serviceData128));

        break;


      case 0xff: // 128-bit Service Data, there can be multiple occurences
          if(bytes[0] === 0x4c
             && bytes[1] === 0x00
              && bytes[2] === 0x02
              && bytes[3] === 0x15
              && bytes.length === 25) {
            title = "Manufacturer Specific Data - iBeacon";
            let uuidData = bytes.slice(4, 20);
            let uuid = "";
            for(let i = 0; i< uuidData.length;i++){
              uuid = uuid +uuidData[i].toString(16).padStart(2,"0");
              if(i === (4-1) ||i === (4+2-1) ||i === (4+2*2-1) ||i === (4+2*3-1) ){
                uuid += "-";
              }
            }

            let major = "0x" + ((bytes[20]<<8) + bytes[21]).toString(16).padStart(4,"0");
            let minor = "0x" + ((bytes[22]<<8) + bytes[23]).toString(16).padStart(4,"0");
            let power = "0x" + (bytes[24]).toString(16).padStart(2,"0");


            infomations.push("uuid : " + uuid);
            infomations.push("major : " + major);
            infomations.push("minor : " + minor);
            infomations.push("power : " + power);

          }else{
            title = "Manufacturer Specific Data";
            infomations.push(array2string(row.slice(1)));
          }
        break;


      default :
        title = "unhandled type";
        infomations.push(array2string(row.slice(1)));
        break;

    }
    title += "(0x"+ row[0].toString(16).padStart(2,"0") +")";
    return {title, infomations};
  }

</script>
</body>
</html>

今すぐ実行

HTMLがブラウザで開かれて実行されます。