UNPKG

pulseplot

Version:
1 lines 173 kB
{"version":3,"file":"pulseplot.umd.cjs","sources":["../lib/autorange.js","../lib/utils.js","../lib/dropzone.js","../lib/hexbuffer.js","../lib/histogram.js","../lib/bitbuffer.js","../lib/slicer.js","../lib/builder.js","../lib/rfraw.js","../lib/broadlinkrm.js","../lib/pulseplot.js","../lib/demos.js"],"sourcesContent":["/**\n Determine divisor and SI prefix.\n\n @author Christian W. Zuckschwerdt <zany@triq.net>\n @copyright Christian W. Zuckschwerdt, 2019\n @license\n This program is free software: you can redistribute it and/or modify\n it under the terms of the GNU General Public License as published by\n the Free Software Foundation, either version 2 of the License, or\n (at your option) any later version.\n*/\n\nconst autoranges = [\n { name: 'yotta', scale: 1e24, prefix: 'Y' },\n { name: 'zetta', scale: 1e21, prefix: 'Z' },\n { name: 'exa', scale: 1e18, prefix: 'E' },\n { name: 'peta', scale: 1e15, prefix: 'P' },\n { name: 'tera', scale: 1e12, prefix: 'T' },\n { name: 'giga', scale: 1e9, prefix: 'G' },\n { name: 'mega', scale: 1e6, prefix: 'M' },\n { name: 'kilo', scale: 1e3, prefix: 'k' },\n { name: '', scale: 1e0, prefix: '' },\n { name: 'milli', scale: 1e-3, prefix: 'm' },\n { name: 'micro', scale: 1e-6, prefix: 'µ' },\n { name: 'nano', scale: 1e-9, prefix: 'n' },\n { name: 'pico', scale: 1e-12, prefix: 'p' },\n { name: 'femto', scale: 1e-15, prefix: 'f' },\n { name: 'atto', scale: 1e-18, prefix: 'a' },\n { name: 'zepto', scale: 1e-21, prefix: 'z' },\n { name: 'yocto', scale: 1e-24, prefix: 'y' },\n];\n\n/** Determine divisor and SI prefix. */\nfunction autorange(num, min_int = 10.0) {\n if (num == 0.0)\n return autoranges[8];\n\n num = num / min_int;\n for (let i = 0; i < autoranges.length; ++i) {\n if (num >= autoranges[i].scale)\n return autoranges[i];\n }\n return autoranges[autoranges.length - 1];\n}\n\nexport { autorange };\n\nconst autoranges_time = [\n { name: 'year', scale: 31557513, prefix: 'Y' },\n { name: 'month', scale: 2635200, prefix: 'M' },\n { name: 'day', scale: 86400, prefix: 'D' },\n { name: 'hour', scale: 3600, prefix: 'h' },\n { name: 'minute', scale: 60, prefix: 'm' },\n { name: 'second', scale: 1e0, prefix: 's' },\n { name: 'milli', scale: 1e-3, prefix: 'ms' },\n { name: 'micro', scale: 1e-6, prefix: 'µs' },\n { name: 'nano', scale: 1e-9, prefix: 'ns' },\n { name: 'pico', scale: 1e-12, prefix: 'ps' },\n { name: 'femto', scale: 1e-15, prefix: 'fs' },\n { name: 'atto', scale: 1e-18, prefix: 'as' },\n { name: 'zepto', scale: 1e-21, prefix: 'zs' },\n { name: 'yocto', scale: 1e-24, prefix: 'ys' },\n];\n\n/** Determine SI divisor or Sexagesimal multiplier and suffix. */\nfunction autorange_time(num, min_int = 10.0) {\n if (num == 0.0)\n return autoranges_time[8];\n\n num = num / min_int;\n for (let i = 0; i < autoranges_time.length; ++i) {\n if (num >= autoranges_time[i].scale)\n return autoranges_time[i];\n }\n return autoranges_time[autoranges_time.length - 1];\n}\n\nexport { autorange_time };\n","/**\n @file Various utils.\n\n @author Christian W. Zuckschwerdt <zany@triq.net>\n @copyright Christian W. Zuckschwerdt, 2019\n @license\n This program is free software: you can redistribute it and/or modify\n it under the terms of the GNU General Public License as published by\n the Free Software Foundation, either version 2 of the License, or\n (at your option) any later version.\n*/\n\n/** Returns element or expands selector. */\nexport function selector(elementOrSelector) {\n if (!elementOrSelector) {\n return elementOrSelector\n } else if (typeof elementOrSelector === 'string') {\n return document.querySelector(elementOrSelector)\n } else {\n return elementOrSelector // instanceof Element\n }\n}\n\n/** Returns array or expands key. */\nexport function lookup(table, arrayOrKey) {\n if (!arrayOrKey)\n return arrayOrKey\n if (typeof arrayOrKey !== 'string')\n return arrayOrKey\n if (table[arrayOrKey])\n return table[arrayOrKey]\n const match = arrayOrKey.toLowerCase()\n for (let key in table)\n if (key.toLowerCase() == match)\n return table[key]\n for (let key in table)\n if (key.toLowerCase().startsWith(match))\n return table[key]\n return null\n}\n\n/** Strips HTML tags. */\nexport function strip(html) {\n var doc = new DOMParser().parseFromString(html, 'text/html');\n return doc.body.textContent || '';\n}\n","/**\n @file DropZone JS.\n\n @author Christian W. Zuckschwerdt <zany@triq.net>\n @copyright Christian W. Zuckschwerdt, 2019\n @license\n This program is free software: you can redistribute it and/or modify\n it under the terms of the GNU General Public License as published by\n the Free Software Foundation, either version 2 of the License, or\n (at your option) any later version.\n*/\n\n/*eslint no-console: \"off\"*/\n\nimport { selector } from './utils.js'\n\nexport class DropZone {\n constructor(elementOrSelector, fileLoader) {\n elementOrSelector = elementOrSelector || '#dropZone'\n elementOrSelector = selector(elementOrSelector)\n\n // https://css-tricks.com/drag-and-drop-file-uploading/\n this.dropZone = elementOrSelector\n this.fileLoader = fileLoader\n this.inputEls = []\n this.dragEnteredEls = []\n\n window.addEventListener('dragenter', this)\n window.addEventListener('dragleave', this)\n window.addEventListener('dropleave', this)\n\n const events = ['dragenter', 'dragover', 'dragleave', 'drop']\n events.forEach(evt => this.dropZone.addEventListener(evt, this, false))\n }\n\n addInput(elementOrSelector) {\n elementOrSelector = elementOrSelector || '#inputfile'\n elementOrSelector = selector(elementOrSelector)\n\n elementOrSelector.addEventListener('change', this, false)\n this.inputEls.push(elementOrSelector)\n }\n\n destroy() {\n window.removeEventListener('dragenter', this, false)\n window.removeEventListener('dragleave', this, false)\n window.removeEventListener('dropleave', this, false)\n\n const events = ['dragenter', 'dragover', 'dragleave', 'drop']\n events.forEach(evt => this.dropZone.removeEventListener(evt, this, false))\n\n for (let el of this.inputEls) {\n el.removeEventListener('change', this, false)\n }\n }\n\n allowDrag(e) {\n // ...Test that the item being dragged is a valid one\n e.dataTransfer.dropEffect = 'copy'\n e.preventDefault()\n }\n\n handleDrop(e) {\n e.preventDefault()\n this.handleFileSelect(e)\n }\n\n // https://www.html5rocks.com/en/tutorials/file/dndfiles/\n handleFileSelect(evt) {\n evt.stopPropagation()\n evt.preventDefault()\n\n //const files = evt.dataTransfer.files // FileList object.\n //const files = evt.target.files // FileList object\n const files = ('dataTransfer' in evt) ? evt.dataTransfer.files : evt.target.files\n\n // files is a FileList of File objects. List some properties.\n for (let i = 0, file; (file = files[i]); i++) {\n // output some info right away?\n\n // Blob.arrayBuffer() promise isn't generally supported\n // use older FileReader.readAsArrayBuffer()\n const reader = new FileReader()\n\n // Closure to capture the file information.\n reader.onload = (e) => {\n // Process data\n file['fileBuffer'] = e.target.result\n //console.log(file.name)\n\n this.fileLoader(file)\n }\n\n // Read in the image file as a data URL.\n reader.readAsArrayBuffer(file)\n }\n }\n\n // events\n\n handleEvent(e) {\n const handler = e.type\n if (typeof this[handler] === 'function') {\n e.preventDefault()\n return this[handler](e)\n }\n }\n\n dragenter(e) {\n this.dragEnteredEls.push(e.target)\n\n document.documentElement.classList.add('dragdrop')\n this.dropZone.classList.add('active')\n }\n\n dragover(e) {\n this.dropZone.classList.add('hover')\n this.allowDrag(e)\n }\n\n dragleave(e) {\n const index = this.dragEnteredEls.indexOf(e.target)\n if (index > -1) {\n this.dragEnteredEls.splice(index, 1)\n }\n if (this.dragEnteredEls.indexOf(this.dropZone) < 0) {\n this.dropZone.classList.remove('hover')\n }\n if (this.dragEnteredEls.length === 0) {\n document.documentElement.classList.remove('dragdrop')\n this.dropZone.classList.remove('active')\n }\n }\n\n dropleave() {\n this.dragEnteredEls.splice(0)\n this.dropZone.classList.remove('hover')\n this.dropZone.classList.remove('active')\n document.documentElement.classList.remove('dragdrop')\n }\n\n drop(e) {\n const event = new Event('dropleave')\n window.dispatchEvent(event)\n\n this.handleDrop(e)\n }\n\n change(e) {\n this.handleFileSelect(e)\n }\n}\n","/**\n @file Hexbuffer JS.\n\n @author Christian W. Zuckschwerdt <zany@triq.net>\n @copyright Christian W. Zuckschwerdt, 2020\n @license\n This program is free software: you can redistribute it and/or modify\n it under the terms of the GNU General Public License as published by\n the Free Software Foundation, either version 2 of the License, or\n (at your option) any later version.\n*/\n\nfunction dec2hex(i, w = 2) {\n i = Math.round(i)\n return (i + 0x10000).toString(16).substr(-w).toUpperCase()\n}\n\nexport class Hexbuffer {\n constructor(line = '') {\n this.fromString(line)\n }\n fromString(s) {\n this.line = s.replace(/\\s/g, '')\n this.index = 0\n }\n\n hasNibble() {\n return this.index + 1 <= this.line.length\n }\n hasByte() {\n return this.index + 2 <= this.line.length\n }\n hasWord() {\n return this.index + 4 <= this.line.length\n }\n\n peekNibble() {\n return parseInt(this.line.substr(this.index, 1), 16)\n }\n peekByte() {\n return parseInt(this.line.substr(this.index, 2), 16)\n }\n peekWord() {\n return parseInt(this.line.substr(this.index, 4), 16)\n }\n\n getNibble() {\n const r = parseInt(this.line.substr(this.index, 1), 16)\n this.index += 1\n return r\n }\n getByte() {\n const r = parseInt(this.line.substr(this.index, 2), 16)\n this.index += 2\n return r\n }\n getWord() {\n const r = parseInt(this.line.substr(this.index, 4), 16)\n this.index += 4\n return r\n }\n\n pushNibble(v) {\n this.line += dec2hex(v, 1)\n }\n pushByte(v) {\n this.line += dec2hex(v, 2)\n }\n pushWord(v) {\n this.line += dec2hex(v, 4)\n }\n}\n","/**\n @file Histogram JS.\n\n @author Christian W. Zuckschwerdt <zany@triq.net>\n @copyright Christian W. Zuckschwerdt, 2020\n @license\n This program is free software: you can redistribute it and/or modify\n it under the terms of the GNU General Public License as published by\n the Free Software Foundation, either version 2 of the License, or\n (at your option) any later version.\n*/\n\nimport { Hexbuffer } from './hexbuffer.js'\n\n/*eslint no-console: \"off\"*/\n\nconst max_hist_bins = 16\n\n/// Histogram data for single bin\nclass Bin {\n constructor(num) {\n if (typeof num !== 'undefined') {\n this.count = 1\n this.sum = num\n this.mean = num\n this.devi = 0\n this.min = num\n this.max = num\n } else {\n this.count = 0\n this.sum = 0\n this.mean = null\n this.devi = 0\n this.min = null\n this.max = null\n }\n }\n add(num) {\n this.count++\n this.sum += num\n this.mean = this.sum / this.count\n this.min = this.min === null ? num : Math.min(num, this.min)\n this.max = this.max === null ? num : Math.max(num, this.max)\n this.devi = (this.max - this.min) / 2\n }\n fuse(bin) {\n this.count += bin.count\n this.sum += bin.sum\n this.mean = this.sum / this.count\n this.min = Math.min(this.min, bin.min)\n this.max = Math.max(this.max, bin.max)\n this.devi = (this.max - this.min) / 2\n }\n contains(num) {\n return num >= this.min && num <= this.max\n }\n}\n\n/// Histogram data for all bins\nexport class Histogram {\n constructor(data, tolerance = 0.2) {\n this.bins = []\n this.histogram_sum(data, tolerance)\n }\n\n get length() {\n return this.bins.length\n }\n\n /// Generate a histogram (unsorted)\n histogram_sum(data, tolerance = 0.2) {\n const len = data.length\n for (let n = 0; n < len; ++n) {\n // Search for match in existing bins\n let bin\n for (bin = 0; bin < this.bins.length; ++bin) {\n const bn = data[n]\n const bm = this.bins[bin].mean\n if (Math.abs(bn - bm) < (tolerance * Math.max(bn, bm))) {\n this.bins[bin].add(data[n])\n break // Match found! Data added to existing bin\n }\n }\n // No match found? Add new bin\n if (bin == this.bins.length && bin < max_hist_bins) {\n this.bins.push(new Bin(data[n]))\n }\n }\n }\n\n /// Delete bin from histogram\n delete_bin(index) {\n this.bins.splice(index, 1)\n }\n\n /// Swap two bins in histogram\n swap_bins(index1, index2) {\n if ((index1 < this.bins.length) && (index2 < this.bins.length)) { // Avoid out of bounds\n const tempbin = this.bins[index1]\n this.bins[index1] = this.bins[index2]\n this.bins[index2] = tempbin\n }\n }\n\n /// Sort histogram with mean value (order lowest to highest)\n sort_mean() {\n if (this.bins.length < 2) return // Avoid underflow\n // Compare all bins (bubble sort)\n for (let n = 0; n < this.bins.length - 1; ++n) {\n for (let m = n + 1; m < this.bins.length; ++m) {\n if (this.bins[m].mean < this.bins[n].mean) {\n this.swap_bins(m, n)\n }\n }\n }\n }\n\n /// Sort histogram with count value (order lowest to highest)\n sort_count() {\n if (this.bins.length < 2) return // Avoid underflow\n // Compare all bins (bubble sort)\n for (let n = 0; n < this.bins.length - 1; ++n) {\n for (let m = n + 1; m < this.bins.length; ++m) {\n if (this.bins[m].count < this.bins[n].count) {\n this.swap_bins(m, n)\n }\n }\n }\n }\n\n /// Fuse histogram bins with means within tolerance\n fuse_bins(tolerance = 0.2) {\n if (this.bins.length < 2) return // Avoid underflow\n // Compare all bins\n for (let n = 0; n < this.bins.length - 1; ++n) {\n for (let m = n + 1; m < this.bins.length; ++m) {\n const bn = this.bins[n].mean\n const bm = this.bins[m].mean\n // if within tolerance\n if (Math.abs(bn - bm) < (tolerance * Math.max(bn, bm))) {\n // Fuse data for bin[n] and bin[m]\n this.bins[n].fuse(this.bins[m])\n // Delete bin[m]\n this.delete_bin(m)\n m-- // Compare new bin in same place!\n }\n }\n }\n }\n\n /// Trim zero-width bins\n trim_bins(tolerance = 0) {\n for (let n = 0; n < this.bins.length; ++n) {\n // if within tolerance\n if (this.bins[n].mean <= tolerance) {\n // Delete bin[n]\n this.delete_bin(n)\n }\n }\n }\n\n /// Find bin index\n find_bin_index(width) {\n for (let n = 0; n < this.bins.length; ++n) {\n if (this.bins[n].contains(width)) {\n return n\n }\n }\n return -1\n }\n\n /// Print a histogram\n console_print() {\n for (let n = 0; n < this.bins.length; ++n) {\n const b = this.bins[n]\n console.log(`[${n}] ${b.count} × ${b.mean.toFixed(1)} ±${b.devi.toFixed(1)} µs [${b.min};${b.max}]`)\n }\n }\n\n string_print(separator = ', ') {\n let ret = []\n for (let n = 0; n < this.bins.length; ++n) {\n const b = this.bins[n]\n ret.push(`${b.count}× ${b.mean.toFixed(1)} <small>±${b.devi.toFixed(1)}</small> µs`)\n }\n return ret.join(separator)\n }\n\n}\n\nexport class Analyzer {\n constructor(data, tolerance = 0.2) {\n this.analyse_pulses(data, tolerance)\n this.create_rfraw(data)\n }\n\n /// Create histograms from pulse data\n analyse_pulses(data, messages, tolerance = 0.2) {\n // Generate pulse/gap/period data\n this.pulses = []\n this.gaps = []\n this.periods = []\n this.pulse_sum = 0\n this.gap_sum = 0\n // Leave out last gap (end)\n for (let j = 0; j < data.length - 2; j += 2) {\n const m = data[j] // mark\n const s = data[j + 1] // space\n this.pulses.push(m)\n this.gaps.push(s)\n this.periods.push(m + s)\n this.pulse_sum += m\n this.gap_sum += s\n }\n const m = data[data.length - 2] // last mark\n //const s = data[data.length - 1] // last space\n this.pulses.push(m)\n this.pulse_sum += m\n //this.gap_sum += s\n this.pulse_gap_ratio = this.pulse_sum / this.gap_sum\n this.pulse_gap_skew = this.pulse_gap_ratio - 1\n\n // Generate statistics\n this.hist_pulses = new Histogram(this.pulses, tolerance)\n this.hist_gaps = new Histogram(this.gaps, tolerance)\n this.hist_periods = new Histogram(this.periods, tolerance)\n this.hist_timings = new Histogram(data, tolerance)\n\n // Trim zero-width bins\n this.hist_pulses.trim_bins(tolerance)\n this.hist_gaps.trim_bins(tolerance)\n this.hist_periods.trim_bins(tolerance)\n this.hist_timings.trim_bins(tolerance)\n\n // Fuse overlapping bins\n this.hist_pulses.fuse_bins(tolerance)\n this.hist_gaps.fuse_bins(tolerance)\n this.hist_periods.fuse_bins(tolerance)\n this.hist_timings.fuse_bins(tolerance)\n }\n\n guess() {\n const pulses = this.hist_pulses\n const gaps = this.hist_gaps\n const periods = this.hist_periods\n pulses.sort_mean() // Easier to work with sorted data\n gaps.sort_mean()\n if (pulses.bins.length > 0 && pulses.bins[0].mean == 0) {\n pulses.delete_bin(0) // Remove FSK initial zero-bin\n }\n //if (pulses.bins[0].mean <= 9 && pulses.bins[0].count <= 2) {\n // pulses.delete_bin(0) // Remove stray pulses\n //}\n\n // Attempt to find a matching modulation\n //console.log(`${pulses.length} ${gaps.length} ${periods.length}`)\n if (this.pulses.length == 1) {\n return {\n name: 'Single pulse detected. Probably Frequency Shift Keying or just noise...',\n }\n }\n else if (pulses.length == 1 && gaps.length == 1) {\n return {\n name: 'Un-modulated signal. Maybe a preamble...',\n }\n }\n else if (pulses.length == 1 && gaps.length > 1) {\n return {\n name: 'Pulse Position Modulation with fixed pulse width',\n modulation: 'PPM',\n short: gaps.bins[0].mean,\n long: gaps.bins[1].mean,\n gap: gaps.bins[1].max * 1.2, // Set limit above next lower gap\n reset: gaps.bins[gaps.length - 1].max * 1.2, // Set limit above biggest gap\n }\n }\n else if (pulses.length == 2 && gaps.length == 1) {\n const short = pulses.bins[0].mean\n const long = pulses.bins[1].mean\n return {\n name: 'Pulse Width Modulation with fixed gap',\n modulation: 'PWM',\n short: short,\n long: long,\n tolerance: (long - short) * 0.4,\n reset: gaps.bins[gaps.length - 1].max * 1.2, // Set limit above biggest gap\n }\n }\n else if (pulses.length == 2 && gaps.length == 2 && periods.length == 1) {\n const short = pulses.bins[0].mean\n const long = pulses.bins[1].mean\n return {\n name: 'Pulse Width Modulation with fixed period',\n modulation: 'PWM',\n short: short,\n long: long,\n tolerance: (long - short) * 0.4,\n reset: gaps.bins[gaps.length - 1].max * 1.2, // Set limit above biggest gap\n }\n }\n else if (pulses.length == 2 && gaps.length == 2 && periods.length == 3) {\n const short = pulses.bins[0].mean\n return {\n name: 'Manchester coding (PCM)',\n modulation: 'MC',\n short: short, // Assume shortest pulse is half period\n long: short, // Not used\n reset: gaps.bins[gaps.length - 1].max * 1.2, // Set limit above biggest gap\n }\n }\n else if (pulses.length == 2 && gaps.length >= 3) {\n const short = pulses.bins[0].mean\n const long = pulses.bins[1].mean\n return {\n name: 'Pulse Width Modulation with multiple packets',\n modulation: 'PWM',\n short: short,\n long: long,\n gap: gaps.bins[1].max * 1.2, // Set limit above second gap\n tolerance: (long - short) * 0.4,\n reset: gaps.bins[gaps.length - 1].max * 1.2, // Set limit above biggest gap\n }\n }\n else if ((pulses.length >= 3 && gaps.length >= 3)\n && (Math.abs(pulses.bins[1].mean - 2 * pulses.bins[0].mean) <= pulses.bins[0].mean / 8) // Pulses are multiples of shortest pulse\n && (Math.abs(pulses.bins[2].mean - 3 * pulses.bins[0].mean) <= pulses.bins[0].mean / 8)\n && (Math.abs(gaps.bins[0].mean - pulses.bins[0].mean) <= pulses.bins[0].mean / 8) // Gaps are multiples of shortest pulse\n && (Math.abs(gaps.bins[1].mean - 2 * pulses.bins[0].mean) <= pulses.bins[0].mean / 8)\n && (Math.abs(gaps.bins[2].mean - 3 * pulses.bins[0].mean) <= pulses.bins[0].mean / 8)) {\n return {\n name: 'Pulse Code Modulation (Not Return to Zero)',\n modulation: 'PCM',\n short: pulses.bins[0].mean, // Shortest pulse is bit width\n long: pulses.bins[0].mean, // Bit period equal to pulse length (NRZ)\n reset: pulses.bins[0].mean * 1024, // No limit to run of zeros...\n }\n }\n else if (pulses.length == 3) {\n // Re-sort to find lowest pulse count index (is probably delimiter)\n pulses.sort_count()\n const p1 = pulses.bins[1].mean\n const p2 = pulses.bins[2].mean\n const short = p1 < p2 ? p1 : p2 // Set to shorter pulse width\n const long = p1 < p2 ? p2 : p1 // Set to longer pulse width\n return {\n name: 'Pulse Width Modulation with sync/delimiter',\n modulation: 'PWM',\n short: short,\n long: long,\n sync: pulses.bins[0].mean, // Set to lowest count pulse width\n reset: gaps.bins[gaps.length - 1].max * 1.2, // Set limit above biggest gap\n }\n }\n else {\n return {\n name: 'No clue...',\n }\n }\n }\n\n create_rfraw(data) {\n const timings = this.hist_timings\n\n if (timings.bins.length < 1) {\n return ''\n }\n if (timings.bins.length > 8) {\n return ''\n }\n if (data.length > 494) {\n return ''\n }\n\n let raw = new Hexbuffer()\n\n for (let b of timings.bins) {\n raw.pushWord(b.mean)\n }\n\n for (let j = 0; j < data.length - 1; j += 2) {\n const m = data[j] // mark\n const s = data[j + 1] // space\n\n const mi = timings.find_bin_index(m)\n if (mi >= 0) {\n raw.pushNibble(mi | 8)\n } else if (m == 0) {\n // skip 0-length, otherwise error on invalid bucket index\n } else {\n console.error('RfRaw encoding mark bucket not found:', m, mi);\n }\n\n const si = timings.find_bin_index(s)\n if (si >= 0) {\n raw.pushNibble(si)\n } else if (s == 0) {\n // skip 0-length, otherwise error on invalid bucket index\n } else {\n console.error('RfRaw encoding space bucket not found:', s, si);\n }\n }\n // Pad buffer to full bytes by adding some space if needed\n if (raw.line.length % 2) {\n raw.pushNibble(0)\n }\n\n raw.pushByte(0x55)\n\n let raw0 = new Hexbuffer()\n raw0.pushByte(0xaa)\n raw0.pushByte(0xb0)\n raw0.pushByte(2 + raw.line.length / 2 - 1)\n raw0.pushByte(timings.bins.length)\n raw0.pushByte(1) // repeats\n\n let raw1 = new Hexbuffer()\n raw1.pushByte(0xaa)\n raw1.pushByte(0xb1)\n raw1.pushByte(timings.bins.length)\n\n this.rfrawB0 = raw0.line + raw.line\n this.rfrawB1 = raw1.line + raw.line\n }\n\n console_log() {\n const guess = this.guess()\n console.log('Analyzing pulses...')\n console.log(`Total count: ${this.pulses.length}`)\n console.log('Pulse width distribution:')\n this.hist_pulses.console_print()\n console.log('Gap width distribution:')\n this.hist_gaps.console_print()\n console.log('Pulse period distribution:')\n this.hist_periods.console_print()\n console.log('Pulse timing distribution:')\n this.hist_timings.console_print()\n console.log(`DC bias (Pulse/Gap skew): ${(this.pulse_gap_skew * 100).toFixed(1)}`)\n console.log('Guessing modulation:')\n console.log(guess)\n }\n\n print_plain(messages) {\n const guess = this.guess()\n messages.innerHTML = `\n <div>Pulses: ${this.hist_pulses.string_print()}</div>\n <div>Gaps: ${this.hist_gaps.string_print()}</div>\n <div>Periods: ${this.hist_periods.string_print()}</div>\n <div>Timings: ${this.hist_timings.string_print()}</div>\n <div>${guess.name}</div>\n `\n }\n\n/*\nconst locale = new Intl.NumberFormat().resolvedOptions().locale\nconst formatter = new Intl.NumberFormat(locale, {\n style: 'percent',\n signDisplay: 'exceptZero',\n maximumFractionDigits: 1,\n})\nformatter.format(0.5)\n*/\n print(timings, messages) {\n const guess = this.guess()\n if (timings) {\n timings.innerHTML = `<table>\n <tr><th>Pulses</th><td>${this.hist_pulses.string_print('</td><td>')}</td></tr>\n <tr><th>Gaps</th><td>${this.hist_gaps.string_print('</td><td>')}</td></tr>\n <tr><th>Periods</th><td>${this.hist_periods.string_print('</td><td>')}</td></tr>\n <tr><th>Timings</th><td>${this.hist_timings.string_print('</td><td>')}</td></tr>\n </table>\n `\n }\n if (messages) {\n messages.innerHTML = `\n <div><small>DC bias (Pulse/Gap skew): ${(this.pulse_gap_skew * 100).toFixed(1)}%</small><br>\n Guessing modulation: <strong>${guess.name}</strong><br>\n modulation: <strong>${guess.modulation}</strong>\n short: <strong>${guess.short ? guess.short.toFixed(1) : '-'}</strong>\n long: <strong>${guess.long ? guess.long.toFixed(1) : '-'}</strong>\n sync: <strong>${guess.sync ? guess.sync.toFixed(1) : '-'}</strong>\n gap: <strong>${guess.gap ? guess.gap.toFixed(1) : '-'}</strong>\n reset: <strong>${guess.reset ? guess.reset.toFixed(1) : '-'}</strong><br>\n <small>RfRaw (rx): <strong>${this.rfrawB1 ? this.rfrawB1 : '-'}</strong></small><br>\n <small>RfRaw (tx): <strong>${this.rfrawB0 ? this.rfrawB0 : '-'}</strong></small>\n </div>\n `\n }\n }\n}\n","/**\n @file Bitbuffer JS.\n\n @author Christian W. Zuckschwerdt <zany@triq.net>\n @copyright Christian W. Zuckschwerdt, 2020\n @license\n This program is free software: you can redistribute it and/or modify\n it under the terms of the GNU General Public License as published by\n the Free Software Foundation, either version 2 of the License, or\n (at your option) any later version.\n*/\n\n//function dec2hex(i) {\n// return (i + 0x100).toString(16).substr(-2).toUpperCase()\n//}\n\nexport class Bitbuffer {\n constructor(bytes = [], len = 0) {\n if (Array.isArray(bytes)) {\n this.bytes = bytes\n this.len = len || bytes.length * 8\n } else {\n this.fromString(bytes)\n }\n }\n fromString(s) {\n this.bytes = []\n this.len = 0\n let len = -1\n s = s.trim()\n // parse length\n if (s.startsWith('{')) {\n const end = s.indexOf('}')\n if (end < 0) return\n len = parseInt(s.slice(1), 10)\n s = s.slice(end + 1)\n }\n // skip 0x prefix\n if (s.startsWith('0x')) {\n s = s.slice(2)\n }\n // parse nibbles\n for (let c of s) {\n const n = parseInt(c, 16)\n this.pushNibble(n)\n }\n // set length if given\n if (len >= 0)\n this.len = len\n }\n pushZero() {\n this.push(0)\n }\n pushOne() {\n this.push(1)\n }\n pushSymbol(s) {\n if (s == '0')\n this.push(0)\n else if (s == '1')\n this.push(1)\n }\n push(bit) {\n bit = bit ? 0x80 : 0\n this.bytes[~~(this.len / 8)] |= bit >> (this.len % 8)\n this.len += 1\n }\n pushNibble(n) {\n for (let j = 3; j >= 0; --j) {\n this.push((n >> j) & 1)\n }\n }\n pushByte(n) {\n for (let j = 7; j >= 0; --j) {\n this.push((n >> j) & 1)\n }\n }\n pushBreak() {\n const b = ~~((this.len + 7) / 8)\n this.bytes[b] = -1\n this.len = (b + 1) * 8\n }\n toBitArray() {\n let bits = []\n for (let j = 0; j < this.len; ++j) {\n const byte = this.bytes[~~(j / 8)] || 0\n const bit = (byte >> (7 - (j % 8))) & 1\n bits.push(bit)\n }\n return bits\n }\n toHexString() {\n let s = `{${this.len}}`\n for (let j = 0; j < this.len; j += 8) {\n const b = this.bytes[~~(j / 8)] || 0\n if (b < 0) {\n s += ' / '\n } else {\n s += ' '\n s += (b >> 4).toString(16).toUpperCase()\n if (j + 4 < this.len)\n s += (b & 0xf).toString(16).toUpperCase()\n }\n }\n return s\n }\n}\n","/**\n @file Pulse Slicer JS.\n\n @author Christian W. Zuckschwerdt <zany@triq.net>\n @copyright Christian W. Zuckschwerdt, 2020\n @license\n This program is free software: you can redistribute it and/or modify\n it under the terms of the GNU General Public License as published by\n the Free Software Foundation, either version 2 of the License, or\n (at your option) any later version.\n*/\n\nimport { Bitbuffer } from './bitbuffer.js'\n\nexport function sliceGuess(pulses, guess) {\n if (guess.modulation == 'PCM')\n return slicePCM(pulses, guess)\n\n else if (guess.modulation == 'MC')\n return sliceMC(pulses, guess)\n\n else if (guess.modulation == 'PPM')\n return slicePPM(pulses, guess)\n\n else if (guess.modulation == 'PWM')\n return slicePWM(pulses, guess)\n\n else if (guess.modulation == 'DM')\n return sliceDM(pulses, guess)\n\n else if (guess.modulation == 'NRZI')\n return sliceNRZI(pulses, guess)\n\n else if (guess.modulation == 'CMI')\n return sliceCMI(pulses, guess)\n\n else if (guess.modulation == 'PIWM')\n return slicePIWM(pulses, guess)\n\n else\n return []\n}\n\n// returned hints array contains triples of start,end,symbol\n\n/// Pulse-code modulation (PCM)\n/// https://en.wikipedia.org/wiki/Pulse-code_modulation\n/// either NRZ or RZ\nexport function slicePCM(pulses, guess) {\n if (!guess.long || guess.long == guess.short) {\n return sliceNRZ(pulses, guess)\n } else {\n return sliceRZ(pulses, guess)\n }\n}\n\n/// NRZ(L) NRZL Non-return-to-zero level\n/// https://en.wikipedia.org/wiki/Non-return-to-zero\nexport function sliceNRZ(pulses, guess) {\n const short = guess.short\n const gap = guess.gap\n\n const bits = new Bitbuffer()\n let hints = []\n\n let x = 0\n for (let j = 0; j < pulses.length; j += 1) {\n const symbol = 1 - j % 2 // even: 1, odd: 0\n const w = pulses[j] // mark or space\n if (gap && w > gap) {\n bits.pushBreak()\n } else {\n const cnt = ~~(w / short + 0.5)\n for (let k = 0; k < cnt; ++k) {\n hints.push([x + w / cnt * k, x + w / cnt * (k + 1), symbol])\n bits.push(symbol)\n }\n }\n x += w\n }\n\n return { hints, bits }\n}\n\n/// Return-to-zero level\n/// https://en.wikipedia.org/wiki/Return-to-zero\nexport function sliceRZ(pulses, guess) {\n const short = guess.short\n const long = guess.long\n const gap = guess.gap\n\n const shortl = short * 0.5\n const shortu = short * 1.5\n\n const bits = new Bitbuffer()\n let hints = []\n\n let x = 0\n for (let j = 0; j < pulses.length; j += 2) {\n const m = pulses[j] // mark\n const s = pulses[j + 1] // space\n if (m < shortl || m > shortu) {\n bits.pushBreak()\n x += m + s\n continue\n }\n let onew = m * long / short // estimate the 1-bit width\n let zs = s + m - onew // estimate 0-bits width\n if (zs < long / 2) {\n onew = m + s // no 0-bits\n zs = 0\n }\n hints.push([x, x + onew, '1'])\n bits.pushOne()\n x += onew\n if (gap && s > gap) {\n bits.pushBreak()\n x += zs\n continue\n }\n const cnt = ~~(zs / long + 0.5)\n for (let k = 0; k < cnt; ++k) {\n hints.push([x + zs * k / cnt, x + zs * (k + 1) / cnt, '0'])\n bits.pushZero()\n }\n x += zs\n }\n\n return { hints, bits }\n}\n\n/// Pulse-position modulation (PPM)\n/// https://en.wikipedia.org/wiki/Pulse-position_modulation\nexport function slicePPM(pulses, guess) {\n const short = guess.short\n const long = guess.long\n const sync = guess.sync\n const gap = guess.gap\n\n const shortl = short * 0.5\n const shortu = short * 1.5\n const longl = long * 0.5\n const longu = long * 1.5\n const syncl = sync * 0.5\n const syncu = sync * 1.5\n\n const bits = new Bitbuffer()\n let hints = []\n\n let x = 0\n for (let j = 0; j < pulses.length; j += 2) {\n const m = pulses[j] // mark\n const s = pulses[j + 1] // space\n const x0 = x\n x += m + s\n if (s > shortl && s < shortu) {\n hints.push([x0, x, '1'])\n bits.pushOne()\n } else if (s > longl && s < longu) {\n hints.push([x0, x, '0'])\n bits.pushZero()\n } else if (s > syncl && s < syncu) {\n hints.push([x0, x, 'X'])\n bits.pushBreak()\n } else if (gap && s > gap) {\n bits.pushBreak()\n }\n }\n\n return { hints, bits }\n}\n\n/// Pulse-width modulation (PWM)\n/// https://en.wikipedia.org/wiki/Pulse-width_modulation\nexport function slicePWM(pulses, guess) {\n const short = guess.short\n const long = guess.long\n const sync = guess.sync\n const gap = guess.gap\n\n const shortl = short * 0.5\n const shortu = short * 1.5\n const longl = long * 0.5\n const longu = long * 1.5\n const syncl = sync * 0.5\n const syncu = sync * 1.5\n\n const bits = new Bitbuffer()\n let hints = []\n\n let x = 0\n for (let j = 0; j < pulses.length; j += 2) {\n const m = pulses[j] // mark\n const s = pulses[j + 1] // space\n\n const x0 = x\n let x1 = x + m + s\n // break on gaps\n if (s > gap)\n x1 = x + m + gap\n x += m + s\n\n if (m > shortl && m < shortu) {\n hints.push([x0, x1, '1'])\n bits.pushOne()\n } else if (m > longl && m < longu) {\n hints.push([x0, x1, '0'])\n bits.pushZero()\n } else if (m > syncl && m < syncu) {\n hints.push([x0, x1, 'X'])\n bits.pushBreak()\n }\n if (gap && s > gap) {\n bits.pushBreak()\n }\n }\n\n return { hints, bits }\n}\n\n/// get Manchester alignment, 1 if we are at the start of a bit, 0 if we are in the middle\nfunction manchesterAligned(pulses, offset, short) {\n for (let j = offset; j < pulses.length; j += 2) {\n const mw = pulses[j] // mark\n const cw = ~~(mw / short + 0.5)\n if (cw > 1) return 0 // middle\n const sw = pulses[j + 1] // space\n const sc = ~~(sw / short + 0.5)\n if (sc > 1) return 1 // start\n }\n // warning, no alignment found\n return 0\n}\n\n/// Manchester code (MC)\n/// https://en.wikipedia.org/wiki/Manchester_code\nexport function sliceMC(pulses, guess) {\n const short = guess.short\n const bits = new Bitbuffer()\n let hints = []\n\n // Manchester align string by finding the position of the first long pulse or gap\n let aligned = manchesterAligned(pulses, 0, short)\n\n let x = 0\n let x1 = 0\n for (let j = 0; j < pulses.length; j += 2) {\n const mark = pulses[j] // mark\n const mcnt = ~~(mark / short + 0.5)\n const space = pulses[j + 1] // space\n const scnt = ~~(space / short + 0.5)\n\n if (mcnt == 1) {\n if (!aligned) {\n hints.push([x1, x + mark, '0'])\n bits.pushZero()\n x1 = x + mark\n }\n else { // aligned\n x1 = x\n }\n aligned = !aligned\n }\n else if (mcnt == 2) {\n if (!aligned) {\n hints.push([x1, x + mark / 2, '0'])\n bits.pushZero()\n x1 = x + mark / 2\n }\n else { // aligned\n // error\n bits.pushBreak()\n x1 = x + mark / 2\n }\n aligned = false\n }\n else if (mcnt > 2) {\n if (!aligned) {\n hints.push([x1, x + mark / mcnt, '0'])\n bits.pushZero()\n x1 = x + mark - mark / mcnt\n }\n else { // aligned\n // error\n x1 = x + mark - mark / mcnt\n }\n bits.pushBreak()\n aligned = manchesterAligned(pulses, j + 1, short)\n }\n\n if (scnt == 1) {\n if (!aligned) {\n hints.push([x1, x + mark + space, '1'])\n bits.pushOne()\n x1 = x + mark + space\n }\n else { // aligned\n x1 = x + mark\n }\n aligned = !aligned\n }\n else if (scnt == 2) {\n if (!aligned) {\n hints.push([x1, x + mark + space / 2, '1'])\n bits.pushOne()\n x1 = x + mark + space / 2\n }\n else { // aligned\n // error\n bits.pushBreak()\n x1 = x + mark + space / 2\n }\n aligned = false\n }\n else if (scnt > 2) {\n if (!aligned) {\n hints.push([x1, x + mark + space / scnt, '1'])\n bits.pushOne()\n x1 = x + mark + space - space / scnt\n }\n else { // aligned\n // error\n x1 = x + mark + space - space / scnt\n }\n bits.pushBreak()\n aligned = manchesterAligned(pulses, j + 1, short)\n }\n\n x += mark + space\n }\n\n return { hints, bits }\n}\n\n/// Differential Manchester Encoding (DM) aka Biphase Mark Code (CC)\n/// https://en.wikipedia.org/wiki/Differential_Manchester_encoding\nexport function sliceDM(pulses, guess) {\n const short = guess.short\n const bits = new Bitbuffer()\n let hints = []\n\n let x = 0\n let x1 = null\n for (let j = 0; j < pulses.length; j += 2) {\n const mark = pulses[j] // mark\n const mcnt = ~~(mark / short + 0.5)\n const space = pulses[j + 1] // space\n const scnt = ~~(space / short + 0.5)\n\n if (!x1 && mcnt == 1 && scnt == 1) {\n hints.push([x, x + mark + space, '0'])\n bits.pushZero()\n }\n else if (mcnt == 1 && scnt == 1) {\n hints.push([x1, x + mark, '0'])\n bits.pushZero()\n x1 = x + mark\n }\n else if (x1 && mcnt == 1 && scnt == 2) {\n hints.push([x1, x + mark, '0'])\n bits.pushZero()\n hints.push([x + mark, x + mark + space, '1'])\n bits.pushOne()\n x1 = null\n }\n else if (mcnt == 2 && scnt == 1) {\n hints.push([x, x + mark, '1'])\n bits.pushOne()\n x1 = x + mark\n }\n else if (mcnt == 2 && scnt == 2) {\n hints.push([x, x + mark, '1'])\n bits.pushOne()\n hints.push([x + mark, x + mark + space, '1'])\n bits.pushOne()\n }\n else if (!x1 && mcnt == 1) {\n // error\n hints.push([x, x + mark + short, '0'])\n bits.pushZero()\n bits.pushBreak()\n }\n else if (!x1 && mcnt == 2) {\n // error\n hints.push([x, x + mark, '1'])\n bits.pushOne()\n bits.pushBreak()\n }\n else {\n // error (!x1 && mcnt == 1 && scnt == 2)\n if (x1) {\n hints.push([x1, x1 + short * 2, '0'])\n bits.pushZero()\n }\n x1 = null\n bits.pushBreak()\n }\n x += mark + space\n }\n\n return { hints, bits }\n}\n\n/// Non-return-to-zero, inverted (NRZI) https://en.wikipedia.org/wiki/Non-return-to-zero#NRZI\n/// NRZ(I) \tNRZI \tNon-return-to-zero inverted \tRefers to either an NRZ(M) or NRZ(S) code.\n/// NRZ(M) \tNRZM \tNon-return-to-zero mark \tSerializer mapping {0: constant, 1: toggle}.\n/// NRZ(S) \tNRZS \tNon-return-to-zero space \tSerializer mapping {0: toggle, 1: constant}.\n/// A 1 is transmitted as a transition, and a 0 is transmitted as no transition.\nexport function sliceNRZI(pulses, guess) {\n const short = guess.short\n const bits = new Bitbuffer()\n let hints = []\n\n let x = 0\n let x1 = 0\n for (let j = 0; j < pulses.length; j += 1) {\n const w = pulses[j] // mark or space\n const cnt = ~~(w / short + 0.5)\n // every edge is a 1, don't use the first edge\n if (x1) {\n hints.push([x1, x + short / 2, '1'])\n bits.pushOne()\n }\n x1 = x + short / 2\n // count minus one amounts of 0\n for (let k = 1; k < cnt; ++k) {\n hints.push([x1, x1 + w / cnt, '0'])\n bits.pushZero()\n x1 += w / cnt\n\n }\n x += w\n }\n\n return { hints, bits }\n}\n\n/// Coded Mark Inversion (CMI) https://en.wikipedia.org/wiki/Coded_mark_inversion\n/// encodes zero bits as a half bit time of zero followed by a half bit time of one,\n/// and one bits are encoded as a full bit time of a constant level,\n/// the level used for one bits alternates each time one is coded.\nexport function sliceCMI(pulses, guess) {\n const short = guess.short\n const bits = new Bitbuffer()\n let hints = []\n\n let x = 0\n let x1 = null\n for (let j = 0; j < pulses.length; j += 2) {\n const mark = pulses[j] // mark\n const mcnt = ~~(mark / short + 0.5)\n const space = pulses[j + 1] // space\n const scnt = ~~(space / short + 0.5)\n\n if (mcnt == 1 && scnt == 1) {\n if (!x1) x1 = x - mark // first bit\n hints.push([x1, x + mark, '0'])\n bits.pushZero()\n x1 = x + mark\n }\n else if (mcnt == 1 && scnt == 2) {\n if (!x1) x1 = x - mark // first bit\n hints.push([x1, x + mark, '0'])\n bits.pushZero()\n x1 = x + mark + space\n hints.push([x + mark, x1, '1'])\n bits.pushOne()\n }\n else if (mcnt == 1 && scnt == 3) {\n if (!x1) x1 = x - mark // first bit\n hints.push([x1, x + mark, '0'])\n bits.pushZero()\n x1 = x + mark + space * 2 / 3\n hints.push([x + mark, x1, '1'])\n bits.pushOne()\n }\n else if (mcnt == 2 && scnt == 1) {\n hints.push([x1, x + mark, '1'])\n bits.pushOne()\n x1 = x + mark\n }\n else if (mcnt == 2 && scnt == 2) {\n hints.push([x1, x + mark, '1'])\n bits.pushOne()\n x1 = x + mark + space\n hints.push([x + mark, x1, '1'])\n bits.pushOne()\n }\n else if (mcnt == 2 && scnt == 3) {\n hints.push([x1, x + mark, '1'])\n bits.pushOne()\n x1 = x + mark + space * 2 / 3\n hints.push([x + mark, x1, '1'])\n bits.pushOne()\n }\n else if (mcnt == 3 && scnt == 1) {\n hints.push([x1, x + mark / 3, '0'])\n bits.pushZero()\n hints.push([x + mark / 3, x + mark, '1'])\n bits.pushOne()\n x1 = x + mark\n }\n else if (mcnt == 3 && scnt == 2) {\n hints.push([x1, x + mark / 3, '0'])\n bits.pushZero()\n hints.push([x + mark / 3, x + mark, '1'])\n bits.pushOne()\n x1 = x + mark + space\n hints.push([x + mark, x1, '1'])\n bits.pushOne()\n }\n else if (mcnt == 3 && scnt == 3) {\n hints.push([x1, x + mark / 3, '0'])\n bits.pushZero()\n hints.push([x, x + mark / 3, '1'])\n bits.pushOne()\n hints.push([x + mark / 3, x + mark, '1'])\n bits.pushOne()\n hints.push([x + mark, x + mark + space * 3 / 2, '1'])\n bits.pushOne()\n x1 = x + mark + space * 3 / 2\n }\n else if (mcnt == 1) { // last zero\n hints.push([x1, x + mark, '0'])\n bits.pushZero()\n bits.pushBreak()\n x1 = x + mark\n }\n else if (mcnt == 2) { // last one\n hints.push([x1, x + mark, '1'])\n bits.pushOne()\n bits.pushBreak()\n x1 = x + mark\n }\n else {\n // error\n bits.pushBreak()\n }\n x += mark + space\n }\n\n return { hints, bits }\n}\n\n/// Pulse-Interval-Width Modulation (PIWM)\n/// Exotic differential coding\nexport function slicePIWM(pulses, guess) {\n const short = guess.short\n const bits = new Bitbuffer()\n let hints = []\n\n let x = 0\n for (let j = 0; j < pulses.length; j += 1) {\n const w = pulses[j] // mark or space\n const cnt = ~~(w / short + 0.5)\n\n if (cnt == 1) {\n hints.push([x, x + w, '1'])\n bits.pushOne()\n }\n else if (cnt == 2) {\n hints.push([x, x + w, '0'])\n bits.pushZero()\n } else {\n // error\n bits.pushBreak()\n }\n x += w\n }\n\n return { hints, bits }\n}\n","/**\n @file Pulse Builder JS.\n\n @author Christian W. Zuckschwerdt <zany@triq.net>\n @copyright Christian W. Zuckschwerdt, 2020\n @license\n This program is free software: you can redistribute it and/or modify\n it under the terms of the GNU General Public License as published by\n the Free Software Foundation, either version 2 of the License, or\n (at your option) any later version.\n*/\n\n/*eslint no-console: \"off\"*/\n\nimport { Bitbuffer } from './bitbuffer.js'\n\nfunction parsePulseString(pulseStr) {\n if (Array.isArray(pulseStr)) {\n return pulseStr\n }\n let pulses = pulseStr.split(/\\D