├── .eslintignore ├── .eslintrc ├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── arduino analog2 ├── .vscode │ ├── arduino.json │ ├── c_cpp_properties.json │ └── settings.json └── src │ ├── adc.h │ ├── input.h │ └── src.ino ├── arduino ├── .vscode │ ├── arduino.json │ ├── c_cpp_properties.json │ └── settings.json └── src │ ├── MemoryFree.cpp │ ├── MemoryFree.h │ ├── TODOS.txt │ ├── adc.h │ ├── dac.h │ ├── data-struct.h │ ├── fillBuffer.h │ ├── input.h │ ├── output.h │ └── src.ino ├── package.json ├── public ├── ExperimentalWebPlatformFeatures.png ├── address-bar.png ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json ├── robots.txt ├── screenshot.png └── src.ino.hex ├── src ├── communication │ ├── Serial.ts │ ├── Serial2.ts │ ├── bindings.tsx │ ├── bindingsHelper.ts │ ├── parseSerial.ts │ └── profile.ts ├── components │ ├── About.tsx │ ├── App.scss │ ├── App.tsx │ ├── Controls │ │ ├── Amplifier.tsx │ │ ├── Channels.tsx │ │ ├── Scales.tsx │ │ ├── SerialControls.tsx │ │ ├── Stats.tsx │ │ ├── TimeScales.tsx │ │ ├── Trigger.tsx │ │ ├── Uploader.tsx │ │ ├── hooks.ts │ │ ├── index.tsx │ │ ├── intel-hex.d.ts │ │ └── stk500.d.ts │ ├── EnableSerialInstructions.tsx │ ├── Plot │ │ ├── Measure.tsx │ │ ├── Plot.scss │ │ ├── Plot.tsx │ │ ├── TriggerPosHandle.tsx │ │ ├── TriggerVoltageHandle.tsx │ │ ├── XAxis.tsx │ │ ├── YAxis.tsx │ │ └── hooks.ts │ └── formatters.ts ├── dataMock.ts ├── dsp │ ├── fourier-transform.d.ts │ └── spectrum.ts ├── index.scss ├── index.tsx ├── react-app-env.d.ts ├── serviceWorker.ts ├── setupTests.ts └── win.ts ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | // .eslintignore 2 | build/* 3 | public/* 4 | src/react-app-env.d.ts 5 | src/serviceWorker.ts -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "jsx": true, 5 | "useJSXTextNode": true 6 | }, 7 | "extends": [ 8 | "plugin:react/recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "prettier/@typescript-eslint", 11 | "prettier" 12 | ], 13 | "plugins": ["@typescript-eslint", "react-hooks"], 14 | "rules": { 15 | "@typescript-eslint/explicit-function-return-type": "off", 16 | "@typescript-eslint/explicit-module-boundary-types": "off", 17 | "@typescript-eslint/no-non-null-assertion": "off", 18 | "@typescript-eslint/no-empty-function": "off", 19 | "@typescript-eslint/ban-ts-comment": "warn", 20 | "@typescript-eslint/no-use-before-define": "off", 21 | "react-hooks/rules-of-hooks": "error", 22 | "no-mixed-operators": "off", 23 | "react-hooks/exhaustive-deps": [ 24 | "warn", 25 | { 26 | "additionalHooks": "(useRecoilCallback|useSetRecoilState)" 27 | } 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deployment 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | deploy: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | node-version: [12.x] 12 | steps: 13 | - uses: actions/checkout@v1 14 | - name: Use Node.js ${{ matrix.node-version }} 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: ${{ matrix.node-version }} 18 | - name: Install Packages 19 | run: yarn 20 | - name: Build page 21 | run: yarn run build 22 | - name: Deploy to gh-pages 23 | uses: peaceiris/actions-gh-pages@v3 24 | with: 25 | deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }} 26 | publish_dir: ./build 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | #arduino 26 | 27 | arduino/build 28 | arduino analog2/build 29 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "trailingComma": "none", 4 | "singleQuote": true, 5 | "printWidth": 80 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 David Buezas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # arduino-web-oscilloscope 2 | 3 | Try it now without having to install anything at [https://dbuezas.github.io/arduino-web-oscilloscope/](https://dbuezas.github.io/arduino-web-oscilloscope/) (only supported in Chrome) 4 | 5 | # Boards support 6 | Currently only supports *lgt328p* boards. If there's demand, I'll make an *Arduino Uno* and *STM32* versions. 7 | 8 | # How to use 9 | To start using it, just plug the board, click upload firmware and plug your signal to A0. You can also connect digital signals to A4 and A5. 10 | 11 | 12 | 13 | # Web firmware uploader 14 | If you want to add browser firmware upload to your page (also supports atmel MCUs), visit https://github.com/dbuezas/arduino-web-uploader 15 | 16 | # Featured in Hackaday! 17 | This makes me so happy! thanks Hackaday! 18 | https://hackaday.com/2021/02/22/slick-web-oscilloscope-is-ready-in-a-flash-literally/ 19 | 20 | # the react stuff 21 | 22 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 23 | 24 | ## Available Scripts 25 | 26 | In the project directory, you can run: 27 | 28 | ### `yarn start` 29 | 30 | Runs the app in the development mode.
31 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 32 | 33 | The page will reload if you make edits.
34 | You will also see any lint errors in the console. 35 | 36 | ### `yarn test` 37 | 38 | Launches the test runner in the interactive watch mode.
39 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 40 | 41 | ### `yarn build` 42 | 43 | Builds the app for production to the `build` folder.
44 | It correctly bundles React in production mode and optimizes the build for the best performance. 45 | 46 | The build is minified and the filenames include the hashes.
47 | Your app is ready to be deployed! 48 | 49 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 50 | 51 | ### `yarn eject` 52 | 53 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 54 | 55 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 56 | 57 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 58 | 59 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 60 | 61 | ## Learn More 62 | 63 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 64 | 65 | To learn React, check out the [React documentation](https://reactjs.org/). 66 | 67 | ## LICENSE 68 | 69 | MIT License 70 | 71 | Copyright (c) 2020 David Buezas 72 | 73 | Permission is hereby granted, free of charge, to any person obtaining a copy 74 | of this software and associated documentation files (the "Software"), to deal 75 | in the Software without restriction, including without limitation the rights 76 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 77 | copies of the Software, and to permit persons to whom the Software is 78 | furnished to do so, subject to the following conditions: 79 | 80 | The above copyright notice and this permission notice shall be included in all 81 | copies or substantial portions of the Software. 82 | 83 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 84 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 85 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 86 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 87 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 88 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 89 | SOFTWARE. 90 | 91 | [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/dbuezas) 92 | -------------------------------------------------------------------------------- /arduino analog2/.vscode/arduino.json: -------------------------------------------------------------------------------- 1 | { 2 | "sketch": "src/src.ino", 3 | "board": "LGT8fx Boards:avr:328", 4 | "port": "/dev/tty.usbmodem0001", 5 | "output": "build", 6 | "programmer": "AVR ISP", 7 | "configuration": "arduino_isp=disable,clock_source=internal,clock=32MHz,variant=modelP" 8 | } -------------------------------------------------------------------------------- /arduino analog2/.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Mac", 5 | "includePath": [ 6 | "~/code/arduino/libraries/**", 7 | "~/code/lgt8fx/lgt8f/cores/**", 8 | "~/code/lgt8fx/lgt8f/libraries/**", 9 | "~/code/lgt8fx/lgt8f/variants/**", 10 | "/System/Library/Frameworks/Kernel.framework/Versions/A/Headers", 11 | "/Applications/Arduino.app/Contents/Java/hardware/tools/**", 12 | "~/Library/Arduino15/packages/LGT8fx Boards/hardware/avr/1.0.6/**", 13 | "/Applications/Arduino.app/Contents/Java/hardware/tools/avr/avr/include", 14 | "/Applications/Arduino.app/Contents/Java/hardware/arduino/avr/libraries/**", 15 | "/Users/davidbuezas/Library/Arduino15/packages/LGT8fx Boards/hardware/avr/1.0.6/**" 16 | ], 17 | "browse": { 18 | "limitSymbolsToIncludedHeaders": false, 19 | "path": [ 20 | "/System/Library/Frameworks/Kernel.framework/Versions/A/Headers", 21 | "/Applications/Arduino.app/Contents/Java/", 22 | "~/code/arduino-web-oscilloscope/**", 23 | "~/code/lgt8fx/lgt8f/" 24 | ] 25 | }, 26 | "forcedInclude": [ 27 | "/Applications/Arduino.app/Contents/Java/hardware/tools/avr/avr/include/avr/iomx8.h", 28 | "~/code/lgt8fx/lgt8f/variants/lgt8fx8p/pins_arduino.h", 29 | "~/code/lgt8fx/lgt8f/cores/lgt8f/lgtx8p.h", 30 | "~/code/lgt8fx/lgt8f/cores/lgt8f/Arduino.h", 31 | "~/code/lgt8fx/lgt8f/cores/lgt8f/lgtx8p_documentation.h" 32 | ], 33 | "macFrameworkPath": ["/System/Library/Frameworks", "/Library/Frameworks"], 34 | "intelliSenseMode": "clang-x64", 35 | "compilerPath": "/Applications/Arduino.app/Contents/Java/hardware/tools/avr/bin/avr-g++ -c -g -Os -w -std=gnu++11 -fpermissive -fno-exceptions -ffunction-sections -fdata-sections -fno-threadsafe-statics -flto -w -x c++ -E -CC -mmcu=atmega328p", 36 | "defines": [ 37 | "CLOCK_SOURCE=1", 38 | "F_CPU=32000000L", 39 | "ARDUINO=10810", 40 | "ARDUINO_AVR_LARDU_328E", 41 | "ARDUINO_ARCH_AVR" 42 | ] 43 | } 44 | ], 45 | "version": 4 46 | } 47 | -------------------------------------------------------------------------------- /arduino analog2/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "istream": "cpp", 4 | "*.tcc": "cpp", 5 | "ostream": "cpp", 6 | "complex": "cpp", 7 | "cmath": "cpp", 8 | "__locale": "cpp", 9 | "__bit_reference": "cpp", 10 | "bitset": "cpp", 11 | "vector": "cpp", 12 | "**/ios": "c", 13 | "*.def": "cpp", 14 | "__hash_table": "cpp", 15 | "__tree": "cpp", 16 | "algorithm": "cpp", 17 | "chrono": "cpp", 18 | "deque": "cpp", 19 | "string_view": "cpp", 20 | "fstream": "cpp", 21 | "limits": "cpp", 22 | "list": "cpp", 23 | "locale": "cpp", 24 | "memory": "cpp", 25 | "string": "cpp", 26 | "rope": "cpp", 27 | "hashtable": "cpp", 28 | "random": "cpp", 29 | "__mutex_base": "cpp", 30 | "mutex": "cpp" 31 | }, 32 | "C_Cpp.exclusionPolicy": "checkFilesAndFolders", 33 | "files.exclude": { 34 | // "**/iom*": true, 35 | // "**/iom3": true, 36 | // "**/iom32": true, 37 | // "**/iom328p.h": false, 38 | 39 | "**/iom328.h": true, 40 | "**/iom328pb.h": true, 41 | "**/iom32.*": true, 42 | "**/iom320*": true, 43 | "**/iom323*": true, 44 | "**/iom324*": true, 45 | "**/iom325*": true, 46 | "**/iom329*": true, 47 | "**/iom32a*": true, 48 | "**/iom32c*": true, 49 | "**/iom32h*": true, 50 | "**/iom32m*": true, 51 | "**/iom32u*": true, 52 | "**/iom30*": true, 53 | "**/iom1*": true, 54 | "**/iom2*": true, 55 | "**/iom4*": true, 56 | "**/iom6*": true, 57 | "**/iom8*": true, 58 | "**/iomx*": true, 59 | "**/io1*": true, 60 | "**/io2*": true, 61 | "**/io4*": true, 62 | "**/io7*": true, 63 | "**/io8*": true, 64 | "**/io9*": true, 65 | "**/ioa*": true, 66 | "**/ioc*": true, 67 | "**/iot*": true, 68 | "**/iou*": true, 69 | "**/iox*": true 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /arduino analog2/src/adc.h: -------------------------------------------------------------------------------- 1 | 2 | #define DAPEN 7 3 | #define GA0 5 4 | #define DNS0 2 5 | #define DPS0 0 6 | #define DIFFS 1 7 | 8 | /* 9 | 0 -> 2 -> 727.28 kSamples 10 | 1 -> 2 -> 727.28 kSamples // adc can't read over 2.5v here 11 | 2 -> 4 -> 363.64 kSamples 12 | 3 -> 8 -> 181.82 kSamples 13 | 4 -> 16 -> 90.91 kSamples 14 | 5 -> 32 -> 45.455 KSamples 15 | 6 -> 64 -> 22.727 kSamples 16 | 7 -> 128 -> 11,363 kSamples 17 | */ 18 | #define ADC_PRESCALER_conf 2 // 1 to 7 19 | // #define ADC_PRESCALER (1 << ADC_PRESCALER_conf) 20 | // uint16_t ticksPerAdcRead = 21 | // ADC_PRESCALER * (15 + 1.5 + 5.5) - 22 | // 9; // A normal conversion takes 15 ADC clock cycles 23 | // // + 1.5 sample and hold + (5.5 unaccounted for) -9 for timing 24 | // overhead 25 | 26 | #define DIV_1 0b0000 27 | #define DIV_1_5 0b1000 28 | #define DIV_4_5 0b1110 29 | #define GAIN1 0b00 30 | #define GAIN8 0b01 31 | #define GAIN16 0b10 32 | #define GAIN32 0b11 33 | const uint8_t gainArray[][2] = { 34 | {GAIN1, DIV_1_5}, {GAIN1, DIV_4_5}, {GAIN1, DIV_1}, {GAIN8, DIV_1_5}, 35 | {GAIN16, DIV_1_5}, {GAIN32, DIV_1_5}, {GAIN8, DIV_4_5}, {GAIN8, DIV_1}, 36 | {GAIN16, DIV_4_5}, {GAIN16, DIV_1}, {GAIN32, DIV_4_5}, {GAIN32, DIV_1}, 37 | }; 38 | inline void startADC(uint8_t prescaler, uint8_t amplifier) { 39 | // ADC0 ---> 40 | // -[ADCSRD]-> voltage division -> 41 | // -[ADMUX]-> muxer -> 42 | // -[DAPCR]-> diff amplifier -> 43 | // -[ADCSRD]-> ADC -> 44 | 45 | // 2, 0.5, 0.25, 0.125, 0.0625, 0.03125, 0.015625 46 | 47 | uint8_t gain = gainArray[amplifier][0]; 48 | uint8_t divisor = gainArray[amplifier][1]; 49 | 50 | ADMUX = 1 << REFS0 | // ADC refference is AVCC [seel also: ADCSRD:REFS2] 51 | 0 << REFS1 | // ADC refference is AVCC [seel also: ADCSRD:REFS2] 52 | 1 << ADLAR | // ADC data register is left adjustment 53 | divisor << MUX0; 54 | // 0b0000 << MUX0; // ADC0 55 | // 0b1000 << MUX0; // 1/5 ADC0 56 | // 0b1110 << MUX0; // 4/5 ADC0 57 | 58 | ADCSRA = 1 << ADEN | // enable ADC 59 | 1 << ADSC | // start conversion 60 | 1 << ADATE | // ADC auto triggering enable 61 | 0 << ADIE | // disable ADC interrupt 62 | prescaler << ADPS0; 63 | ADCSRB = 0 << ADTS0; // Continuous conversion 64 | 65 | ADCSRC = 1 << DIFFS | // 1 = from diff amplifier, 0=multiplexer 66 | 0 << SPN | // ADC conversion input polarity control 67 | 0 << SPD; // 1: high speed conversion (can't hear a difference) 68 | ADCSRD = 0 << REFS2 | // part of ADC reference voltage [see ADMUX:REFS0] 69 | 0b00 << IVSEL0 | // 2v DAC output 70 | 0b001 << VDS0; // ADC0 voltage division 71 | DAPCR = 0b1 << DAPEN | // Enable 72 | // 0b00 << GA0 | // gain 73 | gain << GA0 | // gain 74 | 0b110 << DNS0 | // (-) GND 75 | 0b00 << DPS0; // (+) MUX 76 | } 77 | 78 | inline void stopADC() { ADCSRA = 0; } 79 | 80 | void setupADC() { 81 | bitSet(DIDR0, ADC0D); // disable digital input (reduce noise) 82 | pinMode(A0, INPUT); 83 | } 84 | -------------------------------------------------------------------------------- /arduino analog2/src/input.h: -------------------------------------------------------------------------------- 1 | SoftwareSerial mySerial(3, 2); 2 | 3 | void saveInput(char option, float val) { 4 | switch (option) { 5 | case 'A': 6 | int amplifier = constrain(val, 0, 11); 7 | startADC(2, amplifier); 8 | break; 9 | } 10 | } 11 | 12 | /* 13 | It is a pitty that I need to have my own buffer on top of the circular buffer 14 | that HardwareSerial has. This is because there is no method to read that 15 | buffer without consuming it. 16 | */ 17 | #define INPUT_BUFFER_SIZE 35 18 | char inputBuffer[INPUT_BUFFER_SIZE]; 19 | byte ptr = 0; 20 | bool handleInput() { 21 | bool wait = false; 22 | while (mySerial.available()) { 23 | int s = mySerial.read(); 24 | if (s == '>') { 25 | char option = inputBuffer[0]; 26 | float val = atof(inputBuffer + 1); 27 | ptr = 0; 28 | inputBuffer[ptr] = 0; 29 | 30 | saveInput(option, val); 31 | wait = false; 32 | } else { 33 | if (ptr >= INPUT_BUFFER_SIZE - 1) { 34 | // don't write outside the array 35 | // this is actually an exception. 36 | ptr = 0; 37 | } 38 | inputBuffer[ptr] = (char)s; 39 | ptr++; 40 | inputBuffer[ptr] = 0; 41 | // delayMicroseconds(100); 42 | // give time to receive the rest of the message before filling the buffer 43 | // with trash. Not using normal delay because it uses micros() and that 44 | // timer is off 45 | wait = true; 46 | } 47 | } 48 | return wait; 49 | } 50 | -------------------------------------------------------------------------------- /arduino analog2/src/src.ino: -------------------------------------------------------------------------------- 1 | #include "SoftwareSerial.h" 2 | #include "adc.h" 3 | #include "input.h" 4 | 5 | // #define DEBUG 6 | void setup() { 7 | setupADC(); 8 | startADC(2, 4); 9 | // noInterrupts(); 10 | DDRB = 0b00011111; 11 | DDRD = 0b11100000; 12 | mySerial.begin(115200); 13 | TIMSK0 &= ~_BV(TOIE0); // disable timer0 overflow interrupt 14 | } 15 | 16 | void loop() { 17 | handleInput(); 18 | for (int i = 0; i < 1000; i++) { 19 | loop_until_bit_is_set(ADCSRA, ADIF); 20 | bitSet(ADCSRA, ADIF); // this actually clears the bit 21 | 22 | uint8_t val = ADCH; 23 | // uint8_t valD = val & 0b11100000; 24 | // uint8_t valB = val & 0b00011111; 25 | // PORTD = valD; 26 | // PORTB = valB; 27 | PORTD = val; 28 | PORTB = val; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /arduino/.vscode/arduino.json: -------------------------------------------------------------------------------- 1 | { 2 | "sketch": "src/src.ino", 3 | "board": "LGT8fx Boards:avr:328", 4 | "port": "/dev/tty.usbmodem0001", 5 | "output": "build", 6 | "programmer": "AVR ISP", 7 | "configuration": "arduino_isp=disable,clock_source=internal,clock=32MHz,variant=modelP" 8 | } -------------------------------------------------------------------------------- /arduino/.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Mac", 5 | "includePath": [ 6 | "~/code/arduino/libraries/**", 7 | "~/code/lgt8fx/lgt8f/cores/**", 8 | "~/code/lgt8fx/lgt8f/libraries/**", 9 | "~/code/lgt8fx/lgt8f/variants/**", 10 | "/System/Library/Frameworks/Kernel.framework/Versions/A/Headers", 11 | "/Applications/Arduino.app/Contents/Java/hardware/tools/**", 12 | "~/Library/Arduino15/packages/LGT8fx Boards/hardware/avr/1.0.6/**", 13 | "/Applications/Arduino.app/Contents/Java/hardware/tools/avr/avr/include", 14 | "/Applications/Arduino.app/Contents/Java/hardware/arduino/avr/libraries/**", 15 | "/Users/davidbuezas/Library/Arduino15/packages/LGT8fx Boards/hardware/avr/1.0.6/**" 16 | ], 17 | "browse": { 18 | "limitSymbolsToIncludedHeaders": false, 19 | "path": [ 20 | "/System/Library/Frameworks/Kernel.framework/Versions/A/Headers", 21 | "/Applications/Arduino.app/Contents/Java/", 22 | "~/code/arduino-web-oscilloscope/**", 23 | "~/code/lgt8fx/lgt8f/" 24 | ] 25 | }, 26 | "forcedInclude": [ 27 | "/Applications/Arduino.app/Contents/Java/hardware/tools/avr/avr/include/avr/iomx8.h", 28 | "~/code/lgt8fx/lgt8f/variants/lgt8fx8p/pins_arduino.h", 29 | "~/code/lgt8fx/lgt8f/cores/lgt8f/lgtx8p.h", 30 | "~/code/lgt8fx/lgt8f/cores/lgt8f/Arduino.h", 31 | "~/code/lgt8fx/lgt8f/cores/lgt8f/lgtx8p_documentation.h" 32 | ], 33 | "macFrameworkPath": ["/System/Library/Frameworks", "/Library/Frameworks"], 34 | "intelliSenseMode": "clang-x64", 35 | "compilerPath": "/Applications/Arduino.app/Contents/Java/hardware/tools/avr/bin/avr-g++ -c -g -Os -w -std=gnu++11 -fpermissive -fno-exceptions -ffunction-sections -fdata-sections -fno-threadsafe-statics -flto -w -x c++ -E -CC -mmcu=atmega328p", 36 | "defines": [ 37 | "CLOCK_SOURCE=1", 38 | "F_CPU=32000000L", 39 | "ARDUINO=10810", 40 | "ARDUINO_AVR_LARDU_328E", 41 | "ARDUINO_ARCH_AVR" 42 | ] 43 | } 44 | ], 45 | "version": 4 46 | } 47 | -------------------------------------------------------------------------------- /arduino/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "istream": "cpp", 4 | "*.tcc": "cpp", 5 | "ostream": "cpp", 6 | "complex": "cpp", 7 | "cmath": "cpp", 8 | "__locale": "cpp", 9 | "__bit_reference": "cpp", 10 | "bitset": "cpp", 11 | "vector": "cpp", 12 | "**/ios": "c", 13 | "*.def": "cpp", 14 | "__hash_table": "cpp", 15 | "__tree": "cpp", 16 | "algorithm": "cpp", 17 | "chrono": "cpp", 18 | "deque": "cpp", 19 | "string_view": "cpp", 20 | "fstream": "cpp", 21 | "limits": "cpp", 22 | "list": "cpp", 23 | "locale": "cpp", 24 | "memory": "cpp", 25 | "string": "cpp", 26 | "rope": "cpp", 27 | "hashtable": "cpp", 28 | "random": "cpp", 29 | "__mutex_base": "cpp", 30 | "mutex": "cpp" 31 | }, 32 | "C_Cpp.exclusionPolicy": "checkFilesAndFolders", 33 | "files.exclude": { 34 | // "**/iom*": true, 35 | // "**/iom3": true, 36 | // "**/iom32": true, 37 | // "**/iom328p.h": false, 38 | 39 | "**/iom328.h": true, 40 | "**/iom328pb.h": true, 41 | "**/iom32.*": true, 42 | "**/iom320*": true, 43 | "**/iom323*": true, 44 | "**/iom324*": true, 45 | "**/iom325*": true, 46 | "**/iom329*": true, 47 | "**/iom32a*": true, 48 | "**/iom32c*": true, 49 | "**/iom32h*": true, 50 | "**/iom32m*": true, 51 | "**/iom32u*": true, 52 | "**/iom30*": true, 53 | "**/iom1*": true, 54 | "**/iom2*": true, 55 | "**/iom4*": true, 56 | "**/iom6*": true, 57 | "**/iom8*": true, 58 | "**/iomx*": true, 59 | "**/io1*": true, 60 | "**/io2*": true, 61 | "**/io4*": true, 62 | "**/io7*": true, 63 | "**/io8*": true, 64 | "**/io9*": true, 65 | "**/ioa*": true, 66 | "**/ioc*": true, 67 | "**/iot*": true, 68 | "**/iou*": true, 69 | "**/iox*": true 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /arduino/src/MemoryFree.cpp: -------------------------------------------------------------------------------- 1 | #if (ARDUINO >= 100) 2 | #include 3 | #else 4 | #include 5 | #endif 6 | 7 | extern unsigned int __heap_start; 8 | extern void *__brkval; 9 | 10 | /* 11 | * The free list structure as maintained by the 12 | * avr-libc memory allocation routines. 13 | */ 14 | struct __freelist 15 | { 16 | size_t sz; 17 | struct __freelist *nx; 18 | }; 19 | 20 | /* The head of the free list structure */ 21 | extern struct __freelist *__flp; 22 | 23 | #include "MemoryFree.h" 24 | 25 | /* Calculates the size of the free list */ 26 | int freeListSize() 27 | { 28 | struct __freelist* current; 29 | int total = 0; 30 | for (current = __flp; current; current = current->nx) 31 | { 32 | total += 2; /* Add two bytes for the memory block's header */ 33 | total += (int) current->sz; 34 | } 35 | 36 | return total; 37 | } 38 | 39 | int freeMemory() 40 | { 41 | int free_memory; 42 | if ((int)__brkval == 0) 43 | { 44 | free_memory = ((int)&free_memory) - ((int)&__heap_start); 45 | } 46 | else 47 | { 48 | free_memory = ((int)&free_memory) - ((int)__brkval); 49 | free_memory += freeListSize(); 50 | } 51 | return free_memory; 52 | } 53 | -------------------------------------------------------------------------------- /arduino/src/MemoryFree.h: -------------------------------------------------------------------------------- 1 | // MemoryFree library based on code posted here: 2 | // http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1213583720/15 3 | // Extended by Matthew Murdoch to include walking of the free list. 4 | 5 | #ifndef MEMORY_FREE_H 6 | #define MEMORY_FREE_H 7 | 8 | #ifdef __cplusplus 9 | extern "C" { 10 | #endif 11 | 12 | int freeMemory(); 13 | 14 | #ifdef __cplusplus 15 | } 16 | #endif 17 | 18 | #endif 19 | -------------------------------------------------------------------------------- /arduino/src/TODOS.txt: -------------------------------------------------------------------------------- 1 | done 2 | * add fft 3 | * add amplifier control 4 | * add gain control (1, fake 4, 8, 16, 32) 5 | * remove all about changing ref voltage measurement is super noisy 6 | * add frequency, vpp, vmin and vmax, and vavr 7 | * make each channel independant 8 | * separate controls into many files 9 | * put isOversamplingState handling inside a function 10 | * make smoothing factor a slider 11 | * Do the buffer alignment in the board 12 | * add measure ∂t and ∂v 13 | * use delta T per sample instead of adc clock ticks per sample 14 | * add slow mode by frame 1 reading at a time and accumulating in client 15 | * make axis in react without plot.d3.ts 16 | - select(node).call(axisLeft(y).ticks(10,'%'))}/> 17 | - or just really make it manually with d3.ticks 18 | * optimize plot component 19 | * fix auto mode > 2 seconds 20 | * add checksum 21 | * change dropdowns via btns 22 | * Add hotkey indications 23 | * add measure ruler 24 | * fix time precision 25 | * fix slow mode not resetting adc amplification 26 | * add x y mode 27 | * Upload directly from webpage 28 | * fix xy mode scale 29 | * cleanup warnings 30 | * get rid of "trashed samples" 31 | * dynamic adc prescaler 32 | 33 | todo 34 | * delta compression 35 | * separate analog and digital inputs in different pannels 36 | * store configs and send on start 37 | * final clean up 38 | * publish 39 | * add stm32 support 40 | * Add explanation tooltips 41 | 42 | wont do 43 | * Find another way to synchronise packets (e.g. better than end of message or prelude) -------------------------------------------------------------------------------- /arduino/src/adc.h: -------------------------------------------------------------------------------- 1 | #include "data-struct.h" 2 | 3 | #define DAPEN 7 4 | #define GA0 5 5 | #define DNS0 2 6 | #define DPS0 0 7 | #define DIFFS 1 8 | 9 | /* 10 | 0 -> 2 -> 727.28 kSamples 11 | 1 -> 2 -> 727.28 kSamples // adc can't read over 2.5v here 12 | 2 -> 4 -> 363.64 kSamples 13 | 3 -> 8 -> 181.82 kSamples 14 | 4 -> 16 -> 90.91 kSamples 15 | 5 -> 32 -> 45.455 KSamples 16 | 6 -> 64 -> 22.727 kSamples 17 | 7 -> 128 -> 11,363 kSamples 18 | */ 19 | #define DIV_1 0b0000 20 | #define DIV_1_5 0b1000 21 | #define DIV_4_5 0b1110 22 | #define GAIN1 0b00 23 | #define GAIN8 0b01 24 | #define GAIN16 0b10 25 | #define GAIN32 0b11 26 | const uint8_t gainArray[][2] = { 27 | {GAIN1, DIV_1_5}, // 25 v 28 | {GAIN1, DIV_4_5}, // 6.25 v 29 | {GAIN1, DIV_1}, // 5 v 30 | {GAIN8, DIV_1_5}, // 3.125 v 31 | {GAIN16, DIV_1_5}, // 1.5625 v 32 | {GAIN32, DIV_1_5}, // 0.78125 v 33 | {GAIN8, DIV_4_5}, // 0.78125 v 34 | {GAIN8, DIV_1}, // 0.625 v 35 | {GAIN16, DIV_4_5}, // 0.390625 v 36 | {GAIN16, DIV_1}, // 0.3125 v 37 | {GAIN32, DIV_4_5}, // 0.1953125 v 38 | {GAIN32, DIV_1}, // 0.15625 v 39 | }; 40 | enum ReferenceVoltage { 41 | // The other 1v, 2v and 4v make the ADC jump a lot 42 | AREF = 0b000, 43 | AVCC = 0b001, 44 | v2_048 = 0b010, 45 | v1_024 = 0b011, 46 | v4_096 = 0b100, 47 | }; 48 | 49 | const uint8_t refVoltage = ReferenceVoltage::AVCC; 50 | inline void startADC() { 51 | // input A0 ---> 52 | // -[ADCSRD]-> voltage division -> 53 | // -[ADMUX]-> muxer -> 54 | // -[DAPCR]-> diff amplifier -> 55 | // -[ADCSRD]-> ADC -> buffer 56 | 57 | // uint16_t ticksPerAdcRead = 58 | // ADC_PRESCALER * (15 + 1.5 + 5.5) 59 | // // A normal conversion takes 15 ADC clock cycles 60 | // // + 1.5 sample and hold + (5.5 unaccounted for) 61 | 62 | float adcTicksPerSample = state.secPerSample * F_CPU / 22; 63 | uint8_t prescaler; 64 | 65 | if (adcTicksPerSample >= 128) 66 | prescaler = 7; 67 | else if (adcTicksPerSample >= 64) 68 | prescaler = 6; 69 | else if (adcTicksPerSample >= 32) 70 | prescaler = 5; 71 | else if (adcTicksPerSample >= 16) 72 | prescaler = 4; 73 | else if (adcTicksPerSample >= 8) 74 | prescaler = 3; 75 | else if (adcTicksPerSample >= 4) 76 | prescaler = 2; 77 | else if (adcTicksPerSample >= 2) 78 | prescaler = 1; 79 | else 80 | prescaler = 0; 81 | 82 | uint8_t gain = gainArray[state.amplifier][0]; 83 | uint8_t divisor = gainArray[state.amplifier][1]; 84 | 85 | ADMUX = bitRead(refVoltage, 0) 86 | << REFS0 | // ADC refference is AVCC [seel also: ADCSRD:REFS2] 87 | bitRead(refVoltage, 1) << REFS1 | // ADC refference is AVCC [seel 88 | // also: ADCSRD:REFS2] 89 | 1 << ADLAR | // ADC data register is left adjustment 90 | divisor << MUX0; 91 | 92 | ADCSRA = 1 << ADEN | // enable ADC 93 | 1 << ADSC | // start conversion 94 | 1 << ADATE | // ADC auto triggering enable 95 | 0 << ADIE | // disable ADC interrupt 96 | prescaler << ADPS0; 97 | ADCSRB = 0 << ADTS0; // Continuous conversion 98 | 99 | ADCSRC = 1 << DIFFS | // 1 = from diff amplifier, 0=multiplexer 100 | 0 << SPN | // ADC conversion input polarity control 101 | 0 << SPD; // 1: high speed conversion (can't hear a difference) 102 | ADCSRD = bitRead(refVoltage, 2) 103 | << REFS2 | // part of ADC reference voltage [see ADMUX:REFS0] 104 | 0b00 << IVSEL0 | // 2v DAC output 105 | 0b001 << VDS0; // ADC0 voltage division 106 | DAPCR = 0b1 << DAPEN | // Enable 107 | gain << GA0 | // gain 108 | 0b110 << DNS0 | // (-) GND 109 | 0b00 << DPS0; // (+) MUX 110 | } 111 | 112 | inline void stopADC() { ADCSRA = 0; } 113 | 114 | void setupADC() { 115 | bitSet(DIDR0, PC0D); // disable digital input (reduce noise) A0 116 | // bitSet(DIDR0, PC1D); // disable digital input (reduce noise) A1 117 | // bitSet(DIDR0, PC2D); // disable digital input (reduce noise) A2 118 | // bitSet(DIDR0, PC3D); // disable digital input (reduce noise) A3 119 | 120 | // PC2::5 // 4 digital channels 121 | DDRC = 0b00000000; 122 | PORTC = 0b00000000; 123 | 124 | // PD5::7 // part 1 of external ADC 125 | DDRD = 0b00000000; 126 | PORTD = 0b00000000; 127 | 128 | // PB0::4 // part 2 of external ADC 129 | DDRB = 0b00100000; // d13 as output 130 | PORTB = 0b00000000; // disable all pullups 131 | } 132 | -------------------------------------------------------------------------------- /arduino/src/dac.h: -------------------------------------------------------------------------------- 1 | #include "data-struct.h" 2 | 3 | ISR(TIMER2_COMPA_vect) { 4 | DALR = (DALR + 1); 5 | // if (DALR == 230) DALR = 0; 6 | } 7 | const float vArray[] = { 8 | 25, 6.25, 5, 3.125, 1.5625, 0.78125, 9 | 0.78125, 0.625, 0.390625, 0.3125, 0.1953125, 0.15625, 10 | }; 11 | void setDAC() { 12 | return; 13 | float r = (255.0 / 5.0 * vArray[state.amplifier] * 1 / 2); 14 | r = constrain(r, 0, 255); 15 | DALR = (byte)r; 16 | } 17 | 18 | void setupDAC() { 19 | return; 20 | DACON = (1 << DACEN | // enable dac 21 | 1 << DAOE | // enable output to D4 22 | 0b00 << DAVS0); // 00: voltage source is system working power VCC 23 | // 01: voltage source is external input AVREF 24 | // 10: voltage source is internal reference voltage 25 | // 11: shut down DAC reference source and 26 | // DAC at the sametime 27 | // return; 28 | TCCR2A = 0; 29 | TCCR2B = 0; 30 | TCNT2 = 0; // counter = 0 31 | OCR2A = 255; // TC2 output compare register A 32 | TCCR2A |= (1 << WGM21); 33 | /* CS20; 34 | 0: stop 35 | 1: 1 36 | 2: 8 37 | 3: 32 38 | 4: 64 39 | 5: 128 40 | 6: 256 41 | 7: 1024 42 | */ 43 | TCCR2B |= 1 << CS20; 44 | bitSet(TIMSK2, OCIE2A); 45 | } 46 | -------------------------------------------------------------------------------- /arduino/src/data-struct.h: -------------------------------------------------------------------------------- 1 | #ifndef DATA_STRUCT_H 2 | #define DATA_STRUCT_H 3 | 4 | #define MAX_SAMPLES 512 5 | 6 | enum TriggerMode { autom = 0, normal = 1, single = 2, slow = 3 }; 7 | enum TriggerDir { rising = 0, falling = 1 }; 8 | 9 | typedef struct { 10 | bool inputChanged; 11 | uint16_t bufferStartPtr; 12 | } InternalState; 13 | InternalState internalState = { 14 | false, // bool inputChanged; 15 | 0 // uint16_t bufferStartPtr; 16 | }; 17 | 18 | uint8_t endOfMessage[4] = {0, 1, 255, 253}; 19 | typedef struct { 20 | // input 21 | uint8_t triggerVoltage; 22 | uint8_t triggerDir; 23 | float secPerSample; 24 | uint16_t triggerPos; 25 | uint8_t amplifier; 26 | uint8_t triggerMode; 27 | uint8_t triggerChannel; 28 | uint8_t isChannelOn; 29 | // input & output 30 | // output 31 | bool needData; 32 | bool forceUIUpdate; 33 | bool didTrigger; 34 | uint16_t freeMemory; 35 | uint16_t sentSamples; 36 | uint16_t samplesPerBuffer; 37 | } State; 38 | 39 | State state = { 40 | // input 41 | 128, // uint8_t triggerVoltage; 42 | TriggerDir::falling, // uint8_t triggerDir; 43 | 0.00000275, // float secPerSample; 44 | MAX_SAMPLES * 0 / 3, // uint16_t triggerPos; 45 | 2, // uint8_t amplifier; 46 | TriggerMode::autom, // uint8_t triggerMode 47 | 0, // uint8_t triggerChannel 48 | 0b00000000, // bool isChannelOn; 49 | // input & output 50 | // output 51 | 1, // bool needData; 52 | 0, // bool forceUIUpdate; 53 | false, // bool didTrigger; 54 | 100, // uint16_t freeMemory; 55 | 0, // uint16_t sentSamples; 56 | MAX_SAMPLES // uint16_t samplesPerBuffer; 57 | }; 58 | 59 | uint8_t buffer0[MAX_SAMPLES]; 60 | uint8_t buffer1[MAX_SAMPLES]; 61 | uint8_t buffer2[MAX_SAMPLES]; 62 | 63 | #endif -------------------------------------------------------------------------------- /arduino/src/fillBuffer.h: -------------------------------------------------------------------------------- 1 | #define TCNT3 _SFR_MEM16(0x94) 2 | #define ICR3 _SFR_MEM16(0x96) 3 | #include "data-struct.h" 4 | 5 | void startCPUCounter() { 6 | TCCR3A = 0; 7 | TCCR3B = (1 << WGM33) | (1 << WGM32); // CTC mode, counts to ICR3 8 | uint32_t ticksPerSample = state.secPerSample * F_CPU - 1; 9 | if (ticksPerSample < UINT16_MAX) { 10 | ICR3 = ticksPerSample; 11 | TCCR3B |= (1 << CS30); 12 | } else if (ticksPerSample / 8 < UINT16_MAX) { 13 | ICR3 = ticksPerSample / 8; 14 | TCCR3B |= (2 << CS30); 15 | } else if (ticksPerSample / 64 < UINT16_MAX) { 16 | ICR3 = ticksPerSample / 64; 17 | TCCR3B |= (3 << CS30); 18 | } else if (ticksPerSample / 256 < UINT16_MAX) { 19 | ICR3 = ticksPerSample / 256; 20 | TCCR3B |= (4 << CS30); 21 | } else if (ticksPerSample / 1024 < UINT16_MAX) { 22 | ICR3 = ticksPerSample / 1024; 23 | TCCR3B |= (5 << CS30); 24 | } 25 | TCNT3 = 0; 26 | TIFR3 = 255; // setBit(TIFR3, OCF3A); // clear overflow bit 27 | } 28 | #define FORCE_INLINE __attribute__((always_inline)) inline 29 | 30 | FORCE_INLINE uint8_t storeOne(uint8_t returnChannel) { 31 | loop_until_bit_is_set(TIFR3, OCF3A); 32 | TIFR3 = 255; // setBit(TIFR3, OCF3A); // clear overflow bit 33 | uint8_t val0 = ADCH; 34 | 35 | // equivalent to (PINB & 0b00011111) | (PIND & 0b11100000) because the ignored 36 | // pins are configured as output low. 37 | // uint8_t val1 = PINB | PIND; 38 | uint8_t val1 = (PINB & 0b00011111) | (PIND & 0b11100000); 39 | 40 | // uint8_t val2 = PINC & 0b00111100; 41 | uint8_t val2 = PINC; 42 | buffer0[internalState.bufferStartPtr] = val0; 43 | buffer1[internalState.bufferStartPtr] = val1; 44 | buffer2[internalState.bufferStartPtr] = val2; 45 | internalState.bufferStartPtr = 46 | (internalState.bufferStartPtr + 1) & 0b111111111; 47 | if (returnChannel == 0) return val0; 48 | if (returnChannel == 1) return val1; 49 | return bitRead(val2, returnChannel); 50 | } 51 | void fillBufferAnalogTrigger(uint8_t channel, TriggerDir dir) { 52 | uint8_t triggerPoint = state.triggerVoltage; 53 | uint8_t triggerVoltageMinus = max(0, (int)triggerPoint - 2); 54 | uint8_t triggerVoltagePlus = min(255, (int)triggerPoint + 2); 55 | uint16_t headSamples = state.triggerPos; 56 | uint16_t tailSamples = state.samplesPerBuffer - state.triggerPos - 1; 57 | startADC(); 58 | startCPUCounter(); 59 | while (headSamples--) { 60 | storeOne(channel); 61 | } 62 | if (dir == TriggerDir::rising) { 63 | while (storeOne(channel) > triggerVoltageMinus) { 64 | } 65 | while (storeOne(channel) < triggerPoint) { 66 | } 67 | } else { 68 | while (storeOne(channel) < triggerVoltagePlus) { 69 | } 70 | while (storeOne(channel) > triggerPoint) { 71 | } 72 | } 73 | while (tailSamples--) { 74 | storeOne(channel); 75 | } 76 | stopADC(); 77 | } 78 | 79 | void fillBufferDigitalTrigger(uint8_t channel, TriggerDir dir) { 80 | uint16_t headSamples = state.triggerPos; 81 | uint16_t tailSamples = state.samplesPerBuffer - state.triggerPos; 82 | startADC(); 83 | startCPUCounter(); 84 | while (headSamples--) { 85 | storeOne(channel); 86 | } 87 | if (dir == TriggerDir::rising) { 88 | while (storeOne(channel) == 1) { 89 | } 90 | while (storeOne(channel) == 0) { 91 | } 92 | } else { 93 | while (storeOne(channel) == 0) { 94 | } 95 | while (storeOne(channel) == 1) { 96 | } 97 | } 98 | 99 | while (tailSamples--) { 100 | storeOne(channel); 101 | } 102 | stopADC(); 103 | } 104 | 105 | uint16_t autoInterruptOverflows; 106 | void setupAutoInterrupt() { 107 | TIMSK1 = 0; 108 | TCCR1A = 0; 109 | TCCR1B = 5 << CS10; // 1024 prescaler 110 | TCCR1B |= (1 << WGM12); // turn on CTC mode 111 | int prescaler = 1024; 112 | 113 | float secondsPerFrame = (state.secPerSample * state.samplesPerBuffer) + 114 | 10 / 1000.0; // added 10ms of wait time 115 | float ticksPerFrame = secondsPerFrame * F_CPU / prescaler; 116 | 117 | uint32_t timeoutTicks = ceil(ticksPerFrame * 2.5); 118 | 119 | autoInterruptOverflows = timeoutTicks / 65536; 120 | uint16_t timeoutTicksCycle = timeoutTicks % 65536; 121 | OCR1A = timeoutTicksCycle; // will interrupt when this value is reached 122 | TCNT1 = 0; // counter reset 123 | 124 | TIMSK1 |= (1 << OCIE1A); // enable ISR 125 | } 126 | 127 | void offAutoInterrupt() { 128 | // TIMSK1 = 0; 129 | // TCCR1A = 0; 130 | TCCR1B = 0; 131 | } 132 | 133 | void fillBufferSlow() { 134 | if (internalState.inputChanged) { 135 | internalState.inputChanged = false; 136 | startADC(); 137 | startCPUCounter(); 138 | const int FPS = 30; // try to achive 25 frames per second 139 | float samplesPerSecond = 1 / state.secPerSample; 140 | state.sentSamples = samplesPerSecond / FPS; 141 | if (state.sentSamples < 1) state.sentSamples = 1; 142 | if (state.sentSamples > state.samplesPerBuffer - 1) 143 | state.sentSamples = state.samplesPerBuffer - 1; 144 | } 145 | uint16_t bufferStart = internalState.bufferStartPtr; 146 | for (uint16_t i = state.sentSamples; i > 0; i--) storeOne(0); 147 | internalState.bufferStartPtr = bufferStart; 148 | } 149 | 150 | jmp_buf envAutoTimeout; 151 | void fillBuffer() { 152 | switch (state.triggerMode) { 153 | case TriggerMode::slow: 154 | fillBufferSlow(); 155 | break; 156 | case TriggerMode::autom: { 157 | bool didTimeout = setjmp(envAutoTimeout); 158 | setupAutoInterrupt(); 159 | if (didTimeout) { 160 | offAutoInterrupt(); 161 | state.didTrigger = false; 162 | return; 163 | } 164 | [[fallthrough]]; 165 | } 166 | case TriggerMode::single: 167 | case TriggerMode::normal: 168 | state.sentSamples = state.samplesPerBuffer; 169 | 170 | if (state.triggerChannel < 2) 171 | fillBufferAnalogTrigger(state.triggerChannel, 172 | (TriggerDir)state.triggerDir); 173 | else 174 | fillBufferDigitalTrigger(state.triggerChannel, 175 | (TriggerDir)state.triggerDir); 176 | offAutoInterrupt(); 177 | state.didTrigger = true; 178 | } 179 | } 180 | 181 | ISR(TIMER1_COMPA_vect) { 182 | if (autoInterruptOverflows == 0) longjmp(envAutoTimeout, 1); 183 | autoInterruptOverflows--; 184 | } 185 | -------------------------------------------------------------------------------- /arduino/src/input.h: -------------------------------------------------------------------------------- 1 | #include "MemoryFree.h" 2 | #include "data-struct.h" 3 | #include "output.h" 4 | 5 | void saveInput(char option, float val) { 6 | state.needData = false; 7 | switch (option) { 8 | case 'R': 9 | // request data 10 | sendData(); 11 | break; 12 | case 'C': 13 | state.secPerSample = val; 14 | break; 15 | case 'V': 16 | state.triggerVoltage = constrain(val, 0, 255); 17 | break; 18 | case 'A': 19 | state.amplifier = constrain(val, 0, 11); 20 | break; 21 | case 'P': 22 | state.triggerPos = constrain(val, 0, state.samplesPerBuffer - 1); 23 | break; 24 | case 'S': 25 | state.samplesPerBuffer = constrain(val, 1, MAX_SAMPLES); 26 | state.triggerPos = constrain(state.triggerPos, 1, state.samplesPerBuffer); 27 | break; 28 | case 'D': 29 | state.triggerDir = constrain(val, 0, 1); 30 | break; 31 | case 'B': 32 | state.isChannelOn = val; 33 | break; 34 | case 'M': 35 | state.triggerMode = constrain(val, 0, 3); 36 | // AUTO, NORMAL, SINGLE, SLOW 37 | break; 38 | case 'T': 39 | state.triggerChannel = constrain(val, 0, 5); 40 | break; 41 | } 42 | } 43 | 44 | /* 45 | It is a pitty that I need to have my own buffer on top of the circular buffer 46 | that HardwareSerial has. This is because there is no method to read that 47 | buffer without consuming it. 48 | */ 49 | #define INPUT_BUFFER_SIZE 35 50 | char inputBuffer[INPUT_BUFFER_SIZE]; 51 | uint8_t ptr = 0; 52 | void handleInput() { 53 | while (Serial.available()) { 54 | int s = Serial.read(); 55 | if (s == '>') { 56 | char option = inputBuffer[0]; 57 | float val = atof(inputBuffer + 1); 58 | state.freeMemory = freeMemory(); 59 | ptr = 0; 60 | inputBuffer[ptr] = 0; 61 | 62 | saveInput(option, val); 63 | internalState.inputChanged = true; 64 | } else { 65 | if (ptr >= INPUT_BUFFER_SIZE - 1) { 66 | // don't write outside the array 67 | // this is actually an exception. 68 | ptr = 0; 69 | } 70 | inputBuffer[ptr] = (char)s; 71 | ptr++; 72 | inputBuffer[ptr] = 0; 73 | // delayMicroseconds(100); 74 | // give time to receive the rest of the message before filling the buffer 75 | // with trash. Not using normal delay because it uses micros() and that 76 | // timer is off 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /arduino/src/output.h: -------------------------------------------------------------------------------- 1 | #ifndef OUTPUT_H 2 | #define OUTPUT_H 3 | 4 | #include "data-struct.h" 5 | 6 | uint16_t checksum; 7 | void write(uint8_t c) { 8 | checksum += c; 9 | Serial.write(c); 10 | } 11 | size_t write(const uint8_t* c, size_t length) { 12 | Serial.write(c, length); 13 | for (uint16_t i = 0; i < length; i++) { 14 | checksum += c[i]; 15 | } 16 | return length; 17 | } 18 | void sendBuffer(uint8_t buffer[]) { 19 | uint16_t ptr = internalState.bufferStartPtr; 20 | for (uint16_t i = 0; i < state.sentSamples; i++) { 21 | write(buffer[ptr]); 22 | ptr++; 23 | if (ptr == state.samplesPerBuffer) ptr = 0; 24 | } 25 | } 26 | 27 | void sendData(bool withBuffers = true) { 28 | checksum = 0; 29 | digitalWrite(D13, 1); 30 | write((uint8_t*)&state, sizeof(state)); 31 | if (withBuffers) { 32 | if (state.isChannelOn & 0b1) sendBuffer(buffer0); 33 | if (state.isChannelOn & 0b10) sendBuffer(buffer1); 34 | if (state.isChannelOn & 0b00111100) sendBuffer(buffer2); 35 | } 36 | Serial.write((uint8_t*)&checksum, sizeof(checksum)); 37 | Serial.write((uint8_t*)&endOfMessage, sizeof(endOfMessage)); 38 | state.forceUIUpdate = false; 39 | Serial.flush(); // send all now to avoid interrupts while sampling 40 | digitalWrite(D13, 0); 41 | } 42 | 43 | #endif -------------------------------------------------------------------------------- /arduino/src/src.ino: -------------------------------------------------------------------------------- 1 | #include "MemoryFree.h" 2 | #pragma push_macro("USART_RX_vect") 3 | #undef USART_RX_vect 4 | #define USART_RX_vect _VECTOR(0) // unused vector 0 5 | #include 6 | #pragma pop_macro("USART_RX_vect") 7 | 8 | #include 9 | 10 | #include "adc.h" 11 | #include "dac.h" 12 | #include "data-struct.h" 13 | #include "fillBuffer.h" 14 | #include "input.h" 15 | #include "output.h" 16 | void setup() { 17 | Serial.begin(115200 * 1); 18 | // disable all timer interrupts (millis() gone) 19 | bitWrite(TIMSK0, TOIE0, 0); 20 | bitWrite(TIMSK1, TOIE1, 0); 21 | bitWrite(TIMSK2, TOIE2, 0); 22 | bitWrite(TIMSK3, TOIE3, 0); 23 | 24 | setupADC(); 25 | setupDAC(); 26 | } 27 | 28 | jmp_buf env; 29 | volatile bool canStop; 30 | volatile bool isInputAvailable; 31 | 32 | void loop() { 33 | state.freeMemory = freeMemory(); 34 | sendData(false); 35 | sendData(false); 36 | bool isJump = setjmp(env); 37 | if (isJump) offAutoInterrupt(); 38 | for (;;) { 39 | setDAC(); 40 | if (isInputAvailable) { 41 | isInputAvailable = false; 42 | handleInput(); 43 | } 44 | canStop = true; 45 | 46 | fillBuffer(); 47 | canStop = false; 48 | sendData(); 49 | } 50 | } 51 | 52 | ISR(USART_RX_vect) { 53 | Serial._rx_complete_irq(); 54 | isInputAvailable = true; 55 | if (canStop) { 56 | canStop = false; 57 | longjmp(env, 1); 58 | } 59 | } 60 | 61 | /* 62 | /Applications/Arduino.app/Contents/Java/hardware/tools/avr/avr/bin/objdump -S \ 63 | src.ino.elf > assembler.asm 64 | */ -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arduino-web-oscilloscope", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "/arduino-web-oscilloscope", 6 | "dependencies": { 7 | "@react-hook/copy": "^2.0.1", 8 | "@react-hook/size": "^2.1.0", 9 | "@types/async": "^3.2.3", 10 | "@types/mousetrap": "^1.6.3", 11 | "async": "^3.2.0", 12 | "d3": "^5.16.0", 13 | "fourier-transform": "^1.1.2", 14 | "intel-hex": "^0.1.2", 15 | "lodash": "^4.17.20", 16 | "mousetrap": "^1.6.5", 17 | "node-sass": "^4.14.1", 18 | "react": "^16.13.1", 19 | "react-dom": "^16.13.1", 20 | "react-scripts": "3.4.3", 21 | "readable-web-to-node-stream": "^2.0.0", 22 | "recoil": "^0.0.13", 23 | "rsuite": "^4.8.0", 24 | "stk500": "2.0.2", 25 | "typescript": "3.9.4" 26 | }, 27 | "scripts": { 28 | "start": "react-scripts start", 29 | "build": "react-scripts build", 30 | "test": "react-scripts test", 31 | "eject": "react-scripts eject" 32 | }, 33 | "eslintConfig": { 34 | "extends": "react-app" 35 | }, 36 | "browserslist": { 37 | "production": [ 38 | ">0.2%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 1 chrome version", 44 | "last 1 firefox version", 45 | "last 1 safari version" 46 | ] 47 | }, 48 | "devDependencies": { 49 | "@testing-library/jest-dom": "^4.2.4", 50 | "@testing-library/react": "^9.3.2", 51 | "@testing-library/user-event": "^7.1.2", 52 | "@types/d3": "^5.7.2", 53 | "@types/jest": "^24.0.0", 54 | "@types/lodash": "^4.14.158", 55 | "@types/node": "^12.0.0", 56 | "@types/react": "^16.9.0", 57 | "@types/react-dom": "^16.9.0", 58 | "@typescript-eslint/eslint-plugin": "^3.7.1", 59 | "@typescript-eslint/parser": "^3.7.1", 60 | "eslint-config-prettier": "^6.11.0", 61 | "eslint-plugin-prettier": "^3.1.4", 62 | "eslint-plugin-react": "^7.20.5", 63 | "eslint-plugin-react-hooks": "^4.0.8", 64 | "prettier": "^2.0.5" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /public/ExperimentalWebPlatformFeatures.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbuezas/arduino-web-oscilloscope/53f019f19251c66d9744248bfad8208997fc179a/public/ExperimentalWebPlatformFeatures.png -------------------------------------------------------------------------------- /public/address-bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbuezas/arduino-web-oscilloscope/53f019f19251c66d9744248bfad8208997fc179a/public/address-bar.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbuezas/arduino-web-oscilloscope/53f019f19251c66d9744248bfad8208997fc179a/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 16 | 17 | 18 | 22 | 23 | 32 | Arduino Web Oscilloscope 33 | 34 | 35 | 39 | 48 | 49 | 50 | 51 |
52 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbuezas/arduino-web-oscilloscope/53f019f19251c66d9744248bfad8208997fc179a/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbuezas/arduino-web-oscilloscope/53f019f19251c66d9744248bfad8208997fc179a/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbuezas/arduino-web-oscilloscope/53f019f19251c66d9744248bfad8208997fc179a/public/screenshot.png -------------------------------------------------------------------------------- /public/src.ino.hex: -------------------------------------------------------------------------------- 1 | :100000000C9488000C94B0000C94B0000C94B000D8 2 | :100010000C94B0000C94B0000C94B0000C947A09CD 3 | :100020000C94B0000C94B0000C94B0000C944709F0 4 | :100030000C94B0000C94B0000C94B0000C94B00080 5 | :100040000C94B2080C94B0000C94F8080C948F092E 6 | :100050000C94B0000C94B0000C94B0000C94B00060 7 | :100060000C94B0000C94B0000C9400000C940000B0 8 | :100070000C9400000C9400004E414E494E495459D6 9 | :10008000494E46CDCCCC3D0AD7233C17B7D1387763 10 | :10009000CC2B329595E6241FB14F0A000020410079 11 | :1000A00000C84200401C4620BCBE4CCA1B0E5AAEC3 12 | :1000B000C59D7400000000250028002B002E0004C0 13 | :1000C00004040404040404020202020202030303FF 14 | :1000D00003030305050505050505030102040810D2 15 | :1000E00020408001020408102001020408102002B0 16 | :1000F00008010410204040000000085002010000E8 17 | :100100000304070000000000000000000000850A52 18 | :1001100011241FBECFEFD8E0DEBFCDBF11E0A0E0BD 19 | :10012000B1E0E6EAFEE102C005900D92A434B10709 20 | :10013000D9F728E0A4E4B1E001C01D92AB34B207C6 21 | :10014000E1F710E0C8E8D0E004C02197FE010E946A 22 | :100150004B0FC738D107C9F70E94E7090C94510F1C 23 | :100160000C940000E4E0F1E02491E8EEF0E034913A 24 | :10017000ECECF0E09491992361F12223C9F0215035 25 | :100180002B30B0F4E22FF0E0E853FF4F0C944B0F0C 26 | :10019000EE00F200D300EA00D800D800F500FB0022 27 | :1001A000FF0005010901209180002F772093800036 28 | :1001B000E92FF0E0EE0FFF1FED54FF4FA591B49132 29 | :1001C0009FB7F894EC91811128C030953E233C9361 30 | :1001D0009FBF0895209180002F7DE8CF24B52F7711 31 | :1001E00024BDE6CF24B52F7DFBCF2091B0002F7723 32 | :1001F0002093B000DDCF2091B0002F7DF9CF20916A 33 | :1002000090002F7720939000D3CF209190002F7DE6 34 | :10021000F9CF20919000277FF5CF3E2BD8CFAF921A 35 | :10022000BF92CF92DF92EF92FF920F931F93CF93E3 36 | :10023000DF936C017B018B01040F151FEB015E0145 37 | :10024000AE18BF08C017D10759F06991D601ED91DA 38 | :10025000FC910190F081E02DC6010995892B79F779 39 | :10026000C501DF91CF911F910F91FF90EF90DF902B 40 | :10027000CF90BF90AF900895FC01538D448D252FF2 41 | :1002800030E0842F90E0821B930B541710F0CF9630 42 | :10029000089501970895FC01918D828D981761F062 43 | :1002A000A28DAE0FBF2FB11D5D968C91928D9F5F79 44 | :1002B0009F73928F90E008958FEF9FEF0895FC0158 45 | :1002C000918D828D981731F0828DE80FF11D858D0B 46 | :1002D00090E008958FEF9FEF0895FC01918D228D9E 47 | :1002E000892F90E0805C9F4F821B91098F73992723 48 | :1002F00008958FE997E00E946D0121E0892B09F4B0 49 | :1003000020E0822F089580E090E0892B29F00E9460 50 | :10031000790181110C9400000895FC01A48DA80FAF 51 | :10032000B92FB11DA35ABF4F2C91848D90E0019637 52 | :100330008F739927848FA689B7892C93A089B18957 53 | :100340008C9180648C93938D848D981306C0028861 54 | :10035000F389E02D80818F7D80830895EF92FF9255 55 | :100360000F931F93CF93DF93EC0181E0888F9B8DD8 56 | :100370008C8D981316C0E889F989808185FF11C09A 57 | :10038000EE89FF896083E889F989808180648083B0 58 | :1003900081E090E0DF91CF911F910F91FF90EF905E 59 | :1003A0000895F62E0B8D10E00F5F1F4F0F7311276E 60 | :1003B000E02E8C8D8E110CC00FB607FCFACFE889A9 61 | :1003C000F989808185FFF5CFCE010E948D01F1CFA3 62 | :1003D000EB8DEC0FFD2FF11DE35AFF4FF0820B8FD9 63 | :1003E000EA89FB8980818062D2CFCF93DF93EC01D1 64 | :1003F000888D8823B9F0AA89BB89E889F9898C910D 65 | :1004000085FD03C0808186FD0DC00FB607FCF7CFC8 66 | :100410008C9185FFF2CF808185FFEDCFCE010E94C8 67 | :100420008D01E9CFDF91CF910895EF92FF920F9365 68 | :100430001F93CF93DF937C01C0916007D091610738 69 | :1004400010E000E08091150190911601081719073E 70 | :10045000F8F4F701EC0FFD1F608180915D0790912A 71 | :100460005E07860F911D90935E0780935D078FE96D 72 | :1004700097E00E94AE01219680911701909118019A 73 | :100480008C179D0711F4D0E0C0E00F5F1F4FDACF4B 74 | :10049000DF91CF911F910F91FF90EF900895CF932F 75 | :1004A000C82F10925E0710925D0781E00E94B20093 76 | :1004B00045E150E064E071E08FE997E00E940F01B0 77 | :1004C00080915D0790915E07E4E0F1E029E131E081 78 | :1004D0004191840F911D2E173F07D1F790935E072E 79 | :1004E00080935D07CC23C1F080910F0180FF04C091 80 | :1004F0008DE595E00E94150280910F0181FF04C0F7 81 | :100500008DE593E00E94150280910F018C7321F01C 82 | :100510008DE591E00E94150242E050E06DE577E044 83 | :100520008FE997E00E940F0144E050E060E071E045 84 | :100530008FE997E00E940F01109211018FE997E077 85 | :100540000E94F50180E0CF910C94B20010929000CF 86 | :1005500088E18093910020E034E244EF5BE4609115 87 | :1005600006017091070180910801909109010E9494 88 | :10057000CE0C20E030E040E85FE30E94E00D0E94F6 89 | :10058000D60E6F3F2FEF72078105910580F47093AF 90 | :100590009700609396008091910081608093910014 91 | :1005A00010929500109294008FEF88BB0895683FD9 92 | :1005B0002FEF720727E08207910578F453E09695B4 93 | :1005C0008795779567955A95D1F7709397006093C3 94 | :1005D0009600809191008260E1CF603C2FEF72071E 95 | :1005E0002FE38207910578F446E0969587957795F5 96 | :1005F00067954A95D1F77093970060939600809124 97 | :1006000091008360CBCF61152FEF720782079105B0 98 | :1006100060F4672F782F892F992770939700609344 99 | :100620009600809191008460B9CF61152CEF72071C 100 | :100630002FEF820723E0920708F0B2CF2AE09695C9 101 | :100640008795779567952A95D1F770939700609372 102 | :100650009600809191008560A1CFCF92DF92EF92BA 103 | :10066000FF92CF9320E034E244EF5BE46091060117 104 | :100670007091070180910801909109010E94CE0CB0 105 | :1006800020E030E040EB51E40E94640E6B017C01FD 106 | :1006900020E030E040E053E40E94460FC7E087FFCF 107 | :1006A00042C020E030E040E852E4C701B6010E94B9 108 | :1006B000460FC6E087FF37C020E030E040E052E45C 109 | :1006C000C701B6010E94460FC5E087FF2CC020E09D 110 | :1006D00030E040E851E4C701B6010E94460FC4E093 111 | :1006E00087FF21C020E030E040E051E4C701B601BF 112 | :1006F0000E94460FC3E087FF16C020E030E040E8CC 113 | :1007000050E4C701B6010E94460FC2E087FF0BC04C 114 | :10071000C1E020E030E040E050E4C701B6010E94B3 115 | :10072000460F87FDC0E0E0910C01F0E0EE0FFF1FE7 116 | :10073000E75EFE4F80819181906690937C00C06E51 117 | :10074000C0937A0010927B0092E090937D0091E03C 118 | :100750009093AD0020E2829FC001112488698093AC 119 | :10076000DC00CF91FF90EF90DF90CF900895CF9372 120 | :10077000DF9300D0CDB7DEB7209145083091460811 121 | :10078000CE0101962115310559F48B5498409A8376 122 | :10079000898389819A810F900F90DF91CF9108957D 123 | :1007A000821B930BE0914708F091480830E020E06D 124 | :1007B000309751F02E5F3F4F40815181240F351FFC 125 | :1007C0000280F381E02DF4CF820F931FE0CF6F9270 126 | :1007D0007F928F929F92AF92BF92CF92DF92EF92D1 127 | :1007E000FF920F931F93CF93DF93CDB7DEB704E053 128 | :1007F00011E0D80119968C91833008F056C08130F1 129 | :1008000008F4D4C0E4E0F1E003891489128B018B71 130 | :10081000D284C180E680F78061E06D1508F4F6C1EE 131 | :10082000B0808B2D72E0B71608F482E03EEF932E75 132 | :10083000980E8B2D9DEF9B1508F48DEF92E0A92E5D 133 | :10084000A80E015011090E191F090E942D030E94C4 134 | :10085000A6025FEF29E7622E712CEFE5F7E0A1E039 135 | :10086000EA1AF10808F4FEC0C19BFECF58BBD301C1 136 | :100870008C9043B169B176B1818192819C01235AF8 137 | :100880003A4FD9018C929C01235A3C4F4F71607EA4 138 | :10089000462BD9014C939C01235A3E4FD9017C939E 139 | :1008A0000196917092838183DACF833009F06FC013 140 | :1008B000EFE5F7E08081882389F110820E942D0303 141 | :1008C0000E94A602F801228133814481558160E0B3 142 | :1008D00070E080E89FE30E94640E20E030E040EF8B 143 | :1008E00051E40E94640E0E94D60ED80152967C9369 144 | :1008F0006E935197672B31F481E090E052969C9370 145 | :100900008E935197E4E0F1E08389948901972189DE 146 | :1009100032898217930710F4928B818B6091600764 147 | :10092000709161074091150150911601DD24DA9410 148 | :10093000E9E7AE2EB12CEFE5F7E04115510519F1CD 149 | :10094000C19BFECFD8BAD501EC9023B139B1F6B036 150 | :10095000818192818C01035A1A4FD801EC928C014B 151 | :10096000035A1C4F2F71307E232BD8012C939C01EE 152 | :10097000235A3E4FD901FC92019691709283818354 153 | :1009800041505109DACF7093610760936007DF919E 154 | :10099000CF911F910F91FF90EF90DF90CF90BF907C 155 | :1009A000AF909F908F907F906F90089584E491E036 156 | :1009B0000E94B60A5C014FE6C42ED12CF6011082CB 157 | :1009C0001092800001E810E085E0D8018C938C91B2 158 | :1009D00088608C9354E0E52E51E0F52EF701638991 159 | :1009E000748990E080E00E946D0CD70112962D91E1 160 | :1009F0003D914D915C9115970E94CE0C2AE037ED08 161 | :100A000043E25CE30E94E10D20E034E244EF5BE46A 162 | :100A10000E94CE0C20E030E040E85AE30E94CE0C69 163 | :100A200020E030E040E250E40E94CE0C0E944D0EE7 164 | :100A30000E94D60E90935C0180935B0170938900B5 165 | :100A4000609388001092850010928400F6018081E6 166 | :100A500082608083AB2809F4D5CED8011C92F701BF 167 | :100A6000168695CF4FEFB9E7EB2EF12CEFE5F7E0C7 168 | :100A7000CC2009F450C0C19BFECF48BBD7013C91AC 169 | :100A800023B189B12F71807E282B56B1818192814B 170 | :100A9000BC01635A7A4FDB013C93BC01635A7C4F23 171 | :100AA000DB012C93BC01635A7E4FDB015C93019602 172 | :100AB000917092838183D110322F3A15E0F24FEF7B 173 | :100AC000E9E7EE2EF12CEFE5F7E0C19BFECF48BB46 174 | :100AD000D7013C9123B189B12F71807E282B56B16B 175 | :100AE00081819281BC01635A7A4FDB013C93BC0146 176 | :100AF000635A7C4FDB012C93BC01635A7E4FDB01B0 177 | :100B00005C930196917092838183D11001C0232F51 178 | :100B1000B216D8F24FC0C19BFECF48BBD7013C9163 179 | :100B200023B189B12F71807E282B56B181819281AA 180 | :100B3000BC01635A7A4FDB013C93BC01635A7C4F82 181 | :100B4000DB012C93BC01635A7E4FDB015C93019661 182 | :100B5000917092838183D110322F9316E0F24FEF80 183 | :100B6000A9E7EA2EF12CEFE5F7E0C19BFECF48BBE9 184 | :100B7000D7013C9123B189B12F71807E282B56B1CA 185 | :100B800081819281BC01635A7A4FDB013C93BC01A5 186 | :100B9000635A7C4FDB012C93BC01635A7E4FDB010F 187 | :100BA0005C930196917092838183D11001C0232FB1 188 | :100BB0002B15D8F2FF24FA94F9E7CF2ED12CEFE5CC 189 | :100BC000F7E00150110908F43DC1C19BFECFF8BA0E 190 | :100BD000D6015C9123B139B146B181819281BC01CA 191 | :100BE000635A7A4FDB015C93BC01635A7C4F2F71CF 192 | :100BF000307E232BDB012C939C01235A3E4FD901DD 193 | :100C00004C930196917092838183DBCF0E191F095B 194 | :100C10000E942D030E94A6026FEF79E7872E912C88 195 | :100C2000EFE5F7E0B1E0EB1AF10808F1C19BFECF68 196 | :100C300068BBD401BC9023B139B176B18181928176 197 | :100C4000AC01435A5A4FDA01BC92AC01435A5C4F93 198 | :100C50002F71307E232BDA012C939C01235A3E4FB7 199 | :100C6000D9017C930196917092838183DBCF21E03F 200 | :100C700030E002C0220F331FDA94E2F7BB24BA94AB 201 | :100C800059E7852E912CEFE5F7E0CC2009F458C008 202 | :100C9000C19BFECFB8BAD401DC9053B1C9B046B104 203 | :100CA00081819281BC01635A7A4FDB01DC92BC01E5 204 | :100CB000635A7C4F7B015F716C2D607E562BD70190 205 | :100CC0005C93BC01635A7E4FDB014C93019691709B 206 | :100CD00092838183842F90E082239323892BC1F216 207 | :100CE000BB24BA9489E7882E912CEFE5F7E0C19BED 208 | :100CF000FECFB8BAD401DC9053B1C9B046B18181FE 209 | :100D00009281BC01635A7A4FDB01DC92BC01635AC9 210 | :100D10007C4F7B015F716C2D607E562BD7015C93FD 211 | :100D2000BC01635A7E4FDB014C9301969170928314 212 | :100D30008183842F90E082239323892BC1F657C0AF 213 | :100D4000C19BFECFB8BAD401DC9053B1C9B046B153 214 | :100D500081819281BC01635A7A4FDB01DC92BC0134 215 | :100D6000635A7C4F7B015F716C2D607E562BD701DF 216 | :100D70005C93BC01635A7E4FDB014C9301969170EA 217 | :100D800092838183842F90E082239323892BC1F661 218 | :100D9000BB24BA9449E7842E912CEFE5F7E0C19B80 219 | :100DA000FECFB8BAD401DC9053B1C9B046B181814D 220 | :100DB0009281BC01635A7A4FDB01DC92BC01635A19 221 | :100DC0007C4F7B015F716C2D607E562BD7015C934D 222 | :100DD000BC01635A7E4FDB014C9301969170928364 223 | :100DE0008183842F90E082239323892BC1F2FF24F7 224 | :100DF000FA9499E7C92ED12CEFE5F7E001501109DB 225 | :100E000008F1C19BFECFF8BAD6015C9123B139B18C 226 | :100E100046B181819281BC01635A7A4FDB015C93B8 227 | :100E2000BC01635A7C4F2F71307E232BDB012C9346 228 | :100E30009C01235A3E4FD9014C93019691709283A5 229 | :100E40008183DCCF10927A001092810081E0809340 230 | :100E500012019DCDCF93DF93CDB7DEB70E94B703CC 231 | :100E6000909314018093130180E00E944F0280E070 232 | :100E70000E944F0286E897E00E94B60A892B11F083 233 | :100E80001092810083E6482E87E0582E04E011E09E 234 | :100E900088248394312C212C99249394772473945F 235 | :100EA000612C80919E07882309F452C110929E07FD 236 | :100EB0008FE997E00E946D01892B09F449C18FE900 237 | :100EC00097E00E944B018E33910509F02CC1F2018D 238 | :100ED000B08084E697E00E94B20A6B017C010E9418 239 | :100EE000B703F801908B878710926207F201108296 240 | :100EF000F8011486EFEBEB0DE63100F50E2E000C39 241 | :100F0000FF0BEB57F84F0C944B0FC4075408A20784 242 | :100F100038089E079E079E079E079E079E079E070E 243 | :100F20009E075B089E079E07E0079E079B07070832 244 | :100F300077089E07A807892D0E944F02EFE5F7E08A 245 | :100F40008082B6CFF801C282D382E482F582F6CFE6 246 | :100F500020E030E0A901C701B6010E94680C60E002 247 | :100F600087FD0FC020E030E04FE753E4C701B60132 248 | :100F70000E94460F6FEF181624F0C701B6010E94B9 249 | :100F8000D60EF8016083DACF20E030E0A901C70176 250 | :100F9000B6010E94680C60E087FD0FC020E030E0E1 251 | :100FA00040E351E4C701B6010E94460F6BE01816FA 252 | :100FB00024F0C701B6010E94D60EF8016087BECFAB 253 | :100FC00020E030E0A901C701B6010E94680CA32C03 254 | :100FD000B22C87FD18C0F801A388B488F1E0AF1ADD 255 | :100FE000B108B50190E080E00E946D0C9B01AC015E 256 | :100FF000C701B6010E94460F18162CF0C701B601B2 257 | :101000000E94D60E5B01F801B782A68297CF20E03E 258 | :1010100030E040E85FE3C701B6010E94680C93012D 259 | :1010200087FD12C020E030E040E054E4C701B60183 260 | :101030000E94460F30E022E0181634F0C701B601D6 261 | :101040000E94D60E362F272FF801338B248B468132 262 | :101050005781872D962D4115510531F0832F922F01 263 | :101060004817590708F4CA01F8019783868366CFA9 264 | :1010700020E030E0A901C701B6010E94680C60E0E1 265 | :1010800087FD0FC020E030E040E85FE3C701B60114 266 | :101090000E94460F692D181624F0C701B6010E9460 267 | :1010A000D60EF80161834ACFC701B6010E94D60E61 268 | :1010B000F801638743CF20E030E0A901C701B60102 269 | :1010C0000E94680C60E087FD0FC020E030E040E443 270 | :1010D00050E4C701B6010E94460F63E0181624F0E1 271 | :1010E000C701B6010E94D60EF801618727CF20E024 272 | :1010F00030E0A901C701B6010E94680C60E087FDDD 273 | :101100000FC020E030E040EA50E4C701B6010E9481 274 | :10111000460F65E0181624F0C701B6010E94D60EEE 275 | :10112000F80162870BCF20916207223210F01092F3 276 | :101130006207E0916207AE2FB0E0AD59B84F8C93D3 277 | :10114000EF5FE0936207F0E0ED59F84F1082B0CE08 278 | :1011500090929D070E94E70310929D07882D0E94A0 279 | :101160004F029FCE1F920F920FB60F9211242F9312 280 | :101170003F938F939F93AF93BF938091410890913A 281 | :101180004208A0914308B09144083091400820E4FF 282 | :10119000230F2D3728F023EC230F0196A11DB11D3D 283 | :1011A000209340088093410890934208A0934308FD 284 | :1011B000B093440880913C0890913D08A0913E086E 285 | :1011C000B0913F080196A11DB11D80933C089093FA 286 | :1011D0003D08A0933E08B0933F08BF91AF919F9107 287 | :1011E0008F913F912F910F900FBE0F901F901895E8 288 | :1011F0001F920F920FB60F9211242F933F934F938C 289 | :101200005F936F937F938F939F93AF93BF93EF936E 290 | :10121000FF93E091AF07F091B0078081E091B507AF 291 | :10122000F091B60782FD20C090818091B8078F5F52 292 | :101230008F732091B907821741F0E091B807F0E071 293 | :10124000E156F84F958F8093B80781E080939E0711 294 | :1012500080919D07882351F010929D0761E070E016 295 | :1012600086E897E00E94D60A8081EFCFFF91EF9148 296 | :10127000BF91AF919F918F917F916F915F914F91AE 297 | :101280003F912F910F900FBE0F901F9018951F92B6 298 | :101290000F920FB60F9211242F933F934F935F93AA 299 | :1012A0006F937F938F939F93AF93BF93EF93FF932E 300 | :1012B00080915B0190915C01009731F461E070E0F6 301 | :1012C00084E491E00E94D60A019790935C01809398 302 | :1012D0005B01FF91EF91BF91AF919F918F917F91B2 303 | :1012E0006F915F914F913F912F910F900FBE0F9093 304 | :1012F0001F9018951F920F920FB60F9211248F9383 305 | :10130000EF93FF93E1EAF0E080818F5F8083FF91AC 306 | :10131000EF918F910F900FBE0F901F9018951F9215 307 | :101320000F920FB60F9211242F933F934F935F9319 308 | :101330006F937F938F939F93AF93BF93EF93FF939D 309 | :101340008FE997E00E948D01FF91EF91BF91AF91DE 310 | :101350009F918F917F916F915F914F913F912F91CD 311 | :101360000F900FBE0F901F9018951F920F920FB6FF 312 | :101370000F9211242F938F939F93EF93FF93E091FC 313 | :10138000AF07F091B0078081E091B507F091B60703 314 | :1013900082FD1BC090818091B8078F5F8F73209171 315 | :1013A000B907821741F0E091B807F0E0E156F84F35 316 | :1013B000958F8093B807FF91EF919F918F912F9117 317 | :1013C0000F900FBE0F901F9018958081F4CFE2EF21 318 | :1013D000F0E0808180618EBB80E880839EB3908343 319 | :1013E000E1E6F0E0808391E090838083108278943E 320 | :1013F00084B5826084BD84B5816084BD85B582601A 321 | :1014000085BD85B5816085BDAEE6B0E08C9181601B 322 | :101410008C93E1E8F0E0108280818260808380819B 323 | :1014200081608083E0E8F0E0808181608083E1EB8F 324 | :10143000F0E0808184608083E0EBF0E08081816077 325 | :101440008083E1E9F0E08081826080838081816037 326 | :101450008083E0E9F0E0808181608083EAE7F0E06A 327 | :1014600080818460808380818D7F808380818E7F76 328 | :1014700080838081806880831092C100EFE9F7E06B 329 | :10148000C089D18982E08883C485D5851882C685C4 330 | :10149000D78582E28883108EC489D58986E08883C7 331 | :1014A000C289D389888180618883C289D3898881F0 332 | :1014B00088608883C289D38988818068888302880C 333 | :1014C000F389E02D80818F7D80838C918E7F8C933A 334 | :1014D000EFE6F0E080818E7F8083E0E7F0E08081BE 335 | :1014E0008E7F8083E1E7F0E080818E7F8083EEE76E 336 | :1014F000F0E080818160808317B818B81AB81BB8F3 337 | :1015000080E284B915B80E942A07EFE9F7E0138258 338 | :10151000128288EE93E0A0E0B0E084839583A683F6 339 | :10152000B78385E391E09183808385EC90E0958794 340 | :10153000848784EC90E09787868780EC90E0918B9D 341 | :10154000808B81EC90E0938B828B82EC90E0958B8A 342 | :10155000848B86EC90E0978B868B118E128E138E87 343 | :10156000148E0895662777270C94F70ADC012D92D4 344 | :101570003D924D925D926D927D928D929D92AD9233 345 | :10158000BD92CD92DD92ED92FD920D931D93CD9380 346 | :10159000DD93FF91EF918DB78D938EB78D938FB7BC 347 | :1015A0008D93ED93FD93882799270994DC01CB0156 348 | :1015B00081309105811D2D903D904D905D906D90F5 349 | :1015C0007D908D909D90AD90BD90CD90DD90ED90F3 350 | :1015D000FD900D911D91CD91DD91ED91FD910D90BD 351 | :1015E000F894FEBF0FBEEDBFED91FD910994B0E000 352 | :1015F000A0E0EDEFFAE00C94A00D5C017B01611519 353 | :10160000710519F0DB018D939C9385010F5F1F4FCE 354 | :10161000F501D0818D2F90E00E94470C6C01892B41 355 | :10162000B9F5DD32B9F50F5F1F4FD5011196DC9189 356 | :10163000C1E05801F1E0AF1AB10843E050E060E8C2 357 | :1016400070E0C5010E94500C892B69F5680182E0A9 358 | :10165000C80ED11C45E050E06BE770E0C6010E9467 359 | :10166000500C892B21F4680197E0C90ED11CE114BC 360 | :10167000F10419F0D701CD92DC9260E070E080E8CF 361 | :101680009FEFC111FFC060E070E080E89FE7FAC003 362 | :101690005801BBCFDB3229F485010E5F1F4FF501E6 363 | :1016A000D181C0E0C6CF43E050E068E770E0C501FB 364 | :1016B0000E94500C892BE9F0F80110E000E020E0D6 365 | :1016C00030E0A9015F01B0ED8B2E8D0E89E0881509 366 | :1016D000C8F19C2E689491F88C2F8870C2FF16C0B8 367 | :1016E000811102C00F5F1F4F3196D501DC91C92DCA 368 | :1016F000E9CFE114F10429F00E5F1F4FF7011183C8 369 | :10170000008360E070E080EC9FE7BCC0882311F0AC 370 | :1017100001501109A5E0B0E00E948F0D9B01AC01C2 371 | :10172000220F331F441F551F280D311D411D511D10 372 | :10173000283999E93907490799E15907A8F2C6609C 373 | :101740009C2ED2CFAEEF8A1206C0C3FD3CC09C2EA9 374 | :10175000689493F8C9CFDF7DD534A9F580818D32A7 375 | :1017600039F4C061DF011296818162E070E006C049 376 | :10177000DF018B32C1F3119661E070E080535D01AF 377 | :10178000A61AB70A8A30F8F4E0E8CE16ECE0DE06D6 378 | :101790005CF4B601660F771F660F771FC60ED71E63 379 | :1017A000CC0CDD1CC80ED11C5D01FFEFAF1ABF0AC7 380 | :1017B0008C9180538A30A8F1C4FF03C0D194C194A6 381 | :1017C000D1080C0D1D1DC1FF09C0E114F10431F059 382 | :1017D00081E0A81AB108D701AD92BC92CA01B90143 383 | :1017E0000E946D0CC370C33009F490584B015C012A 384 | :1017F00020E030E0A9010E94680C882309F440C071 385 | :10180000CFEAD0E017FF05C0119501951109C7E98E 386 | :10181000D0E06E01B8E1CB1AD10880E2E82EF12CBD 387 | :101820000FC0D501B1CFFE0125913591459154915D 388 | :101830000E191F09C501B4010E94CE0C4B015C01B9 389 | :10184000D501C4010E151F0574F72497F594E7948C 390 | :10185000CC16DD06A9F78A2F880F8B2F881F8F3FA4 391 | :1018600049F020E030E0A901C501B4010E94680CF4 392 | :10187000811106C082E290E090934A088093490863 393 | :10188000C501B401CDB7DEB7ECE00C94BC0D9111ED 394 | :101890000C943B0D803219F089508550C8F708959B 395 | :1018A000FB01DC014150504088F08D9181341CF0E7 396 | :1018B0008B350CF4805E659161341CF06B350CF453 397 | :1018C000605E861B611171F3990B0895881BFCCF34 398 | :1018D0000E94AA0C08F481E00895E89409C097FBDF 399 | :1018E0003EF490958095709561957F4F8F4F9F4FF7 400 | :1018F0009923A9F0F92F96E9BB279395F69587953B 401 | :1019000077956795B795F111F8CFFAF4BB0F11F4FD 402 | :1019100060FF1BC06F5F7F4F8F4F9F4F16C08823A4 403 | :1019200011F096E911C0772321F09EE8872F762FDA 404 | :1019300005C0662371F096E8862F70E060E02AF01B 405 | :101940009A95660F771F881FDAF7880F9695879507 406 | :1019500097F90895990F0008550FAA0BE0E8FEEFDC 407 | :1019600016161706E807F907C0F012161306E40763 408 | :10197000F50798F0621B730B840B950B39F40A265C 409 | :1019800061F0232B242B252B21F408950A2609F43A 410 | :10199000A140A6958FEF811D811D08950E94E10C45 411 | :1019A0000C94550D0E94470D38F00E944E0D20F00A 412 | :1019B000952311F00C943E0D0C94440D11240C94BD 413 | :1019C000890D0E94660D70F3959FC1F3950F50E04D 414 | :1019D000551F629FF001729FBB27F00DB11D639FE1 415 | :1019E000AA27F00DB11DAA1F649F6627B00DA11D87 416 | :1019F000661F829F2227B00DA11D621F739FB00D2D 417 | :101A0000A11D621F839FA00D611D221F749F33279C 418 | :101A1000A00D611D231F849F600D211D822F762F35 419 | :101A20006A2F11249F5750409AF0F1F088234AF012 420 | :101A3000EE0FFF1FBB1F661F771F881F915050407E 421 | :101A4000A9F79E3F510580F00C943E0D0C94890D32 422 | :101A50005F3FE4F3983ED4F3869577956795B79505 423 | :101A6000F795E7959F5FC1F7FE2B880F911D96951F 424 | :101A7000879597F9089599278827089597F99F677B 425 | :101A800080E870E060E008959FEF80EC0895002406 426 | :101A90000A941616170618060906089500240A94D3 427 | :101AA00012161306140605060895092E0394000C59 428 | :101AB00011F4882352F0BB0F40F4BF2B11F460FFE8 429 | :101AC00004C06F5F7F4F8F4F9F4F089557FD905811 430 | :101AD000440F551F59F05F3F71F04795880F97FBF2 431 | :101AE000991F61F09F3F79F08795089512161306AC 432 | :101AF0001406551FF2CF4695F1DF08C016161706DB 433 | :101B00001806991FF1CF869571056105089408950F 434 | :101B1000E894BB2766277727CB0197F908950E94A1 435 | :101B2000D10DA59F900DB49F900DA49F800D911D88 436 | :101B3000112408952F923F924F925F926F927F925D 437 | :101B40008F929F92AF92BF92CF92DF92EF92FF92CD 438 | :101B50000F931F93CF93DF93CDB7DEB7CA1BDB0B79 439 | :101B60000FB6F894DEBF0FBECDBF09942A8839881E 440 | :101B700048885F846E847D848C849B84AA84B98425 441 | :101B8000C884DF80EE80FD800C811B81AA81B98131 442 | :101B9000CE0FD11D0FB6F894DEBF0FBECDBFED0145 443 | :101BA0000895A29FB001B39FC001A39F700D811D36 444 | :101BB0001124911DB29F700D811D1124911D089556 445 | :101BC0005058BB27AA270E94F80D0C94550D0E946F 446 | :101BD000470D38F00E944E0D20F039F49F3F19F464 447 | :101BE00026F40C94440D0EF4E095E7FB0C943E0DA6 448 | :101BF000E92F0E94660D58F3BA176207730784072E 449 | :101C0000950720F079F4A6F50C94880D0EF4E09574 450 | :101C10000B2EBA2FA02D0B01B90190010C01CA01A6 451 | :101C2000A0011124FF27591B99F0593F50F4503E51 452 | :101C300068F11A16F040A22F232F342F4427585F43 453 | :101C4000F3CF469537952795A795F0405395C9F75B 454 | :101C50007EF41F16BA0B620B730B840BBAF0915013 455 | :101C6000A1F0FF0FBB1F661F771F881FC2F70EC0B2 456 | :101C7000BA0F621F731F841F48F487957795679585 457 | :101C8000B795F7959E3F08F0B0CF9395880F08F071 458 | :101C90009927EE0F9795879508950E942E0F90F043 459 | :101CA0009F3748F4911116F40C94890D60E070E0B0 460 | :101CB00080E89FE3089526F01B16611D711D811DAC 461 | :101CC0000C94050F0C94200F0E94780E0C94550D67 462 | :101CD0000E944E0D58F00E94470D40F029F45F3FDE 463 | :101CE00029F00C943E0D51110C94890D0C94440D67 464 | :101CF0000E94660D68F39923B1F3552391F3951B68 465 | :101D0000550BBB27AA2762177307840738F09F5F1C 466 | :101D10005F4F220F331F441FAA1FA9F335D00E2E89 467 | :101D20003AF0E0E832D091505040E695001CCAF7F6 468 | :101D30002BD0FE2F29D0660F771F881FBB1F2617B9 469 | :101D400037074807AB07B0E809F0BB0B802DBF0190 470 | :101D5000FF2793585F4F3AF09E3F510578F00C945F 471 | :101D60003E0D0C94890D5F3FE4F3983ED4F38695C5 472 | :101D700077956795B795F7959F5FC9F7880F911D80 473 | :101D80009695879597F90895E1E0660F771F881F6C 474 | :101D9000BB1F621773078407BA0720F0621B730B1F 475 | :101DA000840BBA0BEE1F88F7E09508950E946E0D24 476 | :101DB00088F09F5798F0B92F9927B751B0F0E1F00C 477 | :101DC000660F771F881F991F1AF0BA95C9F714C0BC 478 | :101DD000B13091F00E94880DB1E008950C94880D07 479 | :101DE000672F782F8827B85F39F0B93FCCF38695F5 480 | :101DF00077956795B395D9F73EF490958095709552 481 | :101E000061957F4F8F4F9F4F0895882371F47723FB 482 | :101E100021F09850872B762F07C0662311F499275D 483 | :101E20000DC09051862B70E060E02AF09A95660F05 484 | :101E3000771F881FDAF7880F9695879597F9089589 485 | :101E40009F3F31F0915020F4879577956795B7952E 486 | :101E5000880F911D9695879597F908950E946E0DAC 487 | :101E6000A0F0BEE7B91788F4BB279F3860F41616B8 488 | :101E7000B11D672F782F8827985FF7CF86957795C4 489 | :101E80006795B11D93959639C8F308950E94AA0CE1 490 | :101E900008F48FEF0895EE0FFF1F0590F491E02DE9 491 | :061EA0000994F894FFCF45 492 | :101EA6000001FFFD8001A48C38360000020000000E 493 | :101EB6000100006400000000020008000E0000019E 494 | :101EC6000802080308010E0100020E0200030E03B9 495 | :101ED6000000000000AE010F013C01F5016D014B51 496 | :041EE600015F010097 497 | :00000001FF 498 | -------------------------------------------------------------------------------- /src/communication/Serial.ts: -------------------------------------------------------------------------------- 1 | // based in wonky https://github.com/yaakov-h/uniden-web-controller/blob/master/serial.js 2 | 3 | export type Port = { 4 | readable: ReadableStream 5 | writable: WritableStream 6 | open: (options: SerialOptions) => Promise 7 | close: () => Promise 8 | getInfo: () => { usbProductId: number; usbVendorId: number } 9 | } 10 | export type NavigatorSerial = { 11 | requestPort: (optn: unknown) => Port 12 | getPorts: () => Promise 13 | } 14 | export type SerialOptions = { 15 | baudRate?: number 16 | dataBits?: number 17 | stopBits?: number 18 | parity?: string 19 | bufferSize?: number 20 | rtscts?: boolean 21 | xon?: boolean 22 | xoff?: boolean 23 | xany?: boolean 24 | } 25 | declare global { 26 | interface Window { 27 | serial: Serial 28 | } 29 | interface Navigator { 30 | serial: NavigatorSerial 31 | } 32 | } 33 | const END_SEQUENCE = [0, 1, 255, 253] 34 | const indexesOfSequence = (needle: number[], haystack: number[]) => { 35 | const strNeedle = needle.map((c) => String.fromCharCode(c)).join('') 36 | const strHaystack = haystack.map((c) => String.fromCharCode(c)).join('') 37 | const start = strHaystack.indexOf(strNeedle) 38 | const end = strHaystack.indexOf(strNeedle, start + strNeedle.length) 39 | return [start, end] 40 | } 41 | 42 | const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)) 43 | 44 | export class Serial { 45 | reader?: ReadableStreamDefaultReader 46 | writer?: WritableStreamDefaultWriter 47 | port?: Port 48 | readbuffer: number[] = [] 49 | outputDone?: Promise 50 | async close() { 51 | console.log('closing') 52 | if (this.reader) { 53 | const reader = this.reader 54 | this.reader = undefined 55 | await reader.cancel() 56 | } 57 | if (this.writer) { 58 | const writer = this.writer 59 | this.writer = undefined 60 | await writer.close() 61 | } 62 | if (this.outputDone) { 63 | const outputDone = this.outputDone 64 | this.outputDone = undefined 65 | await outputDone 66 | } 67 | if (this.port) { 68 | const port = this.port 69 | this.port = undefined 70 | await port.close() 71 | } 72 | console.log('closed') 73 | } 74 | async connectWithPaired(options: SerialOptions) { 75 | const filter = JSON.parse(localStorage.serialFilter) 76 | const ports = await navigator.serial.getPorts() 77 | const [port] = ports.filter( 78 | (port) => JSON.stringify(filter) === JSON.stringify(port.getInfo()) 79 | ) 80 | console.log(ports, port) 81 | if (!port) throw new Error('no paired') 82 | return this._connect(options, port) 83 | } 84 | async connect(options: SerialOptions) { 85 | const port = await navigator.serial.requestPort({}) 86 | return this._connect(options, port) 87 | } 88 | async _connect(options: SerialOptions, port: Port) { 89 | options = { 90 | baudRate: 9600, 91 | dataBits: 8, 92 | stopBits: 1, 93 | parity: 'none', 94 | bufferSize: 255, 95 | rtscts: false, 96 | xon: false, 97 | xoff: false, 98 | ...options 99 | } 100 | if (this.port) await this.close() 101 | this.port = port 102 | 103 | await this.port.open(options) 104 | const serialFilter = port.getInfo() 105 | localStorage.serialFilter = JSON.stringify(serialFilter) 106 | this.readbuffer = [] 107 | this.reader = this.port.readable.getReader() 108 | // this.writer = this.port.writable.getWriter() // binary 109 | const encoder = new TextEncoderStream() 110 | this.outputDone = encoder.readable.pipeTo(this.port.writable) 111 | const textOutputStream = encoder.writable 112 | this.writer = textOutputStream.getWriter() 113 | return serialFilter 114 | } 115 | 116 | write = async (text: string) => { 117 | if (!this.writer) return 118 | await this.writer.write(text) 119 | } 120 | onData(callback: (data: number[]) => unknown) { 121 | const consume = () => { 122 | let busy = true 123 | while (busy) { 124 | const [start, end] = indexesOfSequence(END_SEQUENCE, this.readbuffer) 125 | if (start > -1 && end > -1) { 126 | const dataFrame = this.readbuffer.slice( 127 | start + END_SEQUENCE.length, 128 | end 129 | ) 130 | this.readbuffer = this.readbuffer.slice(end) 131 | const checksumShould = (dataFrame.pop()! << 8) + dataFrame.pop()! 132 | const checksumIs = 133 | dataFrame.reduce((prev, curr) => prev + curr, 0) % Math.pow(2, 16) 134 | if (checksumShould === checksumIs) { 135 | callback(dataFrame) 136 | } else { 137 | console.error(`Checksum error: ${checksumIs}≠${checksumShould}`) 138 | // callback(dataFrame) 139 | } 140 | } else { 141 | busy = false 142 | } 143 | } 144 | } 145 | let running = true 146 | const produce = async () => { 147 | while (running) { 148 | await sleep(16) 149 | if (!this.reader) continue 150 | const { value } = await this.reader.read() 151 | if (value !== undefined) { 152 | this.readbuffer.push(...value) 153 | consume() 154 | } 155 | } 156 | } 157 | 158 | produce() 159 | // consume() 160 | return () => { 161 | running = false 162 | } 163 | } 164 | } 165 | const serial = new Serial() 166 | 167 | export default serial 168 | 169 | window.serial = serial 170 | window.addEventListener('beforeunload', () => serial.close()) 171 | -------------------------------------------------------------------------------- /src/communication/Serial2.ts: -------------------------------------------------------------------------------- 1 | // based in wonky https://github.com/yaakov-h/uniden-web-controller/blob/master/serial.js 2 | import { ReadableWebToNodeStream } from 'readable-web-to-node-stream' 3 | import { NavigatorSerial, Port, SerialOptions } from './Serial' 4 | 5 | declare global { 6 | interface Window { 7 | serial2: Serial 8 | } 9 | interface Navigator { 10 | serial: NavigatorSerial 11 | } 12 | } 13 | 14 | export class Serial { 15 | port?: Port 16 | reader?: NodeJS.ReadableStream 17 | writer?: WritableStreamDefaultWriter 18 | 19 | async close() { 20 | console.log('closing') 21 | if (this.reader) { 22 | const reader = this.reader 23 | this.reader = undefined 24 | // @ts-ignore 25 | await reader.reader.cancel() 26 | // await this.reader.close() // this blocks if uploading failed 27 | } 28 | if (this.writer) { 29 | const writer = this.writer 30 | this.writer = undefined 31 | await writer.close() 32 | } 33 | if (this.port) { 34 | const port = this.port 35 | this.port = undefined 36 | await port.close() 37 | } 38 | console.log('closed') 39 | } 40 | async connectWithPaired(options: SerialOptions) { 41 | const [port] = await navigator.serial.getPorts() 42 | if (!port) throw new Error('no paired') 43 | return this._connect(options, port) 44 | } 45 | async connect(options: SerialOptions) { 46 | const port = await navigator.serial.requestPort({}) 47 | return this._connect(options, port) 48 | } 49 | async _connect(options: SerialOptions, port: Port) { 50 | options = { 51 | baudRate: 9600, 52 | dataBits: 8, 53 | stopBits: 1, 54 | parity: 'none', 55 | bufferSize: 255, 56 | rtscts: false, 57 | xon: false, 58 | xoff: false, 59 | ...options 60 | } 61 | if (this.port) await this.close() 62 | this.port = port 63 | await this.port.open(options) 64 | this.reader = new ReadableWebToNodeStream(this.port.readable) 65 | this.writer = this.port.writable.getWriter() 66 | 67 | // next I'm faking a NodeJS.ReadWriteStream 68 | const rwStream = (this.reader as unknown) as NodeJS.ReadWriteStream 69 | // @ts-ignore 70 | rwStream.write = ( 71 | buffer: string | Uint8Array, 72 | onDone: (err: Error | null | undefined) => void 73 | ) => { 74 | this.writer!.write(buffer).then(() => onDone(null), onDone) 75 | return true 76 | } 77 | return rwStream 78 | } 79 | } 80 | const serial = new Serial() 81 | 82 | export default serial 83 | 84 | window.serial2 = serial 85 | // window.addEventListener('beforeunload', () => serial.close()) 86 | -------------------------------------------------------------------------------- /src/communication/bindings.tsx: -------------------------------------------------------------------------------- 1 | import { atom, selector, DefaultValue } from 'recoil' 2 | import parseSerial from './parseSerial' 3 | import { makeIntercom, memoSelector } from './bindingsHelper' 4 | import { getFFT, getFrequencyCount, oversample } from '../dsp/spectrum' 5 | import win from '../win' 6 | 7 | export const useTriggerVoltage = makeIntercom({ 8 | key: 'V', 9 | ui2mcu: (v, get) => { 10 | const [vmin, , vpp] = get ? get(voltageRangeState) : [0, 5, 5] 11 | return Math.ceil(((v + vmin) / vpp) * 255) 12 | }, 13 | mcu2ui: (n, get) => { 14 | const [vmin, , vpp] = get ? get(voltageRangeState) : [0, 5, 5] 15 | return (n / 255) * vpp + vmin 16 | }, 17 | default: 1 18 | }) 19 | 20 | export const use50percentTriggerVoltage = selector({ 21 | key: '50% trigger voltage', 22 | get: () => false, 23 | set: ({ set, get }) => { 24 | const { vavr } = get(voltagesState) 25 | set(useTriggerVoltage.send, vavr) 26 | } 27 | }) 28 | 29 | export const useTriggerPos = makeIntercom({ 30 | key: 'P', 31 | ui2mcu: (v, get) => { 32 | const samples = get ? get(useSamplesPerBuffer.send) : 512 33 | const result = v * samples 34 | if (result < 0) return 0 35 | if (result > samples - 1) return samples - 1 36 | return result 37 | }, 38 | mcu2ui: (v, get) => { 39 | const samples = get ? get(useSamplesPerBuffer.send) : 512 40 | return v / samples 41 | }, 42 | default: 0.5 43 | }) 44 | export const useSecPerSample = makeIntercom({ 45 | key: 'C', 46 | ui2mcu: (v) => v, 47 | mcu2ui: (v) => v, 48 | default: 0.00000275 49 | }) 50 | export const requestData = makeIntercom({ 51 | key: 'R', 52 | ui2mcu: (v) => v, 53 | mcu2ui: (v) => v, 54 | default: 0 55 | }) 56 | export enum TriggerDirection { 57 | FALLING = 'Falling', 58 | RISING = 'Rising' 59 | } 60 | export const useTriggerDirection = makeIntercom({ 61 | key: 'D', 62 | ui2mcu: (v) => (v === TriggerDirection.RISING ? 0 : 1), 63 | mcu2ui: (v) => (v ? TriggerDirection.FALLING : TriggerDirection.RISING), 64 | default: TriggerDirection.FALLING 65 | }) 66 | export const useTriggerChannel = makeIntercom({ 67 | key: 'T', 68 | ui2mcu: (v) => v, 69 | mcu2ui: (v) => v, 70 | default: 0 71 | }) 72 | export const useIsChannelOn = makeIntercom({ 73 | key: 'B', 74 | ui2mcu: (v) => { 75 | const result = v 76 | .map((b) => (b ? 1 : 0) as number) 77 | .reduce((acc, n, i) => acc + (n << i), 0) 78 | return result 79 | }, 80 | mcu2ui: (v) => { 81 | const result = Array(8) 82 | .fill(0) 83 | .map((_, i) => Boolean(v & (1 << i))) 84 | return result 85 | }, 86 | default: [true, false, false, false, false, false] 87 | }) 88 | 89 | export const constrain = (v: number, min: number, max: number) => 90 | v < min ? min : v > max ? max : v 91 | export const voltageRanges = [ 92 | 25, 93 | 6.25, 94 | 5, 95 | 3.125, 96 | 1.5625, 97 | 0.78125, 98 | 0.78125, 99 | 0.625, 100 | 0.390625, 101 | 0.3125, 102 | 0.1953125, 103 | 0.15625 104 | ] 105 | export const useAmplifier = makeIntercom({ 106 | key: 'A', 107 | ui2mcu: (v) => constrain(v, 0, voltageRanges.length - 1), 108 | mcu2ui: (v) => v, 109 | default: 1 //TODO: use volt range or per division 110 | }) 111 | export const useSamplesPerBuffer = makeIntercom({ 112 | key: 'S', 113 | ui2mcu: (v) => v, 114 | mcu2ui: (v) => v, 115 | default: 512 // TODO: this shouldn't be a setter 116 | }) 117 | export enum TriggerMode { 118 | AUTO = 'Auto', 119 | NORMAL = 'Normal', 120 | SINGLE = 'Single', 121 | SLOW = 'Slow' 122 | } 123 | 124 | export const useTriggerMode = makeIntercom({ 125 | key: 'M', 126 | ui2mcu: (v) => Object.values(TriggerMode).indexOf(v), 127 | mcu2ui: (v) => Object.values(TriggerMode)[v], 128 | default: TriggerMode.AUTO 129 | }) 130 | 131 | export type PlotDatum = { t: number; v: number } 132 | export const dataState = atom({ 133 | key: 'data', 134 | default: [...new Array(8)].map(() => [] as PlotDatum[]) 135 | }) 136 | 137 | export const isRunningState = memoSelector( 138 | atom({ 139 | key: 'is-running', 140 | default: true 141 | }) 142 | ) 143 | export const oversamplingFactorState = memoSelector( 144 | atom({ 145 | key: 'oversampling-factor', 146 | default: 0 147 | }) 148 | ) 149 | export const XYModeState = atom({ 150 | key: 'xy-mode', 151 | default: false 152 | }) 153 | export const fftState0 = atom({ 154 | key: 'fft0', 155 | default: false 156 | }) 157 | export const fftState1 = atom({ 158 | key: 'fft1', 159 | default: false 160 | }) 161 | 162 | export const didTriggerState = memoSelector( 163 | atom({ 164 | key: 'did-trigger', 165 | default: false 166 | }) 167 | ) 168 | export const freeMemoryState = memoSelector( 169 | atom({ 170 | key: 'free-memory', 171 | default: 0 172 | }) 173 | ) 174 | export const frequencyState = selector({ 175 | key: 'frequency', 176 | get: ({ get }) => getFrequencyCount(get(dataState)[0]) 177 | }) 178 | 179 | const sum = (signal: number[]) => 180 | signal.reduce((previous, current) => previous + current, 0) 181 | export const voltagesState = selector({ 182 | key: 'voltages', 183 | get: ({ get }) => { 184 | const signal = get(dataState)[0].map(({ v }) => v) 185 | const vmax = Math.max(...signal) 186 | const vmin = Math.min(...signal) 187 | const vpp = vmax - vmin 188 | const vavr = sum(signal) / signal.length 189 | 190 | return { 191 | vavr, 192 | vpp, 193 | vmin, 194 | vmax 195 | } 196 | } 197 | }) 198 | 199 | export const voltageRangeState = selector({ 200 | key: 'voltage-range', 201 | get: ({ get }) => { 202 | //https://docs.google.com/spreadsheets/d/1urWB28qDmB_LL_khdBBfB-djku5h4lSx-Cw9l7Rz1u8/edit?usp=sharing 203 | // TODO: do this in another way 204 | // TODO: account for the last fifth of the ADC, which is not usable 205 | const vmax = voltageRanges[get(useAmplifier.send)] 206 | const vmin = 0 207 | return [vmin, vmax, vmax - vmin] 208 | } 209 | }) 210 | 211 | const sendFullState = selector({ 212 | key: 'sendFullState-this shouldnt be a selector', 213 | get: () => null, 214 | set: ({ get, set }) => { 215 | set(useTriggerPos.send, get(useTriggerPos.send)) 216 | set(useSecPerSample.send, get(useSecPerSample.send)) 217 | set(useSamplesPerBuffer.send, get(useSamplesPerBuffer.send)) 218 | set(useAmplifier.send, get(useAmplifier.send)) 219 | set(useTriggerVoltage.send, get(useTriggerVoltage.send)) 220 | set(useTriggerDirection.send, get(useTriggerDirection.send)) 221 | set(useTriggerChannel.send, get(useTriggerChannel.send)) 222 | set(useTriggerMode.send, get(useTriggerMode.send)) 223 | set(useIsChannelOn.send, get(useIsChannelOn.send)) 224 | } 225 | }) 226 | 227 | type Data = ReturnType 228 | const receiveFullState = selector({ 229 | key: 'receiveFullState-this shouldnt be a selector', 230 | get: () => { 231 | throw new Error('write only selector') 232 | }, 233 | set: ({ set }, data) => { 234 | if (data instanceof DefaultValue) return 235 | set(useTriggerPos.receive, data.triggerPos) 236 | set(useSecPerSample.receive, data.secPerSample) 237 | set(useSamplesPerBuffer.receive, data.samplesPerBuffer) 238 | set(useAmplifier.receive, data.amplifier) 239 | set(useTriggerVoltage.receive, data.triggerVoltage) 240 | set(useTriggerDirection.receive, data.triggerDir) 241 | set(useTriggerChannel.receive, data.triggerChannel) 242 | set(useTriggerMode.receive, data.triggerMode) 243 | set(useIsChannelOn.receive, data.isChannelOn) 244 | } 245 | }) 246 | 247 | export const allDataState = selector({ 248 | key: 'all-data-this shouldnt be a selector', 249 | get: () => [], // this is a write only selector 250 | set: ({ set, get }, newData) => { 251 | win.$recoilDebugStates = [] // TODO: fix memory leak in recoiljs beta 252 | if (newData instanceof DefaultValue) return 253 | if (newData.length === 0) return 254 | const data = parseSerial(newData) 255 | if (data.needData) set(sendFullState, null) 256 | if (data.forceUIUpdate) set(receiveFullState, data) 257 | 258 | let buffers = data.buffers 259 | 260 | set(freeMemoryState, data.freeMemory) 261 | set(didTriggerState, !!data.didTrigger) 262 | const shouldUpdate = 263 | // todo use isRunning state in board for this 264 | get(isRunningState) && buffers.some((buffer) => buffer.length > 0) 265 | if (shouldUpdate) { 266 | const oldBuffers = get(dataState) 267 | 268 | const oversamplingFactor = get(oversamplingFactorState) 269 | if (oversamplingFactor > 0) { 270 | const factor = 1 - 2 / (oversamplingFactor + 1) 271 | buffers = buffers.map((b, i) => 272 | oversample(factor, buffers[i], oldBuffers[i]) 273 | ) 274 | } 275 | 276 | if (get(useTriggerMode.send) === TriggerMode.SLOW) { 277 | const oldLastT = Math.max( 278 | ...oldBuffers.map((b) => b[b.length - 1]?.t || 0) 279 | ) 280 | 281 | buffers = buffers.map((b, i) => [ 282 | ...oldBuffers[i], 283 | ...b.map(({ v, t }) => ({ v, t: t + oldLastT })) 284 | ]) 285 | const totalSecs = 286 | get(useSecPerSample.send) * get(useSamplesPerBuffer.send) 287 | const lastT = Math.max(...buffers.map((b) => b[b.length - 1]?.t || 0)) 288 | if (lastT > totalSecs) { 289 | buffers = buffers.map(() => []) 290 | } 291 | } 292 | const withFFT = [ 293 | ...buffers, 294 | get(fftState0) ? getFFT(buffers[0]) : [], 295 | get(fftState1) ? getFFT(buffers[1]) : [] 296 | ] 297 | 298 | set(dataState, withFFT) 299 | if (get(useTriggerMode.send) === TriggerMode.SINGLE) { 300 | set(isRunningState, false) 301 | } 302 | } 303 | } 304 | }) 305 | -------------------------------------------------------------------------------- /src/communication/bindingsHelper.ts: -------------------------------------------------------------------------------- 1 | import { atom, selector, DefaultValue, RecoilState, RecoilValue } from 'recoil' 2 | import serial from './Serial' 3 | 4 | export function memoSelector(theAtom: RecoilState) { 5 | return selector({ 6 | key: 'memo' + theAtom.key, 7 | get: ({ get }) => get(theAtom), 8 | set: ({ set, get }, newValue) => { 9 | const old = get(theAtom) 10 | if (old !== newValue) { 11 | set(theAtom, newValue) 12 | } 13 | } 14 | }) 15 | } 16 | type GetRecoilValue = (recoilVal: RecoilValue) => T 17 | export function makeIntercom({ 18 | key, 19 | ui2mcu, 20 | mcu2ui, 21 | default: defaultValue 22 | }: { 23 | key: string 24 | ui2mcu: (v: T, get: GetRecoilValue | null) => number 25 | mcu2ui: (v: number, get: GetRecoilValue | null) => T 26 | default: T 27 | }) { 28 | const remoteState = memoSelector( 29 | atom({ 30 | key, 31 | default: ui2mcu(defaultValue, null) 32 | }) 33 | ) 34 | 35 | // throttle to avoid filling the MCU serial buffer 36 | const serial_write = serial.write 37 | // const serial_write = throttle(serial.write, 40, { 38 | // leading: false, 39 | // trailing: true 40 | // }) 41 | const send = selector({ 42 | key: key + '-selector', 43 | get: ({ get }) => mcu2ui(get(remoteState), get), 44 | set: ({ set, get, reset }, newValue) => { 45 | if (newValue instanceof DefaultValue) return reset(remoteState) 46 | set(remoteState, ui2mcu(newValue, get)) 47 | serial_write(key + ui2mcu(newValue, get) + '>') 48 | } 49 | }) 50 | const receive = selector({ 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 | 50 | ))} 51 | 52 | 53 |
54 | Oversample 55 | 63 |
64 | 65 | 66 | 76 | 86 | 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 | 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 | 63 | ))} 64 | 65 | 66 | 67 | 68 | {Object.values(TriggerMode).map((mode) => ( 69 | 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 | 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 | --------------------------------------------------------------------------------