({
51 | key: key + '-receive-selector',
52 | get: () => {
53 | throw new Error('cant get here')
54 | },
55 | set: ({ set }, newValue) => {
56 | if (newValue instanceof DefaultValue) throw new Error('no reset allowed')
57 | set(remoteState, newValue)
58 | }
59 | })
60 |
61 | return { send, receive }
62 | }
63 |
--------------------------------------------------------------------------------
/src/communication/parseSerial.ts:
--------------------------------------------------------------------------------
1 | import { voltageRanges } from './bindings'
2 |
3 | type Data = {
4 | data: number[]
5 | i: number
6 | }
7 |
8 | function pull(data: Data, count: number) {
9 | const result = data.data.slice(data.i, data.i + count)
10 | data.i += count
11 | return Array.from(result)
12 | }
13 |
14 | function get_uint8_t(data: Data) {
15 | const res = data.data[data.i]
16 | data.i++
17 | return res
18 | }
19 | const get_bool = get_uint8_t
20 |
21 | function get_uint16_t(data: Data) {
22 | const l = data.data[data.i]
23 | data.i++
24 | const h = data.data[data.i]
25 | data.i++
26 | return (h << 8) | l
27 | }
28 | function get_float32_t(data: Data) {
29 | // IEEE 754
30 | // https://gist.github.com/Jozo132/2c0fae763f5dc6635a6714bb741d152f#file-float32encoding-js-L32-L43
31 | const arr = data.data.slice(data.i, data.i + 4)
32 | data.i += 4
33 |
34 | const int = arr.reverse().reduce((acc, curr) => (acc << 8) + curr)
35 | if (int === 0) return 0
36 | const sign = int >>> 31 ? -1 : 1
37 | let exp = ((int >>> 23) & 0xff) - 127
38 | const mantissa = ((int & 0x7fffff) + 0x800000).toString(2)
39 | let float32 = 0
40 | for (let i = 0; i < mantissa.length; i += 1) {
41 | float32 += parseInt(mantissa[i]) ? Math.pow(2, exp) : 0
42 | exp--
43 | }
44 | return float32 * sign
45 | }
46 | // function get_int16_t(buffer: number[]) {
47 | // const raw = get_uint16_t(buffer)
48 | // if (raw & (1 << 15)) {
49 | // // negative
50 | // return -(~raw + (1 << 16) + 1)
51 | // }
52 | // return raw
53 | // }
54 | export default function parseSerial(data: number[]) {
55 | const myData: Data = {
56 | data,
57 | i: 0
58 | }
59 | // input
60 | const triggerVoltage = get_uint8_t(myData)
61 | const triggerDir = get_uint8_t(myData)
62 | const secPerSample = get_float32_t(myData)
63 | const triggerPos = get_uint16_t(myData)
64 | const amplifier = get_uint8_t(myData)
65 | const triggerMode = get_uint8_t(myData)
66 | const triggerChannel = get_uint8_t(myData)
67 | const isChannelOn = get_uint8_t(myData)
68 | // input output
69 | // output
70 | const needData = get_bool(myData)
71 | const forceUIUpdate = get_bool(myData)
72 | const didTrigger = get_bool(myData)
73 | const freeMemory = get_uint16_t(myData)
74 | const sentSamples = get_uint16_t(myData)
75 | const samplesPerBuffer = get_uint16_t(myData)
76 | const analogs = [
77 | isChannelOn & 0b1 ? pull(myData, sentSamples) : [],
78 | isChannelOn & 0b10 ? pull(myData, sentSamples) : []
79 | ]
80 | const digitalBytes = isChannelOn & 0b11111100 ? pull(myData, sentSamples) : []
81 | const digitals = [0b000100, 0b001000, 0b010000, 0b100000].map((mask) => {
82 | if (isChannelOn & mask) {
83 | return digitalBytes.map((byte) => byte & mask && 1)
84 | }
85 | return []
86 | })
87 | const vMax = voltageRanges[amplifier]
88 | const buffers = [
89 | ...analogs.map((analog) => analog.map((n) => (n / 256) * vMax)),
90 | ...digitals.map((digital, i) =>
91 | digital.map((bit) => (bit * vMax) / 8 + ((i + 0.25) * vMax) / 4)
92 | )
93 | ].map((channel) => channel.map((v, i) => ({ v, t: (i + 1) * secPerSample })))
94 |
95 | return {
96 | //input
97 | triggerVoltage,
98 | triggerDir,
99 | secPerSample,
100 | triggerPos,
101 | amplifier,
102 | triggerMode,
103 | triggerChannel,
104 | isChannelOn,
105 | // output
106 | needData,
107 | forceUIUpdate,
108 | didTrigger,
109 | freeMemory,
110 | samplesPerBuffer,
111 | buffers
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/communication/profile.ts:
--------------------------------------------------------------------------------
1 | const report = {} as any
2 | const starts = {} as any
3 | let startT = 0
4 | export function profileS(name: string) {
5 | report[name] = report[name] || { calls: 0, t: 0 }
6 | starts[name] = performance.now()
7 | startT = startT || performance.now()
8 | }
9 | export function profileE(name: string) {
10 | report[name].t += performance.now() - starts[name]
11 | report[name].calls++
12 | starts[name] = undefined
13 | }
14 |
15 | const win = window as any
16 | win.startReport = () => {
17 | startT = performance.now()
18 | Object.keys(report).forEach((name) => {
19 | report[name] = { calls: 0, t: 0 }
20 | })
21 | }
22 | win.report = report
23 | win.getReport = () => {
24 | const tab = Object.keys(report).map((name) => {
25 | const { calls, t } = report[name]
26 | return {
27 | name,
28 | calls,
29 | t,
30 | tPerCall: t / calls,
31 | percentage: t / (startT - performance.now())
32 | }
33 | })
34 | console.table(tab)
35 | }
36 |
--------------------------------------------------------------------------------
/src/components/About.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { Panel } from 'rsuite'
4 |
5 | export default function About() {
6 | return (
7 |
8 | David Buezas 2020
9 |
10 | https://github.com/dbuezas/arduino-web-oscilloscope
11 |
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/App.scss:
--------------------------------------------------------------------------------
1 | body {
2 | /* prevent scroll bounce on body */
3 | overflow: hidden;
4 | }
5 |
6 | form {
7 | display: inline-block;
8 | }
9 |
10 | .App {
11 | display: flex;
12 | position: absolute;
13 |
14 | top: 0;
15 | left: 0;
16 | right: 0;
17 | bottom: 0;
18 | }
19 |
20 | header {
21 | /* no flex rules, it will grow */
22 | width: 100%;
23 | }
24 | .rs-content {
25 | padding: 10px;
26 | padding-right: 0;
27 | }
28 | .rs-content,
29 | .rs-content > .rs-panel,
30 | .rs-content .rs-panel-body {
31 | height: 100%;
32 | }
33 | .rs-sidebar {
34 | overflow: scroll;
35 | padding: 10px;
36 | }
37 | .rs-sidebar .rs-panel {
38 | margin-bottom: 10px;
39 | }
40 | footer {
41 | width: 100%;
42 | }
43 | .rs-panel-heading {
44 | padding: 10px;
45 | }
46 | .rs-panel-body {
47 | padding: 10px;
48 | padding-top: 0 !important;
49 | }
50 |
51 | .rs-panel-collapsible > .rs-panel-heading::before {
52 | top: 10px;
53 | }
54 |
--------------------------------------------------------------------------------
/src/components/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Plot from './Plot/Plot'
3 |
4 | import Controls from './Controls'
5 | import { Panel, Container, Content, Sidebar } from 'rsuite'
6 | import About from './About'
7 | import EnableSerialInstructions from './EnableSerialInstructions'
8 |
9 | function getChromeVersion() {
10 | const raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)
11 | return Number(raw ? parseInt(raw[2], 10) : false)
12 | }
13 | function App() {
14 | if (getChromeVersion() < 86)
15 | return Requires an updated version of Chrome (≥ 86.x.x)
16 | if (!navigator.serial) return
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | )
32 | }
33 |
34 | export default App
35 |
--------------------------------------------------------------------------------
/src/components/Controls/Amplifier.tsx:
--------------------------------------------------------------------------------
1 | import MouseTrap from 'mousetrap'
2 | import React, { useEffect } from 'react'
3 | import { useAmplifier, voltageRanges } from '../../communication/bindings'
4 | import { formatVoltage } from '../formatters'
5 | import { Icon, IconButton, SelectPicker } from 'rsuite'
6 | import { useRecoilState } from 'recoil'
7 | import { useActiveBtns } from './hooks'
8 |
9 | function Amplifier() {
10 | const [amplifier, setAmplifier] = useRecoilState(useAmplifier.send)
11 | const [isUpActive, tapUp] = useActiveBtns()
12 | const [isDownActive, tapDown] = useActiveBtns()
13 | useEffect(() => {
14 | MouseTrap.bind('up', (e) => {
15 | e.preventDefault()
16 | tapUp()
17 | setAmplifier(amplifier - 1)
18 | })
19 | MouseTrap.bind('down', (e) => {
20 | e.preventDefault()
21 | tapDown()
22 | setAmplifier(amplifier + 1)
23 | })
24 | return () => {
25 | MouseTrap.unbind('up')
26 | MouseTrap.unbind('down')
27 | }
28 | }, [amplifier, setAmplifier, tapDown, tapUp])
29 |
30 | return (
31 |
39 | }
43 | onClick={() => setAmplifier(amplifier + 1)}
44 | />
45 | {
51 | return {
52 | label: formatVoltage(v / 10) + ' / div',
53 | value: i
54 | }
55 | })}
56 | style={{ flex: 1, marginLeft: 5, marginRight: 5 }}
57 | />
58 | }
62 | onClick={() => setAmplifier(amplifier - 1)}
63 | />
64 |
65 | )
66 | }
67 |
68 | export default Amplifier
69 |
--------------------------------------------------------------------------------
/src/components/Controls/Channels.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {
3 | useIsChannelOn,
4 | oversamplingFactorState,
5 | fftState0,
6 | fftState1,
7 | XYModeState
8 | } from '../../communication/bindings'
9 | import { Panel, ButtonToolbar, ButtonGroup, Button, Slider } from 'rsuite'
10 | import { useRecoilState } from 'recoil'
11 |
12 | const ButtonToolbarStyle = {
13 | marginTop: 10,
14 | display: 'flex',
15 | justifyContent: 'space-between',
16 | alignItems: 'center'
17 | }
18 |
19 | export default function Channels() {
20 | const [oversamplingFactor, setOversamplingFactor] = useRecoilState(
21 | oversamplingFactorState
22 | )
23 | const [xyMode, setXYMode] = useRecoilState(XYModeState)
24 | const [fft0, setFFT0] = useRecoilState(fftState0)
25 | const [fft1, setFFT1] = useRecoilState(fftState1)
26 | const [isChannelOn, setIsChannelOn] = useRecoilState(useIsChannelOn.send)
27 |
28 | return (
29 |
30 |
31 |
32 | {['A0', 'AS', 'A2', 'A3', 'A4', 'A5'].map((name, i) => (
33 | {
43 | const buffer = isChannelOn.slice()
44 | buffer[i] = !buffer[i]
45 | setIsChannelOn(buffer)
46 | }}
47 | >
48 | {name}
49 |
50 | ))}
51 |
52 |
53 |
54 | Oversample
55 |
63 |
64 |
65 |
66 | {
71 | setFFT0(!fft0)
72 | }}
73 | >
74 | FFT A0
75 |
76 | {
81 | setFFT1(!fft1)
82 | }}
83 | >
84 | FFT AS
85 |
86 | {
91 | setXYMode(!xyMode)
92 | }}
93 | >
94 | XY
95 |
96 |
97 |
98 | )
99 | }
100 |
--------------------------------------------------------------------------------
/src/components/Controls/Scales.tsx:
--------------------------------------------------------------------------------
1 | import MouseTrap from 'mousetrap'
2 | import React, { useEffect } from 'react'
3 | import { isRunningState } from '../../communication/bindings'
4 | import { Panel, Button } from 'rsuite'
5 | import { useRecoilState } from 'recoil'
6 | import Amplifier from './Amplifier'
7 | import TimeScales from './TimeScales'
8 | import { useActiveBtns } from './hooks'
9 |
10 | export default function Scales() {
11 | const [isRunning, setIsRunning] = useRecoilState(isRunningState)
12 | const [isSpaceActive, tapSpace] = useActiveBtns()
13 |
14 | useEffect(() => {
15 | MouseTrap.bind('space', (e) => {
16 | e.preventDefault()
17 | tapSpace()
18 | setIsRunning((isRunning) => !isRunning)
19 | })
20 | return () => {
21 | MouseTrap.unbind('space')
22 | }
23 | }, [setIsRunning, tapSpace])
24 |
25 | return (
26 |
27 | {
37 | setIsRunning(!isRunning)
38 | }}
39 | >
40 | {(isRunning ? 'Run' : 'Hold') + ' [space]'}
41 |
42 |
43 |
44 |
45 | )
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/Controls/SerialControls.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | IconButton,
3 | Icon,
4 | ButtonToolbar,
5 | Tag,
6 | ButtonGroup,
7 | Panel
8 | } from 'rsuite'
9 | import React, { useEffect, useState } from 'react'
10 | import { allDataState } from '../../communication/bindings'
11 | import serial from '../../communication/Serial'
12 | import { useSetRecoilState } from 'recoil'
13 | import Uploader from './Uploader'
14 |
15 | const serialOptions = {
16 | baudRate: 115200 * 1,
17 | bufferSize: 20000
18 | }
19 | const ButtonToolbarStyle = {
20 | marginTop: 10,
21 | display: 'flex',
22 | justifyContent: 'space-between',
23 | alignItems: 'center'
24 | }
25 | type ConnectedState = 'Connected' | 'Disconnected' | 'Connecting...' | 'Error'
26 |
27 | function SerialControls() {
28 | const [connectedWith, setConnectedWith] = useState({})
29 | const [error, setError] = useState('')
30 | const [serialState, setSerialState] = useState('Disconnected')
31 | const setAllData = useSetRecoilState(allDataState)
32 | useEffect(() => {
33 | if (serialState !== 'Error') setError('')
34 | }, [serialState])
35 | useEffect(() => {
36 | return serial.onData((data) => {
37 | setAllData(data)
38 | })
39 | }, [setAllData])
40 | useEffect(() => {
41 | setSerialState('Connecting...')
42 | serial
43 | .connectWithPaired(serialOptions)
44 | .then(setConnectedWith)
45 | .then(() => setSerialState('Connected'))
46 | .catch(() => setSerialState('Disconnected'))
47 | }, [])
48 | return (
49 |
50 |
51 | {
56 | serial
57 | .connect(serialOptions)
58 | .then(setConnectedWith)
59 | .then(() => setSerialState('Connected'))
60 | .catch((e) => {
61 | setSerialState('Error')
62 | setError(e.toString())
63 | })
64 | }}
65 | icon={ }
66 | placement="right"
67 | />
68 | {
73 | serial
74 | .close()
75 | .then(() => setSerialState('Disconnected'))
76 | .catch(() => setSerialState('Error'))
77 | }}
78 | icon={ }
79 | placement="right"
80 | />
81 |
82 | {
86 | setSerialState('Connecting...')
87 |
88 | await serial
89 | .connectWithPaired(serialOptions)
90 | .then(setConnectedWith)
91 | .catch(() => serial.connect(serialOptions).then(setConnectedWith))
92 | .then(() => setSerialState('Connected'))
93 | .catch(() => setSerialState('Error'))
94 | }}
95 | icon={ }
96 | placement="right"
97 | />
98 |
99 |
100 |
101 | State:
102 | {(() => {
103 | const color = {
104 | Connected: 'green',
105 | 'Connecting...': 'yellow',
106 | Error: 'red',
107 | Disconnected: undefined
108 | }[serialState]
109 |
110 | return (
111 |
112 | {serialState} {error}
113 |
114 | )
115 | })()}
116 |
117 | {serialState === 'Connected' && (
118 | {JSON.stringify(connectedWith)}
119 | )}
120 | {serialState === 'Disconnected' && }
121 |
122 | )
123 | }
124 |
125 | export default SerialControls
126 |
--------------------------------------------------------------------------------
/src/components/Controls/Stats.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import {
3 | dataState,
4 | freeMemoryState,
5 | frequencyState,
6 | voltagesState
7 | } from '../../communication/bindings'
8 | import { formatFreq, formatTime, formatVoltage } from '../formatters'
9 | import { Panel, Tag } from 'rsuite'
10 | import { useRecoilValue } from 'recoil'
11 |
12 | function FreeMemory() {
13 | const freeMemory = useRecoilValue(freeMemoryState)
14 | return Mem: {freeMemory}bytes
15 | }
16 | function FPS() {
17 | const [, setLastT] = useState(0)
18 | const [fps, setFps] = useState(0)
19 | const data = useRecoilValue(dataState)
20 | useEffect(() => {
21 | setLastT((lastT) => {
22 | setFps((fps) => {
23 | const newFps = 1000 / (performance.now() - lastT)
24 | return fps * 0.9 + newFps * 0.1
25 | })
26 | return performance.now()
27 | })
28 | }, [data])
29 | return FPS: {Math.round(fps)}
30 | }
31 |
32 | function Frequency() {
33 | const frequency = useRecoilValue(frequencyState)
34 | return Freq: {formatFreq(frequency)}
35 | }
36 | function Wavelength() {
37 | const frequency = useRecoilValue(frequencyState)
38 | return WLength: {formatTime(1 / frequency)}
39 | }
40 |
41 | const style = {
42 | width: ' 100%',
43 | display: ' flex',
44 | justifyContent: ' space-between'
45 | }
46 | function Voltages() {
47 | const voltages = useRecoilValue(voltagesState)
48 | return (
49 | <>
50 |
51 | Vmin: {formatVoltage(voltages.vmin)}
52 | Vmax: {formatVoltage(voltages.vmax)}
53 |
54 |
55 | Vavr: {formatVoltage(voltages.vavr)}
56 | Vp-p: {formatVoltage(voltages.vpp)}
57 |
58 | >
59 | )
60 | }
61 | export default function Stats() {
62 | return (
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | )
77 | }
78 |
--------------------------------------------------------------------------------
/src/components/Controls/TimeScales.tsx:
--------------------------------------------------------------------------------
1 | import MouseTrap from 'mousetrap'
2 | import React, { useEffect } from 'react'
3 | import {
4 | useSecPerSample,
5 | useSamplesPerBuffer,
6 | constrain
7 | } from '../../communication/bindings'
8 | import { formatTime } from '../formatters'
9 | import { Icon, IconButton, SelectPicker } from 'rsuite'
10 | import { useRecoilState, useRecoilValue } from 'recoil'
11 | import win from '../../win'
12 | import { useActiveBtns } from './hooks'
13 | const us = (n: number) => n / 1000000
14 | const ms = (n: number) => n / 1000
15 | const s = (n: number) => n
16 | const offset = (list: { value: number }[], current: number, offset: number) => {
17 | let i = list.map(({ value }) => value).indexOf(current) + offset
18 | i = constrain(i, 0, list.length - 1)
19 | return list[i].value
20 | }
21 | export default function TimeScales() {
22 | const [secPerSample, setSecPerSample] = useRecoilState(useSecPerSample.send)
23 | const samples = useRecoilValue(useSamplesPerBuffer.send)
24 | const [isLeftActive, tapLeft] = useActiveBtns()
25 | const [isRightActive, tapRight] = useActiveBtns()
26 |
27 | const perSample = [
28 | us(60.8), // as fast as it goes
29 | us(73.6), // matching fastest adc
30 | us(100.8), // as fast as it went before
31 | us(140.8), // in synch with adc clock
32 | us(200),
33 | us(500),
34 | us(1000),
35 | ms(2),
36 | ms(5),
37 | ms(10),
38 | ms(20),
39 | ms(50),
40 | s(0.1),
41 | s(0.2),
42 | s(0.5),
43 | s(1),
44 | s(2),
45 | s(5),
46 | s(10),
47 | s(20),
48 | s(50),
49 | s(100),
50 | s(1000)
51 | ].map((secPerDivision) => {
52 | const secPerSample = (secPerDivision * 10) / samples
53 | return {
54 | label: formatTime(secPerDivision) + ' / div',
55 | value: secPerSample
56 | }
57 | })
58 | useEffect(() => {
59 | MouseTrap.bind('right', (e) => {
60 | e.preventDefault()
61 | tapRight()
62 | setSecPerSample(offset(perSample, secPerSample, 1))
63 | })
64 | MouseTrap.bind('left', (e) => {
65 | e.preventDefault()
66 | tapLeft()
67 | setSecPerSample(offset(perSample, secPerSample, -1))
68 | })
69 | return () => {
70 | MouseTrap.unbind('right')
71 | MouseTrap.unbind('left')
72 | }
73 | }, [setSecPerSample, secPerSample, perSample, tapRight, tapLeft])
74 | win.setSecPerSample = setSecPerSample
75 |
76 | return (
77 |
85 | }
89 | onClick={() => setSecPerSample(offset(perSample, secPerSample, -1))}
90 | />
91 | {
96 | setSecPerSample(n)
97 | }}
98 | data={perSample}
99 | style={{ flex: 1, marginLeft: 5, marginRight: 5 }}
100 | />
101 | }
105 | onClick={() => setSecPerSample(offset(perSample, secPerSample, 1))}
106 | />
107 |
108 | )
109 | }
110 |
--------------------------------------------------------------------------------
/src/components/Controls/Trigger.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {
3 | useTriggerDirection,
4 | isRunningState,
5 | useTriggerMode,
6 | TriggerMode,
7 | didTriggerState,
8 | useTriggerChannel,
9 | TriggerDirection,
10 | requestData,
11 | useSecPerSample
12 | } from '../../communication/bindings'
13 | import {
14 | Icon,
15 | Panel,
16 | Tag,
17 | ButtonToolbar,
18 | ButtonGroup,
19 | Button,
20 | IconButton
21 | } from 'rsuite'
22 | import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'
23 | import win from '../../win'
24 |
25 | const ButtonToolbarStyle = {
26 | marginTop: 10,
27 | display: 'flex',
28 | justifyContent: 'space-between',
29 | alignItems: 'center'
30 | }
31 |
32 | export default function Trigger() {
33 | const isRunning = useRecoilValue(isRunningState)
34 | win.requestData = useSetRecoilState(requestData.send)
35 | const [triggerMode, setTriggerMode] = useRecoilState(useTriggerMode.send)
36 | const [triggerChannel, setTriggerChannel] = useRecoilState(
37 | useTriggerChannel.send
38 | )
39 | const didTrigger = useRecoilValue(didTriggerState)
40 | const [triggerDirection, setTriggerDirection] = useRecoilState(
41 | useTriggerDirection.send
42 | )
43 | const secPerSample = useRecoilValue(useSecPerSample.send)
44 | const tooFastForSlowMode = secPerSample < 0.003
45 | return (
46 |
47 |
48 |
49 | {['A0', 'AS', 'A2', 'A3', 'A4', 'A5'].map((name, idx) => (
50 | setTriggerChannel(idx)}
60 | >
61 | {name}
62 |
63 | ))}
64 |
65 |
66 |
67 |
68 | {Object.values(TriggerMode).map((mode) => (
69 | setTriggerMode(mode)}
79 | >
80 | {mode}
81 |
82 | ))}
83 |
84 |
85 |
86 | Direction:
87 |
88 | }
96 | onClick={() => setTriggerDirection(TriggerDirection.FALLING)}
97 | />
98 | }
106 | onClick={() => setTriggerDirection(TriggerDirection.RISING)}
107 | />
108 |
109 |
110 |
111 | State:
112 | {!isRunning ? (
113 | Hold
114 | ) : didTrigger ? (
115 | Triggered
116 | ) : (
117 | Searching
118 | )}
119 |
120 |
121 | )
122 | }
123 |
--------------------------------------------------------------------------------
/src/components/Controls/Uploader.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { Button, Icon, Progress } from 'rsuite'
3 | import serial2 from '../../communication/Serial2'
4 | import async from 'async'
5 | import intel_hex from 'intel-hex'
6 | import Stk500 from 'stk500'
7 |
8 | const stk500 = new Stk500()
9 | const bootload = async (
10 | stream: NodeJS.ReadWriteStream,
11 | hex: string,
12 | opt: typeof board,
13 | progress: (percent: number) => void
14 | ) => {
15 | let sent = 0
16 | stk500.log = (what: string) => {
17 | if (what === 'loaded page') {
18 | sent += 1
19 | const percent = Math.round((100 * sent) / (hex.length / opt.pageSize))
20 | progress(percent)
21 | }
22 | }
23 |
24 | await async.series([
25 | // send two dummy syncs like avrdude does
26 | stk500.sync.bind(stk500, stream, 3, opt.timeout),
27 | stk500.sync.bind(stk500, stream, 3, opt.timeout),
28 | stk500.sync.bind(stk500, stream, 3, opt.timeout),
29 | stk500.verifySignature.bind(stk500, stream, opt.signature, opt.timeout),
30 | stk500.setOptions.bind(stk500, stream, {}, opt.timeout),
31 | stk500.enterProgrammingMode.bind(stk500, stream, opt.timeout),
32 | stk500.upload.bind(stk500, stream, hex, opt.pageSize, opt.timeout),
33 | // stk500.verify.bind(stk500, stream, hex, opt.pageSize, opt.timeout),
34 | stk500.exitProgrammingMode.bind(stk500, stream, opt.timeout)
35 | ])
36 | }
37 |
38 | const serialOptions = {
39 | baudRate: 57600,
40 | bufferSize: 20000
41 | }
42 | const board = {
43 | signature: Buffer.from([0x1e, 0x95, 0x0f]),
44 | pageSize: 128,
45 | timeout: 400
46 | }
47 | function Uploader() {
48 | const [percent, setPercent] = useState(0)
49 | const [status, setStatus] = useState<'active' | 'fail' | 'success'>('active')
50 | const [isProgressHidden, setIsProgressHidden] = useState(true)
51 | const [message, setMessage] = useState('')
52 | const onClick = async () => {
53 | setMessage('Uploading...')
54 | try {
55 | setIsProgressHidden(true)
56 | const hex = await fetch(process.env.PUBLIC_URL + '/src.ino.hex')
57 | .then((response) => response.text())
58 | .then((text) => intel_hex.parse(text).data)
59 | const serialStream = await serial2.connect(serialOptions)
60 | setStatus('active')
61 | setPercent(0)
62 | setIsProgressHidden(false)
63 | await bootload(serialStream, hex, board, setPercent)
64 | setStatus('success')
65 | setMessage(`Uploaded ${hex.length} bytes.`)
66 | } catch (e) {
67 | console.error(e)
68 | setMessage(e.toString())
69 | setStatus('fail')
70 | }
71 | serial2.close()
72 | }
73 | return (
74 | <>
75 |
76 |
77 | Upload lgt328p Firmware
78 |
79 | {!isProgressHidden && (
80 | <>
81 |
82 | {message}
83 | >
84 | )}
85 | >
86 | )
87 | }
88 |
89 | export default Uploader
90 |
--------------------------------------------------------------------------------
/src/components/Controls/hooks.ts:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 |
3 | export function useActiveBtns() {
4 | const [state, setState] = useState(false)
5 |
6 | const [timeout, setTimeout] = useState(-1)
7 | const activateBtn = () => {
8 | setState(true)
9 | clearTimeout(timeout)
10 | const timeoutId = window.setTimeout(() => setState(false), 200)
11 | setTimeout(timeoutId)
12 | }
13 | return [state, activateBtn] as [typeof state, typeof activateBtn]
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/Controls/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Trigger from './Trigger'
4 | import Channels from './Channels'
5 | import Scales from './Scales'
6 | import SerialControls from './SerialControls'
7 | import Stats from './Stats'
8 |
9 | function Controls() {
10 | return (
11 | <>
12 |
13 |
14 |
15 |
16 |
17 | >
18 | )
19 | }
20 |
21 | export default Controls
22 |
--------------------------------------------------------------------------------
/src/components/Controls/intel-hex.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'intel-hex' {
2 | export const parse = (
3 | data: string
4 | ): {
5 | data: string
6 | } => {}
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/Controls/stk500.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'stk500' {
2 | const a: any
3 | export default a
4 | }
5 |
--------------------------------------------------------------------------------
/src/components/EnableSerialInstructions.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { Panel, Button, Notification, Steps } from 'rsuite'
4 | import useCopy from '@react-hook/copy'
5 |
6 | const chromeFlagsUrl =
7 | 'chrome://flags/#enable-experimental-web-platform-features'
8 | const styles = {
9 | width: '200px',
10 | display: 'inline-table',
11 | verticalAlign: 'top'
12 | }
13 | function EnableSerialInstructions() {
14 | const { copy } = useCopy(chromeFlagsUrl)
15 |
16 | return (
17 |
18 |
28 |
31 |
32 | Enable experimental web platform features to activate the Web
33 | Serial API{' '}
34 |
35 |
36 | >
37 | }
38 | >
39 |
40 | {
46 | copy()
47 | Notification.success({
48 | title: 'It is now in your clipboard',
49 | description: chromeFlagsUrl
50 | })
51 | }}
52 | >
53 | {chromeFlagsUrl}
54 |
55 | }
56 | />
57 |
61 |
65 |
66 | Right there. This will take you to the the page where you
67 | can support for the serial port.
68 |
69 | >
70 | }
71 | />
72 |
82 | }
83 | />
84 |
88 |
92 |
97 |
98 | Do not do something stupid, the board is connected to your
99 | computer so it shares Ground with it, also do not push more
100 | than 5v to it.
101 |
102 | >
103 | }
104 | />
105 |
106 |
107 |
108 |
109 | )
110 | }
111 |
112 | export default EnableSerialInstructions
113 |
--------------------------------------------------------------------------------
/src/components/Plot/Measure.tsx:
--------------------------------------------------------------------------------
1 | import { isEqual } from 'lodash'
2 | import React, {
3 | forwardRef,
4 | MouseEventHandler,
5 | useImperativeHandle,
6 | useState
7 | } from 'react'
8 |
9 | import { useRecoilValue } from 'recoil'
10 | import { formatTime, formatVoltage } from '../formatters'
11 | import { xScaleSelector, yScaleSelector } from './hooks'
12 |
13 | export type MeasureRef = {
14 | onMouseDown: MouseEventHandler
15 | onMouseUp: MouseEventHandler
16 | onMouseMove: MouseEventHandler
17 | }
18 |
19 | const Measure = forwardRef((_props, ref) => {
20 | const [dragging, setDragging] = useState(false)
21 | const xScale = useRecoilValue(xScaleSelector)
22 | const yScale = useRecoilValue(yScaleSelector)
23 | const [startPos, setStartPos] = useState({ x: -1, y: -1 })
24 | const [endPos, setEndPos] = useState({ x: -1, y: -1 })
25 | useImperativeHandle(ref, () => ({
26 | onMouseDown(e) {
27 | setStartPos({
28 | x: xScale.invert(e.nativeEvent.offsetX),
29 | y: yScale.invert(e.nativeEvent.offsetY)
30 | })
31 | setEndPos({
32 | x: xScale.invert(e.nativeEvent.offsetX),
33 | y: yScale.invert(e.nativeEvent.offsetY)
34 | })
35 | setDragging(true)
36 | },
37 | onMouseUp(e) {
38 | if (dragging) {
39 | setEndPos({
40 | x: xScale.invert(e.nativeEvent.offsetX),
41 | y: yScale.invert(e.nativeEvent.offsetY)
42 | })
43 | setDragging(false)
44 | }
45 | },
46 | onMouseMove(e) {
47 | if (dragging) {
48 | setEndPos({
49 | x: xScale.invert(e.nativeEvent.offsetX),
50 | y: yScale.invert(e.nativeEvent.offsetY)
51 | })
52 | }
53 | }
54 | }))
55 | if (isEqual(startPos, endPos)) return <>>
56 | return (
57 |
58 |
65 |
72 |
79 |
86 | {formatTime(endPos.x - startPos.x)}
87 |
88 |
95 |
102 |
109 |
116 | {formatVoltage(endPos.y - startPos.y)}
117 |
118 |
119 | )
120 | })
121 |
122 | Measure.displayName = 'Measure'
123 | export default Measure
124 |
--------------------------------------------------------------------------------
/src/components/Plot/Plot.scss:
--------------------------------------------------------------------------------
1 | .plotContainer {
2 |
3 | width: 100%;
4 | height: 100%;
5 |
6 | svg.plot {
7 | width: 100%;
8 | height: 100%;
9 | }
10 |
11 | text {
12 | font-size: 14px;
13 | paint-order: stroke;
14 | stroke: #ffffff;
15 | stroke-width: 2px;
16 | stroke-linecap: butt;
17 | stroke-linejoin: miter;
18 | }
19 |
20 |
21 | path {
22 | stroke-width: 1;
23 | fill: none;
24 |
25 | &.plot-area-a0 {
26 | stroke: steelblue;
27 | stroke-width: 3;
28 | }
29 |
30 | &.plot-area-a1 {
31 | stroke: crimson;
32 | stroke-width: 3;
33 | }
34 |
35 | &.plot-area-d0 {
36 | stroke: lightcoral;
37 | }
38 |
39 | &.plot-area-d1 {
40 | stroke: chartreuse;
41 | }
42 |
43 | &.plot-area-d2 {
44 | stroke: cornsilk;
45 | }
46 |
47 | &.plot-area-d3 {
48 | stroke: darkmagenta;
49 | }
50 |
51 | &.plot-area-fft0 {
52 | stroke: rgb(4, 65, 116);
53 | }
54 |
55 | &.plot-area-fft1 {
56 | stroke: rgb(160, 4, 35);
57 | }
58 |
59 | &.plot-area-xy {
60 | stroke: rgb(3, 102, 53);
61 | }
62 | }
63 |
64 | line {
65 |
66 | &.measureX,
67 | &.measureY {
68 | stroke: black;
69 | stroke-dasharray: 5;
70 | stroke-width: 1;
71 | }
72 |
73 | &.measureCap {
74 | stroke: black;
75 | stroke-width: 1;
76 | }
77 |
78 | &.triggerPosHandle {
79 | cursor: col-resize;
80 | }
81 |
82 | &.triggerVoltageHandle {
83 | cursor: row-resize;
84 | }
85 |
86 | &.triggerPosHandle,
87 | &.triggerVoltageHandle {
88 | stroke-width: 40;
89 | stroke: white;
90 | opacity: 0.03;
91 | }
92 |
93 | &.triggerPos,
94 | &.triggerVoltage {
95 | fill: none;
96 | stroke-dasharray: 10;
97 | stroke-width: 2;
98 | opacity: 0.7;
99 | }
100 |
101 | &.triggerPos {
102 | stroke: black;
103 | }
104 |
105 | &.triggerVoltage {
106 | stroke: black;
107 | }
108 |
109 | &.triggerPos.active,
110 | &.triggerVoltage.active {
111 | stroke-width: 1;
112 | }
113 | }
114 |
115 | .axis {
116 | .domain {
117 | fill: rgba(0, 0, 0, 0.2);
118 | stroke: grey;
119 | stroke-width: 2;
120 | }
121 |
122 | &.y text {
123 | font-size: 14px;
124 | }
125 |
126 | line {
127 | fill: none;
128 | stroke: grey;
129 | stroke-width: 1;
130 | opacity: 0.3;
131 | stroke-dasharray: 5;
132 | }
133 |
134 | }
135 | }
--------------------------------------------------------------------------------
/src/components/Plot/Plot.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from 'react'
2 | import useSize from '@react-hook/size'
3 |
4 | import {
5 | dataState,
6 | useIsChannelOn,
7 | XYModeState
8 | } from '../../communication/bindings'
9 | import { useRecoilValue, useSetRecoilState } from 'recoil'
10 | import TriggerVoltageHandle, { TriggerVoltageRef } from './TriggerVoltageHandle'
11 | import TriggerPosHandle, { TriggerPosRef } from './TriggerPosHandle'
12 | import {
13 | lineSelector,
14 | plotHeightSelector,
15 | plotWidthSelector,
16 | XYLineSelector
17 | } from './hooks'
18 | import XAxis from './XAxis'
19 | import YAxis from './YAxis'
20 | import Measure, { MeasureRef } from './Measure'
21 |
22 | function XYCurve() {
23 | const xyMode = useRecoilValue(XYModeState)
24 | const isChannelOn = useRecoilValue(useIsChannelOn.send)
25 |
26 | const xyLine = useRecoilValue(XYLineSelector)
27 | const data = useRecoilValue(dataState)
28 | const d = data[0].map(
29 | (d, i) => [-data[1][i]?.v || 0, d.v] as [number, number]
30 | )
31 | if (!xyMode || !isChannelOn[0] || !isChannelOn[1]) return <>>
32 | return (
33 | <>
34 |
35 | >
36 | )
37 | }
38 | function Curves() {
39 | const line = useRecoilValue(lineSelector)
40 | const data = useRecoilValue(dataState)
41 | const ds = data.map((data) => line(data) || undefined)
42 | const analogs = ds.slice(0, 2)
43 | const digitals = ds.slice(2, 6)
44 | const ffts = ds.slice(6, 8)
45 | return (
46 | <>
47 | {analogs.map((d, i) => (
48 |
49 | ))}
50 | {digitals.map((d, i) => (
51 |
52 | ))}
53 | {ffts.map((d, i) => (
54 |
55 | ))}
56 | >
57 | )
58 | }
59 |
60 | export default function Plot() {
61 | const nodeRef = useRef(null)
62 | const triggerPosRef = useRef(null)
63 | const triggerVoltageRef = useRef(null)
64 | const measureRef = useRef(null)
65 | const containerRef = useRef(null)
66 | const [width, height] = useSize(containerRef)
67 | const setPlotHeight = useSetRecoilState(plotHeightSelector)
68 | const setPlotWidth = useSetRecoilState(plotWidthSelector)
69 | useEffect(() => {
70 | setPlotHeight(height)
71 | setPlotWidth(width)
72 | }, [height, setPlotHeight, setPlotWidth, width])
73 | return (
74 |
75 | {
79 | triggerPosRef.current?.onMouseMove(e)
80 | triggerVoltageRef.current?.onMouseMove(e)
81 | measureRef.current?.onMouseMove(e)
82 | e.preventDefault()
83 | }}
84 | onMouseLeave={(e) => {
85 | triggerPosRef.current?.onMouseUp(e)
86 | triggerVoltageRef.current?.onMouseUp(e)
87 | measureRef.current?.onMouseUp(e)
88 | e.preventDefault()
89 | }}
90 | onMouseUp={(e) => {
91 | triggerPosRef.current?.onMouseUp(e)
92 | triggerVoltageRef.current?.onMouseUp(e)
93 | measureRef.current?.onMouseUp(e)
94 | e.preventDefault()
95 | }}
96 | onMouseDown={(e) => {
97 | measureRef.current?.onMouseDown(e)
98 | e.preventDefault()
99 | }}
100 | >
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 | )
111 | }
112 |
--------------------------------------------------------------------------------
/src/components/Plot/TriggerPosHandle.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | forwardRef,
3 | MouseEventHandler,
4 | useImperativeHandle,
5 | useState
6 | } from 'react'
7 |
8 | import { useRecoilState, useRecoilValue } from 'recoil'
9 | import { useTriggerPos } from '../../communication/bindings'
10 | import {
11 | margin,
12 | plotHeightSelector,
13 | xDomainSelector,
14 | xScaleSelector
15 | } from './hooks'
16 |
17 | export type TriggerPosRef = {
18 | onMouseUp: MouseEventHandler
19 | onMouseMove: MouseEventHandler
20 | }
21 |
22 | const TriggerPosHandle = forwardRef((_props, ref) => {
23 | const [draggingTP, setDraggingTP] = useState(false)
24 | const xScale = useRecoilValue(xScaleSelector)
25 | const height = useRecoilValue(plotHeightSelector)
26 | const [triggerPos, setTriggerPos] = useRecoilState(useTriggerPos.send)
27 |
28 | const xDomain = useRecoilValue(xDomainSelector)
29 | useImperativeHandle(ref, () => ({
30 | onMouseUp() {
31 | setDraggingTP(false)
32 | },
33 | onMouseMove(e) {
34 | if (draggingTP) {
35 | setTriggerPos(xScale.invert(e.nativeEvent.offsetX) / xDomain[1])
36 | }
37 | }
38 | }))
39 | const scaledX = xScale(triggerPos * xDomain[1])
40 |
41 | return (
42 | <>
43 |
50 | {
53 | e.preventDefault()
54 | e.stopPropagation()
55 | setDraggingTP(true)
56 | }}
57 | x1={scaledX}
58 | x2={scaledX}
59 | y1={height - margin.bottom}
60 | y2={margin.top}
61 | >
62 |
63 | {Math.round(triggerPos * 100)}%
64 |
65 | >
66 | )
67 | })
68 |
69 | TriggerPosHandle.displayName = 'TriggerPosHandle'
70 | export default TriggerPosHandle
71 |
--------------------------------------------------------------------------------
/src/components/Plot/TriggerVoltageHandle.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | forwardRef,
3 | MouseEventHandler,
4 | useImperativeHandle,
5 | useState
6 | } from 'react'
7 |
8 | import { useTriggerVoltage } from '../../communication/bindings'
9 | import { useRecoilState, useRecoilValue } from 'recoil'
10 | import { margin, plotWidthSelector, yScaleSelector } from './hooks'
11 |
12 | export type TriggerVoltageRef = {
13 | onMouseUp: MouseEventHandler
14 | onMouseMove: MouseEventHandler
15 | }
16 |
17 | const TriggerVoltageHandle = forwardRef((_props, ref) => {
18 | const [draggingTV, setDraggingTV] = useState(false)
19 | const yScale = useRecoilValue(yScaleSelector)
20 | const width = useRecoilValue(plotWidthSelector)
21 | const [triggerVoltage, setTriggerVoltage] = useRecoilState(
22 | useTriggerVoltage.send
23 | )
24 | useImperativeHandle(ref, () => ({
25 | onMouseUp() {
26 | setDraggingTV(false)
27 | },
28 | onMouseMove(e) {
29 | if (draggingTV) {
30 | setTriggerVoltage(yScale.invert(e.nativeEvent.offsetY))
31 | }
32 | }
33 | }))
34 | const scaledY = yScale(triggerVoltage)
35 | return (
36 | <>
37 |
44 | {
47 | e.preventDefault()
48 | e.stopPropagation()
49 | setDraggingTV(true)
50 | }}
51 | x1={margin.left}
52 | x2={width - margin.right}
53 | y1={scaledY}
54 | y2={scaledY}
55 | >
56 |
63 | {triggerVoltage.toFixed(2)}v
64 |
65 | >
66 | )
67 | })
68 |
69 | TriggerVoltageHandle.displayName = 'TriggerVoltageHandle'
70 |
71 | export default TriggerVoltageHandle
72 |
--------------------------------------------------------------------------------
/src/components/Plot/XAxis.tsx:
--------------------------------------------------------------------------------
1 | import * as d3 from 'd3'
2 | import React, { useRef, useLayoutEffect } from 'react'
3 |
4 | import { useRecoilValue } from 'recoil'
5 | import { margin, plotHeightSelector, xScaleSelector } from './hooks'
6 | import { formatTime } from '../formatters'
7 |
8 | export default function XAxis() {
9 | const nodeRef = useRef(null)
10 | const height = useRecoilValue(plotHeightSelector)
11 | const xScale = useRecoilValue(xScaleSelector)
12 | const gEl = nodeRef.current
13 | useLayoutEffect(() => {
14 | if (!gEl) return
15 | const xTicks = d3.ticks(xScale.domain()[0], xScale.domain()[1], 10)
16 | d3.select(gEl).call((g) =>
17 | g
18 | .attr('transform', `translate(0,${height - margin.bottom})`)
19 | .call(
20 | d3
21 | .axisBottom(xScale)
22 | .tickValues(xTicks)
23 | .tickPadding(10)
24 | .tickSize(-height + margin.top + margin.bottom)
25 | .tickFormat(formatTime)
26 | .tickSizeOuter(0)
27 | )
28 | .call((g) => g.select('.domain').remove())
29 | )
30 | }, [gEl, xScale, height])
31 |
32 | return
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/Plot/YAxis.tsx:
--------------------------------------------------------------------------------
1 | import * as d3 from 'd3'
2 | import React, { useRef, useLayoutEffect } from 'react'
3 |
4 | import { useRecoilValue } from 'recoil'
5 | import { margin, plotWidthSelector, yScaleSelector } from './hooks'
6 |
7 | export default function YAxis() {
8 | const nodeRef = useRef(null)
9 | const width = useRecoilValue(plotWidthSelector)
10 | const yScale = useRecoilValue(yScaleSelector)
11 | const gEl = nodeRef.current
12 |
13 | useLayoutEffect(() => {
14 | if (!gEl) return
15 | const yTicks = d3.ticks(yScale.domain()[0], yScale.domain()[1], 10)
16 | d3.select(gEl)
17 | .call((g) =>
18 | g.attr('transform', `translate(${margin.left},0)`).call(
19 | d3
20 | .axisLeft(yScale)
21 | .tickValues(yTicks)
22 | .tickPadding(10)
23 | .tickSize(-width + margin.right + margin.left - 1)
24 | .tickFormat((v) => v + 'v')
25 | )
26 | )
27 | .call((g) =>
28 | g.select('.domain').attr(
29 | 'd',
30 | (_d, _, path) =>
31 | // close path so the domain has a right border
32 | d3.select(path[0]).attr('d') + 'z'
33 | )
34 | )
35 | }, [gEl, yScale, width])
36 |
37 | return
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/Plot/hooks.ts:
--------------------------------------------------------------------------------
1 | import * as d3 from 'd3'
2 |
3 | import {
4 | useSecPerSample,
5 | dataState,
6 | useSamplesPerBuffer,
7 | voltageRangeState
8 | } from '../../communication/bindings'
9 | import { selector, atom } from 'recoil'
10 | import { memoSelector } from '../../communication/bindingsHelper'
11 | export const margin = { top: 20, right: 50, bottom: 30, left: 50 }
12 |
13 | export const xDomainSelector = selector({
14 | key: 'xDomain',
15 | get: ({ get }) => {
16 | const xMax = get(useSecPerSample.send) * get(useSamplesPerBuffer.send)
17 | return [0, xMax] as [number, number]
18 | }
19 | })
20 | export const yDomainSelector = voltageRangeState
21 |
22 | export const plotWidthSelector = memoSelector(
23 | atom({
24 | key: 'plot-width',
25 | default: 0
26 | })
27 | )
28 | export const plotHeightSelector = memoSelector(
29 | atom({
30 | key: 'plot-height',
31 | default: 0
32 | })
33 | )
34 | export const xScaleSelector = selector({
35 | key: 'xScale',
36 | get: ({ get }) => {
37 | const xDomain = get(xDomainSelector)
38 | const width = get(plotWidthSelector)
39 | return d3
40 | .scaleLinear()
41 | .domain(xDomain)
42 | .range([margin.left, width - margin.right])
43 | }
44 | })
45 | export const yScaleSelector = selector({
46 | key: 'yScale',
47 | get: ({ get }) => {
48 | const yDomain = get(yDomainSelector)
49 | const height = get(plotHeightSelector)
50 | return d3
51 | .scaleLinear()
52 | .domain(yDomain)
53 | .rangeRound([height - margin.bottom, margin.top])
54 | }
55 | })
56 | export const lineSelector = selector({
57 | key: 'line',
58 | get: ({ get }) => {
59 | const xScale = get(xScaleSelector)
60 | const yScale = get(yScaleSelector)
61 |
62 | return d3
63 | .line<{ v: number; t: number }>()
64 | .x(({ t }) => xScale(t)!)
65 | .y(({ v }) => yScale(v)!)
66 | }
67 | })
68 | export const XYLineSelector = selector({
69 | key: 'xy-line',
70 | get: ({ get }) => {
71 | const yScale = get(yScaleSelector)
72 | const xScale = get(xScaleSelector)
73 | const [, xMax] = get(xDomainSelector)
74 | const [, yMax] = get(yDomainSelector)
75 |
76 | return d3
77 | .line<[number, number]>()
78 | .x((d) => xScale((d[1] / yMax) * xMax)!)
79 | .y((d) => yScale(-d[0])!)
80 | }
81 | })
82 | export const plotDataSelector = selector({
83 | key: 'plot-data',
84 | get: ({ get }) => {
85 | const data = get(dataState)
86 | const line = get(lineSelector)
87 | return data.map((data) => line(data) || undefined)
88 | }
89 | })
90 |
--------------------------------------------------------------------------------
/src/components/formatters.ts:
--------------------------------------------------------------------------------
1 | const toFixed = (float: number, digits = 0) => {
2 | const padding = Math.pow(10, digits)
3 | return (Math.round(float * padding) / padding).toFixed(digits)
4 | }
5 |
6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
7 | type ohNoItIsAny = any
8 |
9 | export function formatTime(s: ohNoItIsAny) {
10 | s = Number(s)
11 | if (!Number.isFinite(s)) return '--'
12 |
13 | const m = s / 60
14 | const h = s / 60 / 60
15 | const ms = s * 1000
16 | const us = ms * 1000
17 | if (us < 1000) return toFixed(us, 0) + 'μs'
18 | if (ms < 10) return toFixed(ms, 2) + 'ms'
19 | if (ms < 1000) return toFixed(ms) + 'ms'
20 | if (s < 10) return toFixed(s, 1) + 's'
21 | if (h > 1) return toFixed(h, 0) + 'h' + toFixed(m % 60, 1) + 'm'
22 | if (m > 5) return toFixed(m, 0) + 'm' + toFixed(s % 60, 1) + 's'
23 | return toFixed(s, 0) + 's'
24 | }
25 |
26 | export function formatFreq(hz: number) {
27 | if (!Number.isFinite(hz)) return '--'
28 |
29 | const khz = hz / 1000
30 | if (hz < 1000) return toFixed(hz) + 'Hz'
31 | if (khz < 10) return toFixed(khz, 2) + 'KHz'
32 | return toFixed(khz) + 'KHz'
33 | }
34 | export function formatVoltage(v: number): string {
35 | if (!Number.isFinite(v)) return '--'
36 |
37 | if (v < 0) return '-' + formatVoltage(-v)
38 | const mv = v * 1000
39 | const uv = mv * 1000
40 | if (uv < 10) return toFixed(uv, 2) + 'µv'
41 | if (uv < 50) return toFixed(uv, 1) + 'µv'
42 | if (uv < 1000) return toFixed(uv, 0) + 'µv'
43 | if (mv < 10) return toFixed(mv, 2) + 'mv'
44 | if (mv < 50) return toFixed(mv, 1) + 'mv'
45 | if (mv < 1000) return toFixed(mv, 0) + 'mv'
46 | return toFixed(v, 2) + 'v'
47 | }
48 |
--------------------------------------------------------------------------------
/src/dataMock.ts:
--------------------------------------------------------------------------------
1 | export default [
2 | // const triggerVoltageInt = get(myData, 'uint8')
3 | 145,
4 | // const triggerDir = get(myData, 'uint8')
5 | 0,
6 | // const ADC_MAIN_CLOCK_TICKS = get(myData, 'uint16')
7 | 79,
8 | 0,
9 | // const triggerPos = get(myData, 'int16')
10 | 83,
11 | 0,
12 | // const didTrigger = get(myData, 'uint8')
13 | 0,
14 | // const freeMemory = get(myData, 'int16')
15 | 48,
16 | 2,
17 | // const SERIAL_SAMPLES = get(myData, 'int16')
18 | 244,
19 | 1,
20 |
21 | // DATA:
22 | 95,
23 | 91,
24 | 81,
25 | 78,
26 | 69,
27 | 64,
28 | 60,
29 | 50,
30 | 48,
31 | 44,
32 | 36,
33 | 33,
34 | 28,
35 | 24,
36 | 22,
37 | 16,
38 | 15,
39 | 14,
40 | 12,
41 | 12,
42 | 11,
43 | 11,
44 | 12,
45 | 13,
46 | 14,
47 | 15,
48 | 20,
49 | 22,
50 | 25,
51 | 30,
52 | 33,
53 | 41,
54 | 44,
55 | 47,
56 | 56,
57 | 60,
58 | 68,
59 | 73,
60 | 77,
61 | 88,
62 | 90,
63 | 95,
64 | 104,
65 | 108,
66 | 115,
67 | 120,
68 | 124,
69 | 131,
70 | 134,
71 | 136,
72 | 143,
73 | 143,
74 | 148,
75 | 150,
76 | 151,
77 | 152,
78 | 153,
79 | 153,
80 | 152,
81 | 152,
82 | 150,
83 | 148,
84 | 146,
85 | 141,
86 | 140,
87 | 137,
88 | 130,
89 | 128,
90 | 120,
91 | 116,
92 | 112,
93 | 102,
94 | 100,
95 | 95,
96 | 85,
97 | 82,
98 | 76,
99 | 68,
100 | 64,
101 | 56,
102 | 52,
103 | 48,
104 | 39,
105 | 37,
106 | 31,
107 | 27,
108 | 24,
109 | 19,
110 | 17,
111 | 16,
112 | 12,
113 | 12,
114 | 12,
115 | 11,
116 | 11,
117 | 12,
118 | 13,
119 | 14,
120 | 18,
121 | 19,
122 | 23,
123 | 28,
124 | 30,
125 | 37,
126 | 40,
127 | 44,
128 | 53,
129 | 56,
130 | 60,
131 | 69,
132 | 72,
133 | 82,
134 | 86,
135 | 91,
136 | 100,
137 | 104,
138 | 109,
139 | 116,
140 | 120,
141 | 128,
142 | 130,
143 | 134,
144 | 140,
145 | 142,
146 | 145,
147 | 148,
148 | 150,
149 | 152,
150 | 152,
151 | 153,
152 | 153,
153 | 152,
154 | 151,
155 | 149,
156 | 148,
157 | 143,
158 | 142,
159 | 140,
160 | 132,
161 | 131,
162 | 126,
163 | 120,
164 | 116,
165 | 107,
166 | 104,
167 | 100,
168 | 89,
169 | 86,
170 | 82,
171 | 72,
172 | 69,
173 | 60,
174 | 56,
175 | 52,
176 | 42,
177 | 40,
178 | 36,
179 | 29,
180 | 28,
181 | 22,
182 | 19,
183 | 17,
184 | 14,
185 | 13,
186 | 12,
187 | 11,
188 | 11,
189 | 12,
190 | 12,
191 | 12,
192 | 16,
193 | 17,
194 | 19,
195 | 25,
196 | 27,
197 | 32,
198 | 36,
199 | 40,
200 | 49,
201 | 51,
202 | 56,
203 | 65,
204 | 68,
205 | 76,
206 | 82,
207 | 86,
208 | 96,
209 | 99,
210 | 104,
211 | 112,
212 | 116,
213 | 124,
214 | 128,
215 | 130,
216 | 138,
217 | 140,
218 | 142,
219 | 147,
220 | 148,
221 | 151,
222 | 152,
223 | 152,
224 | 153,
225 | 153,
226 | 152,
227 | 150,
228 | 150,
229 | 146,
230 | 143,
231 | 142,
232 | 136,
233 | 134,
234 | 130,
235 | 123,
236 | 120,
237 | 111,
238 | 108,
239 | 104,
240 | 93,
241 | 91,
242 | 86,
243 | 76,
244 | 73,
245 | 66,
246 | 60,
247 | 56,
248 | 46,
249 | 44,
250 | 40,
251 | 32,
252 | 30,
253 | 24,
254 | 22,
255 | 20,
256 | 15,
257 | 14,
258 | 13,
259 | 11,
260 | 11,
261 | 11,
262 | 12,
263 | 12,
264 | 14,
265 | 15,
266 | 17,
267 | 22,
268 | 24,
269 | 28,
270 | 33,
271 | 36,
272 | 44,
273 | 47,
274 | 52,
275 | 61,
276 | 64,
277 | 70,
278 | 78,
279 | 81,
280 | 92,
281 | 94,
282 | 99,
283 | 108,
284 | 111,
285 | 120,
286 | 124,
287 | 128,
288 | 135,
289 | 136,
290 | 140,
291 | 145,
292 | 146,
293 | 148,
294 | 151,
295 | 152,
296 | 153,
297 | 153,
298 | 153,
299 | 152,
300 | 151,
301 | 148,
302 | 146,
303 | 143,
304 | 138,
305 | 137,
306 | 134,
307 | 126,
308 | 124,
309 | 117,
310 | 112,
311 | 108,
312 | 98,
313 | 95,
314 | 91,
315 | 80,
316 | 78,
317 | 72,
318 | 64,
319 | 60,
320 | 51,
321 | 47,
322 | 44,
323 | 35,
324 | 33,
325 | 28,
326 | 24,
327 | 22,
328 | 17,
329 | 15,
330 | 14,
331 | 12,
332 | 12,
333 | 11,
334 | 11,
335 | 12,
336 | 13,
337 | 14,
338 | 15,
339 | 20,
340 | 22,
341 | 24,
342 | 31,
343 | 33,
344 | 40,
345 | 44,
346 | 47,
347 | 57,
348 | 60,
349 | 64,
350 | 74,
351 | 77,
352 | 88,
353 | 90,
354 | 95,
355 | 105,
356 | 108,
357 | 112,
358 | 120,
359 | 124,
360 | 131,
361 | 134,
362 | 136,
363 | 143,
364 | 143,
365 | 147,
366 | 150,
367 | 150,
368 | 152,
369 | 153,
370 | 153,
371 | 152,
372 | 152,
373 | 151,
374 | 148,
375 | 146,
376 | 141,
377 | 140,
378 | 137,
379 | 129,
380 | 128,
381 | 122,
382 | 115,
383 | 112,
384 | 103,
385 | 99,
386 | 95,
387 | 85,
388 | 82,
389 | 76,
390 | 67,
391 | 64,
392 | 56,
393 | 52,
394 | 48,
395 | 39,
396 | 36,
397 | 33,
398 | 26,
399 | 24,
400 | 20,
401 | 17,
402 | 16,
403 | 12,
404 | 12,
405 | 12,
406 | 11,
407 | 11,
408 | 12,
409 | 12,
410 | 14,
411 | 18,
412 | 19,
413 | 22,
414 | 28,
415 | 30,
416 | 36,
417 | 40,
418 | 44,
419 | 52,
420 | 56,
421 | 60,
422 | 69,
423 | 72,
424 | 81,
425 | 86,
426 | 90,
427 | 101,
428 | 104,
429 | 108,
430 | 117,
431 | 120,
432 | 127,
433 | 130,
434 | 134,
435 | 140,
436 | 142,
437 | 143,
438 | 148,
439 | 150,
440 | 152,
441 | 152,
442 | 153,
443 | 153,
444 | 152,
445 | 152,
446 | 149,
447 | 148,
448 | 145,
449 | 142,
450 | 140,
451 | 132,
452 | 131,
453 | 128,
454 | 119,
455 | 116,
456 | 108,
457 | 104,
458 | 100,
459 | 89,
460 | 86,
461 | 82,
462 | 72,
463 | 69,
464 | 61,
465 | 56,
466 | 52,
467 | 43,
468 | 40,
469 | 36,
470 | 29,
471 | 28,
472 | 23,
473 | 19,
474 | 17,
475 | 14,
476 | 12,
477 | 12,
478 | 11,
479 | 11,
480 | 12,
481 | 12,
482 | 12,
483 | 16,
484 | 17,
485 | 19,
486 | 25,
487 | 27,
488 | 32,
489 | 37,
490 | 40,
491 | 48,
492 | 52,
493 | 56,
494 | 65,
495 | 68,
496 | 76,
497 | 82,
498 | 86,
499 | 96,
500 | 99,
501 | 104,
502 | 113,
503 | 116,
504 | 122,
505 | 128,
506 | 130,
507 | 138,
508 | 140,
509 | 142,
510 | 147,
511 | 148,
512 | 150,
513 | 152,
514 | 152,
515 | 153,
516 | 153,
517 | 152,
518 | 150,
519 | 150,
520 | 148,
521 | 98,
522 | 187,
523 | 187,
524 | 187,
525 | 187,
526 | 187,
527 | 155,
528 | 155,
529 | 155,
530 | 159,
531 | 159,
532 | 159,
533 | 159,
534 | 159,
535 | 159,
536 | 159,
537 | 159,
538 | 159,
539 | 159,
540 | 159,
541 | 159,
542 | 159,
543 | 159,
544 | 159,
545 | 159,
546 | 159,
547 | 159,
548 | 155,
549 | 155,
550 | 155,
551 | 155,
552 | 155,
553 | 155,
554 | 155,
555 | 155,
556 | 155,
557 | 155,
558 | 155,
559 | 155,
560 | 155,
561 | 155,
562 | 155,
563 | 155,
564 | 155,
565 | 155,
566 | 155,
567 | 159,
568 | 159,
569 | 159,
570 | 191,
571 | 191,
572 | 191,
573 | 191,
574 | 191,
575 | 191,
576 | 191,
577 | 191,
578 | 191,
579 | 191,
580 | 191,
581 | 191,
582 | 191,
583 | 191,
584 | 191,
585 | 187,
586 | 187,
587 | 187,
588 | 187,
589 | 187,
590 | 187,
591 | 187,
592 | 187,
593 | 187,
594 | 187,
595 | 187,
596 | 187,
597 | 251,
598 | 251,
599 | 251,
600 | 219,
601 | 219,
602 | 219,
603 | 223,
604 | 223,
605 | 223,
606 | 223,
607 | 223,
608 | 223,
609 | 223,
610 | 223,
611 | 223,
612 | 223,
613 | 223,
614 | 223,
615 | 223,
616 | 223,
617 | 223,
618 | 223,
619 | 223,
620 | 223,
621 | 219,
622 | 219,
623 | 219,
624 | 219,
625 | 219,
626 | 219,
627 | 219,
628 | 219,
629 | 219,
630 | 219,
631 | 219,
632 | 219,
633 | 219,
634 | 219,
635 | 219,
636 | 219,
637 | 219,
638 | 219,
639 | 223,
640 | 223,
641 | 223,
642 | 223,
643 | 254,
644 | 254,
645 | 254,
646 | 254,
647 | 254,
648 | 254,
649 | 254,
650 | 254,
651 | 254,
652 | 254,
653 | 254,
654 | 254,
655 | 254,
656 | 254,
657 | 254,
658 | 251,
659 | 251,
660 | 251,
661 | 251,
662 | 251,
663 | 251,
664 | 251,
665 | 251,
666 | 251,
667 | 251,
668 | 251,
669 | 251,
670 | 251,
671 | 251,
672 | 251,
673 | 219,
674 | 219,
675 | 219,
676 | 223,
677 | 223,
678 | 223,
679 | 223,
680 | 223,
681 | 223,
682 | 223,
683 | 223,
684 | 223,
685 | 223,
686 | 223,
687 | 223,
688 | 223,
689 | 223,
690 | 223,
691 | 223,
692 | 223,
693 | 223,
694 | 219,
695 | 219,
696 | 219,
697 | 219,
698 | 219,
699 | 219,
700 | 219,
701 | 219,
702 | 219,
703 | 219,
704 | 219,
705 | 219,
706 | 219,
707 | 219,
708 | 219,
709 | 219,
710 | 219,
711 | 219,
712 | 223,
713 | 223,
714 | 159,
715 | 191,
716 | 191,
717 | 191,
718 | 191,
719 | 191,
720 | 191,
721 | 191,
722 | 191,
723 | 191,
724 | 191,
725 | 191,
726 | 191,
727 | 191,
728 | 191,
729 | 191,
730 | 187,
731 | 187,
732 | 187,
733 | 187,
734 | 187,
735 | 187,
736 | 187,
737 | 187,
738 | 187,
739 | 187,
740 | 187,
741 | 187,
742 | 187,
743 | 187,
744 | 187,
745 | 155,
746 | 155,
747 | 155,
748 | 159,
749 | 159,
750 | 159,
751 | 159,
752 | 159,
753 | 159,
754 | 159,
755 | 159,
756 | 159,
757 | 159,
758 | 159,
759 | 159,
760 | 159,
761 | 159,
762 | 159,
763 | 159,
764 | 159,
765 | 159,
766 | 159,
767 | 155,
768 | 155,
769 | 155,
770 | 155,
771 | 155,
772 | 155,
773 | 155,
774 | 155,
775 | 155,
776 | 155,
777 | 155,
778 | 155,
779 | 155,
780 | 155,
781 | 155,
782 | 155,
783 | 155,
784 | 155,
785 | 159,
786 | 159,
787 | 159,
788 | 191,
789 | 191,
790 | 191,
791 | 191,
792 | 191,
793 | 191,
794 | 191,
795 | 191,
796 | 191,
797 | 191,
798 | 191,
799 | 191,
800 | 191,
801 | 191,
802 | 191,
803 | 187,
804 | 187,
805 | 187,
806 | 187,
807 | 187,
808 | 187,
809 | 187,
810 | 187,
811 | 187,
812 | 187,
813 | 187,
814 | 187,
815 | 187,
816 | 187,
817 | 187,
818 | 155,
819 | 155,
820 | 155,
821 | 159,
822 | 159,
823 | 159,
824 | 159,
825 | 159,
826 | 159,
827 | 159,
828 | 159,
829 | 159,
830 | 159,
831 | 223,
832 | 223,
833 | 223,
834 | 223,
835 | 223,
836 | 223,
837 | 223,
838 | 223,
839 | 223,
840 | 219,
841 | 219,
842 | 219,
843 | 219,
844 | 219,
845 | 219,
846 | 219,
847 | 219,
848 | 219,
849 | 219,
850 | 219,
851 | 219,
852 | 219,
853 | 219,
854 | 219,
855 | 219,
856 | 219,
857 | 219,
858 | 223,
859 | 223,
860 | 223,
861 | 254,
862 | 254,
863 | 254,
864 | 254,
865 | 254,
866 | 254,
867 | 254,
868 | 254,
869 | 254,
870 | 254,
871 | 254,
872 | 254,
873 | 254,
874 | 254,
875 | 254,
876 | 251,
877 | 251,
878 | 251,
879 | 251,
880 | 251,
881 | 251,
882 | 251,
883 | 251,
884 | 251,
885 | 251,
886 | 251,
887 | 251,
888 | 251,
889 | 251,
890 | 251,
891 | 219,
892 | 219,
893 | 219,
894 | 223,
895 | 223,
896 | 223,
897 | 223,
898 | 223,
899 | 223,
900 | 223,
901 | 223,
902 | 223,
903 | 223,
904 | 223,
905 | 223,
906 | 223,
907 | 223,
908 | 223,
909 | 223,
910 | 223,
911 | 223,
912 | 219,
913 | 219,
914 | 219,
915 | 219,
916 | 219,
917 | 219,
918 | 219,
919 | 219,
920 | 219,
921 | 219,
922 | 219,
923 | 219,
924 | 219,
925 | 219,
926 | 219,
927 | 219,
928 | 219,
929 | 219,
930 | 223,
931 | 223,
932 | 223,
933 | 223,
934 | 254,
935 | 254,
936 | 254,
937 | 254,
938 | 254,
939 | 254,
940 | 254,
941 | 254,
942 | 254,
943 | 191,
944 | 191,
945 | 191,
946 | 191,
947 | 191,
948 | 191,
949 | 187,
950 | 187,
951 | 187,
952 | 187,
953 | 187,
954 | 187,
955 | 187,
956 | 187,
957 | 187,
958 | 187,
959 | 187,
960 | 187,
961 | 187,
962 | 187,
963 | 187,
964 | 155,
965 | 155,
966 | 155,
967 | 159,
968 | 159,
969 | 159,
970 | 159,
971 | 159,
972 | 159,
973 | 159,
974 | 159,
975 | 159,
976 | 159,
977 | 159,
978 | 159,
979 | 159,
980 | 159,
981 | 159,
982 | 159,
983 | 159,
984 | 159,
985 | 155,
986 | 155,
987 | 155,
988 | 155,
989 | 155,
990 | 155,
991 | 155,
992 | 155,
993 | 155,
994 | 155,
995 | 155,
996 | 155,
997 | 155,
998 | 155,
999 | 155,
1000 | 155,
1001 | 155,
1002 | 155,
1003 | 159,
1004 | 159,
1005 | 159,
1006 | 159,
1007 | 191,
1008 | 191,
1009 | 191,
1010 | 191,
1011 | 191,
1012 | 191,
1013 | 191,
1014 | 191,
1015 | 191,
1016 | 191,
1017 | 191,
1018 | 191,
1019 | 191,
1020 | 191
1021 | ]
1022 |
--------------------------------------------------------------------------------
/src/dsp/fourier-transform.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'fourier-transform' {
2 | function ft(n: number[]): number[]
3 | export = ft
4 | }
5 |
--------------------------------------------------------------------------------
/src/dsp/spectrum.ts:
--------------------------------------------------------------------------------
1 | import ft from 'fourier-transform'
2 | import { PlotDatum } from '../communication/bindings'
3 |
4 | const average = (arr: number[]) =>
5 | arr.reduce((acc, n) => acc + n, 0) / arr.length
6 | export function getFFT(data: PlotDatum[]) {
7 | let signal = data.map(({ v }) => v)
8 | if (signal.length === 0) return []
9 | if (signal.length < 2) {
10 | console.log('fix me')
11 | return []
12 | }
13 | const length = data.length
14 | const dt = data[length - 1].t - data[length - 2].t
15 |
16 | const mid = average(signal)
17 | signal = signal.map((v) => v - mid)
18 | // const nextPowerOf2 = Math.ceil(Math.log2(signal.length))
19 | const nextPowerOf2 = Math.ceil(Math.log2(512))
20 | const padding = Math.pow(2, nextPowerOf2) - signal.length
21 | let paddedSignal = signal
22 | if (padding > 0) {
23 | paddedSignal = [...signal, ...Array(padding).fill(0)]
24 | }
25 | if (padding < 0) {
26 | paddedSignal = signal.slice(0, 512)
27 | }
28 | const fft = ft(paddedSignal)
29 | // https://www.dsprelated.com/showthread/comp.dsp/87526-1.php
30 | const normalized = fft.map((v) => (512 * v) / signal.length)
31 | // const normalized = fft.map((v) => v * 2)
32 | return normalized.map((v, i) => ({ v, t: dt * i }))
33 | }
34 |
35 | export function getFrequencyCount(data: PlotDatum[]) {
36 | if (data.length < 2) return 0
37 | const signal = data.map(({ v }) => v)
38 | const max = Math.max(...signal)
39 | const min = Math.min(...signal)
40 |
41 | const lowThird = (max + min * 2) / 3
42 | const mid = (max + min) / 2
43 | let firstCross = -1
44 | let lastCross = 0
45 | let count = 0
46 | let locked = true
47 | for (let i = 1; i < data.length; i++) {
48 | if (data[i].v < lowThird) locked = false
49 | const risingCross = !locked && data[i - 1].v < mid && data[i].v >= mid
50 | if (risingCross) {
51 | locked = true
52 | count++
53 | if (firstCross < 0) firstCross = data[i].t
54 | lastCross = data[i].t
55 | }
56 | }
57 | const result = (count - 1) / (lastCross - firstCross)
58 | if (count > 1 && Number.isFinite(result)) return result
59 | return Number.NaN
60 | }
61 |
62 | export function oversample(
63 | factor: number,
64 | newBuffer: PlotDatum[],
65 | oldBuffer: PlotDatum[]
66 | ) {
67 | return newBuffer.map(({ v, t }, j) => ({
68 | t,
69 | v: (oldBuffer[j]?.v || 0) * factor + v * (1 - factor)
70 | }))
71 | }
72 |
--------------------------------------------------------------------------------
/src/index.scss:
--------------------------------------------------------------------------------
1 | @import '~rsuite/dist/styles/rsuite-default';
2 | @import './components/App.scss';
3 | @import './components/Plot/Plot.scss';
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import './index.scss'
2 |
3 | import React from 'react'
4 | import ReactDOM from 'react-dom'
5 |
6 | import App from './components/App'
7 | import * as serviceWorker from './serviceWorker'
8 | import { RecoilRoot } from 'recoil'
9 |
10 | ReactDOM.render(
11 |
12 |
13 |
14 |
15 | ,
16 | document.getElementById('root')
17 | )
18 |
19 | // If you want your app to work offline and load faster, you can change
20 | // unregister() to register() below. Note this comes with some pitfalls.
21 | // Learn more about service workers: https://bit.ly/CRA-PWA
22 | serviceWorker.unregister()
23 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/serviceWorker.ts:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | type Config = {
24 | onSuccess?: (registration: ServiceWorkerRegistration) => void;
25 | onUpdate?: (registration: ServiceWorkerRegistration) => void;
26 | };
27 |
28 | export function register(config?: Config) {
29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
30 | // The URL constructor is available in all browsers that support SW.
31 | const publicUrl = new URL(
32 | process.env.PUBLIC_URL,
33 | window.location.href
34 | );
35 | if (publicUrl.origin !== window.location.origin) {
36 | // Our service worker won't work if PUBLIC_URL is on a different origin
37 | // from what our page is served on. This might happen if a CDN is used to
38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
39 | return;
40 | }
41 |
42 | window.addEventListener('load', () => {
43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
44 |
45 | if (isLocalhost) {
46 | // This is running on localhost. Let's check if a service worker still exists or not.
47 | checkValidServiceWorker(swUrl, config);
48 |
49 | // Add some additional logging to localhost, pointing developers to the
50 | // service worker/PWA documentation.
51 | navigator.serviceWorker.ready.then(() => {
52 | console.log(
53 | 'This web app is being served cache-first by a service ' +
54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
55 | );
56 | });
57 | } else {
58 | // Is not localhost. Just register service worker
59 | registerValidSW(swUrl, config);
60 | }
61 | });
62 | }
63 | }
64 |
65 | function registerValidSW(swUrl: string, config?: Config) {
66 | navigator.serviceWorker
67 | .register(swUrl)
68 | .then(registration => {
69 | registration.onupdatefound = () => {
70 | const installingWorker = registration.installing;
71 | if (installingWorker == null) {
72 | return;
73 | }
74 | installingWorker.onstatechange = () => {
75 | if (installingWorker.state === 'installed') {
76 | if (navigator.serviceWorker.controller) {
77 | // At this point, the updated precached content has been fetched,
78 | // but the previous service worker will still serve the older
79 | // content until all client tabs are closed.
80 | console.log(
81 | 'New content is available and will be used when all ' +
82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
83 | );
84 |
85 | // Execute callback
86 | if (config && config.onUpdate) {
87 | config.onUpdate(registration);
88 | }
89 | } else {
90 | // At this point, everything has been precached.
91 | // It's the perfect time to display a
92 | // "Content is cached for offline use." message.
93 | console.log('Content is cached for offline use.');
94 |
95 | // Execute callback
96 | if (config && config.onSuccess) {
97 | config.onSuccess(registration);
98 | }
99 | }
100 | }
101 | };
102 | };
103 | })
104 | .catch(error => {
105 | console.error('Error during service worker registration:', error);
106 | });
107 | }
108 |
109 | function checkValidServiceWorker(swUrl: string, config?: Config) {
110 | // Check if the service worker can be found. If it can't reload the page.
111 | fetch(swUrl, {
112 | headers: { 'Service-Worker': 'script' }
113 | })
114 | .then(response => {
115 | // Ensure service worker exists, and that we really are getting a JS file.
116 | const contentType = response.headers.get('content-type');
117 | if (
118 | response.status === 404 ||
119 | (contentType != null && contentType.indexOf('javascript') === -1)
120 | ) {
121 | // No service worker found. Probably a different app. Reload the page.
122 | navigator.serviceWorker.ready.then(registration => {
123 | registration.unregister().then(() => {
124 | window.location.reload();
125 | });
126 | });
127 | } else {
128 | // Service worker found. Proceed as normal.
129 | registerValidSW(swUrl, config);
130 | }
131 | })
132 | .catch(() => {
133 | console.log(
134 | 'No internet connection found. App is running in offline mode.'
135 | );
136 | });
137 | }
138 |
139 | export function unregister() {
140 | if ('serviceWorker' in navigator) {
141 | navigator.serviceWorker.ready
142 | .then(registration => {
143 | registration.unregister();
144 | })
145 | .catch(error => {
146 | console.error(error.message);
147 | });
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/src/win.ts:
--------------------------------------------------------------------------------
1 | export default window as any
2 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "noEmit": true,
20 | "jsx": "react"
21 | },
22 | "include": [
23 | "src"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------