├── .editorconfig ├── .gitattributes ├── .gitignore ├── LICENSE.md ├── README.md ├── THEME.md ├── arduino ├── Cereal │ ├── Cereal.h │ ├── CerealStream.cpp │ └── CerealStream.h └── README.md ├── babel.config.json ├── images ├── demo.gif └── example.png ├── package-lock.json ├── package.json ├── src ├── app.js ├── components │ ├── base │ │ ├── box.js │ │ ├── heading.js │ │ └── text.js │ ├── header.js │ ├── header │ │ └── status.js │ ├── input.js │ ├── monitor.js │ ├── monitor │ │ ├── direction.js │ │ └── timestamp.js │ ├── variables.js │ └── variables │ │ ├── base │ │ ├── gauge.js │ │ ├── indicator.js │ │ ├── name.js │ │ └── value.js │ │ ├── flag.js │ │ ├── range.js │ │ └── variable.js ├── index.js ├── redux │ ├── actions │ │ ├── config.js │ │ ├── messages.js │ │ ├── serial.js │ │ ├── types.js │ │ └── variables.js │ ├── reducers │ │ ├── config.js │ │ ├── index.js │ │ ├── messages.js │ │ ├── serial.js │ │ └── variables.js │ └── store │ │ └── index.js ├── serial │ ├── index.js │ └── parser.js ├── theme │ ├── index.js │ └── presets │ │ ├── black.js │ │ ├── dark.js │ │ ├── light.js │ │ └── white.js └── utils │ ├── cli.js │ ├── config.js │ └── use-init.js └── test └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | .DS_Store 4 | .idea/ 5 | 6 | /lib/ 7 | *.log 8 | 9 | cereal.config.json 10 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Mladen Ilic 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Arduino Cereal Monitor 2 | 3 | Drop-in replacement for Arduino's serial monitor with variable watches. 4 | 5 | ![image](images/example.png) 6 | 7 | ### How it works 8 | 9 | Use `Serial.print`, `Serial.println`, etc. to print data to **[Monitor]** window. To track variables in **[Variables]** window, use *Cereal library* ([documentation & demo](./arduino/README.md)). 10 | 11 | ### Install 12 | 13 | ```bash 14 | $ npm install --global arduino-cereal 15 | ``` 16 | 17 | OR run it with [npx](https://www.npmjs.com/package/npx) 18 | 19 | ```bash 20 | $ npx arduino-cereal 21 | ``` 22 | 23 | *Requires Node v14.0.0 or later* 24 | 25 | ### CLI options 26 | 27 | ```bash 28 | $ arduino-cereal --help 29 | 30 | Usage 31 | $ arduino-cereal 32 | 33 | Options 34 | --port Serial port 35 | --baud Baud rate (default: 9600) 36 | --theme Theme name (light, dark, black, white) 37 | --fps Screen render rate (default: 15) 38 | ``` 39 | 40 | * **port:** – Serial port to which Arduino board is connected. 41 | * **baud:** – Baud rate. Default value 9600 42 | * **theme:** – Change the color theme. Available options are *light*, *dark*, *black*, *black*. The default theme is *dark*. Read more about [theme customization](THEME.md). 43 | * **fps** – Control the rate at which the CLI screen is updated. Helps to deal with flickering on some terminal apps (iTerm). Default: 15 44 | 45 | Example: 46 | ```bash 47 | $ arduino-cereal --port=/dev/tty.usbserial-1410 --baud=19200 --theme=light --fps=10 48 | ``` 49 | 50 | ### Configuration file 51 | 52 | Alternatively, CLI options can be read from a file – `cereal.config.json`. This file is expected to be located in the current working directory. 53 | 54 | ```json5 55 | { 56 | "port": "/dev/tty.usbserial-1410", 57 | "baud": 9600, 58 | "fps": 15, 59 | 60 | "theme": { /** Theme customization options */ } 61 | } 62 | ``` 63 | 64 | ### Contributing 65 | 66 | Contributions are welcome. 🎉 67 | 68 | ### License 69 | 70 | Licensed under the MIT license. See the [LICENSE.md](LICENSE.md) for more information. 71 | -------------------------------------------------------------------------------- /THEME.md: -------------------------------------------------------------------------------- 1 | # Arduino Cereal Monitor Themes 2 | 3 | This document describes monitor theme customization. 4 | 5 | ### Getting Started 6 | 7 | These options are specified in the configuration file. Read [here](README.md#configuration-file) how to set up a configuration file. 8 | 9 | ### Configuration 10 | 11 | Below is a sample configuration file with all theme options. You can use it as a template to create a custom theme. 12 | 13 | ```json 14 | { 15 | "port": "/dev/tty.usbserial-1410", 16 | "baud": 9600, 17 | "fps": 15, 18 | 19 | "theme": { 20 | "modules": { 21 | "monitor": { 22 | "timestamp": true, 23 | "timestampLocale": "en-GB" 24 | }, 25 | 26 | "variables": { 27 | "enabled": true, 28 | "range": { 29 | "fill": "▇", 30 | "empty": "-" 31 | }, 32 | 33 | "flag": { 34 | "on": "▣", 35 | "off": "▢" 36 | } 37 | }, 38 | 39 | "input": { 40 | "enabled": true, 41 | "prefix": "$>" 42 | } 43 | }, 44 | 45 | "colors": { 46 | "default": "#839496", 47 | 48 | "global": { 49 | "heading": "#b58900", 50 | "border": "#268bd2" 51 | }, 52 | 53 | "status": { 54 | "success": "#859900", 55 | "error": "#dc322f" 56 | }, 57 | 58 | "monitor": { 59 | "timestamp": "#b58900", 60 | "message": "#839496" 61 | }, 62 | 63 | "variables": { 64 | "name": "#839496", 65 | "value": "#839496", 66 | 67 | "range": { 68 | "fill": "#b58900", 69 | "empty": "#839496" 70 | }, 71 | "flag": { 72 | "on": "#b58900", 73 | "off": "#b58900" 74 | } 75 | } 76 | } 77 | } 78 | } 79 | 80 | ``` 81 | -------------------------------------------------------------------------------- /arduino/Cereal/Cereal.h: -------------------------------------------------------------------------------- 1 | #ifndef _CEREAL_H_ 2 | #define _CEREAL_H_ 3 | 4 | #include "CerealStream.h" 5 | 6 | CerealStream Cereal(Serial); 7 | 8 | #endif _CEREAL_H_ 9 | -------------------------------------------------------------------------------- /arduino/Cereal/CerealStream.cpp: -------------------------------------------------------------------------------- 1 | #include "CerealStream.h" 2 | 3 | #define _CEREAL_PRINT_VARIABLE(name, value) start(Type::VARIABLE) + print(name) + d() + print(value) + end(); 4 | #define _CEREAL_PRINT_RANGE(name, value, from, to) \ 5 | start(Type::RANGE) + print(name) + d() \ 6 | + print(value) + d() \ 7 | + print(from) + d() \ 8 | + print(to) + end(); \ 9 | 10 | CerealStream::CerealStream(Print& stream): stream(stream) {} 11 | 12 | size_t CerealStream::variable(const char *name, char value) { 13 | return _CEREAL_PRINT_VARIABLE(name, value); 14 | } 15 | 16 | size_t CerealStream::variable(const char *name, unsigned char value) { 17 | return variable(name, (unsigned long) value); 18 | } 19 | 20 | size_t CerealStream::variable(const char *name, int value) { 21 | return variable(name, (long) value); 22 | } 23 | 24 | size_t CerealStream::variable(const char *name, unsigned int value) { 25 | return variable(name, (unsigned long) value); 26 | } 27 | 28 | size_t CerealStream::variable(const char *name, long value) { 29 | return _CEREAL_PRINT_VARIABLE(name, value); 30 | } 31 | 32 | size_t CerealStream::variable(const char *name, unsigned long value) { 33 | return _CEREAL_PRINT_VARIABLE(name, value); 34 | } 35 | 36 | size_t CerealStream::variable(const char *name, double value) { 37 | return _CEREAL_PRINT_VARIABLE(name, value); 38 | } 39 | 40 | size_t CerealStream::range(const char *name, char value, char from, char to) { 41 | return range(name, (long) value, (long) from, (long) to); 42 | } 43 | 44 | size_t CerealStream::range(const char *name, unsigned char value, unsigned char from, unsigned char to) { 45 | return range(name, (unsigned long) value, (unsigned long) from, (unsigned long) to); 46 | } 47 | 48 | size_t CerealStream::range(const char *name, int value, int from, int to) { 49 | return range(name, (long) value, (long) from, (long) to); 50 | } 51 | 52 | size_t CerealStream::range(const char *name, unsigned int value, unsigned int from, unsigned int to) { 53 | return range(name, (unsigned long) value, (unsigned long) from, (unsigned long) to); 54 | } 55 | 56 | size_t CerealStream::range(const char *name, long value, long from, long to) { 57 | return _CEREAL_PRINT_RANGE(name, value, from, to); 58 | } 59 | 60 | size_t CerealStream::range(const char *name, unsigned long value, unsigned long from, unsigned long to) { 61 | return _CEREAL_PRINT_RANGE(name, value, from, to); 62 | } 63 | 64 | size_t CerealStream::range(const char *name, double value, double from, double to) { 65 | return _CEREAL_PRINT_RANGE(name, value, from, to); 66 | } 67 | 68 | size_t CerealStream::flag(const char *name, unsigned int value) { 69 | return start(Type::FLAG) + print(name) + d() + print(value) + end(); 70 | } 71 | 72 | size_t CerealStream::digital(const char *name, unsigned int value) { 73 | return flag(name, value); 74 | } 75 | 76 | size_t CerealStream::analog(const char *name, int value) { 77 | return range(name, (long) value, (long) 0, (long) 1023); 78 | } 79 | -------------------------------------------------------------------------------- /arduino/Cereal/CerealStream.h: -------------------------------------------------------------------------------- 1 | #ifndef _CEREAL_STREAM_H_ 2 | #define _CEREAL_STREAM_H_ 3 | 4 | #include "Arduino.h" 5 | 6 | #define MESSAGE_START "\xc0" 7 | #define MESSAGE_END "\xc0" 8 | #define VARIABLE_DELIMTER "\xc1" 9 | 10 | class CerealStream: public Print { 11 | public: 12 | CerealStream(Print& stream); 13 | 14 | size_t variable(const char *name, char value); 15 | size_t variable(const char *name, unsigned char value); 16 | size_t variable(const char *name, int value); 17 | size_t variable(const char *name, unsigned int value); 18 | size_t variable(const char *name, long value); 19 | size_t variable(const char *name, unsigned long value); 20 | size_t variable(const char *name, double value); 21 | 22 | size_t range(const char *name, char value, char from, char to); 23 | size_t range(const char *name, unsigned char value, unsigned char from, unsigned char to); 24 | size_t range(const char *name, int value, int from, int to); 25 | size_t range(const char *name, unsigned int value, unsigned int from, unsigned int to); 26 | size_t range(const char *name, long value, long from, long to); 27 | size_t range(const char *name, unsigned long value, unsigned long from, unsigned long to); 28 | size_t range(const char *name, double value, double from, double to); 29 | 30 | size_t flag(const char *name, unsigned int value); 31 | 32 | size_t digital(const char *name, unsigned int value); 33 | size_t analog(const char *name, int value); 34 | private: 35 | enum Type { 36 | VARIABLE = 0x00, 37 | RANGE = 0x01, 38 | FLAG = 0x02 39 | }; 40 | 41 | Print& stream; 42 | 43 | using Print::write; 44 | 45 | size_t start() { return print(MESSAGE_START); } 46 | size_t start(Type type) { return start() + t(type) + d(); } 47 | size_t end() { return print(MESSAGE_END); } 48 | size_t t(Type type) { return print(type); } 49 | size_t d() { return print(VARIABLE_DELIMTER); } 50 | size_t write(uint8_t c) { return stream.write(c); } 51 | }; 52 | 53 | #endif _CEREAL_STREAM_H_ 54 | -------------------------------------------------------------------------------- /arduino/README.md: -------------------------------------------------------------------------------- 1 | # Arduino Cereal Library 2 | 3 | Arduino library for tracking variables in [Cereal monitor](../README.md). 4 | 5 | ### Install 6 | 7 | Download and manually install to your Arduino libraries. Read the "Manual Installation" section on Arduino's [library installation instruction](https://www.arduino.cc/en/Guide/Libraries) page. 8 | 9 | ### Usage 10 | 11 | Cereal monitor supports three different types of variables: *basic* variable, *range*, and a *flag* variable. 12 | 13 | For basic variable output, use: 14 | ```cpp 15 | Cereal.variable(name, value); 16 | ``` 17 | 18 | For range variable outputs use: 19 | ```cpp 20 | Cereal.range(name, value, from, to); 21 | Cereal.analog(name, value); 22 | ``` 23 | 24 | For flag variable output use: 25 | ```cpp 26 | Cereal.digital(name, value); 27 | ``` 28 | 29 | ### Example 30 | 31 | ```cpp 32 | #include "Cereal.h" 33 | 34 | #define STATUS_PIN 3 35 | #define THROTTLE_PIN A2 36 | 37 | void setup() { 38 | // Set the baud rate for serial data transmission 39 | Serial.begin(9600); 40 | 41 | pinMode(STATUS_PIN, INPUT); 42 | pinMode(THROTTLE_PIN, INPUT); 43 | 44 | // Serial output shown in [Monitor] window 45 | Serial.println("Setup done..."); 46 | } 47 | 48 | void loop() { 49 | int rnd = random(10, 20); 50 | int throttle = analogRead(THROTTLE_PIN); 51 | int status = digitalRead(STATUS_PIN); 52 | 53 | // Basic variable type output 54 | Cereal.variable("Random", rnd); 55 | 56 | // Range variable type output 57 | Cereal.analog("Throttle", throttle); 58 | 59 | // Flag variable type output 60 | Cereal.digital("Status", status); 61 | } 62 | 63 | ``` 64 | 65 | Running this examples gives the following output: 66 | 67 | ![image](../images/demo.gif) 68 | 69 | ### Software Serial 70 | 71 | You can send data to multiple Cereal monitors by creating more than one `CerealStream`. 72 | `CerealStream` object take a destination stream it should write to. 73 | 74 | Destination stream is any object of class that inherits from built-in `Print` class. 75 | 76 | ```cpp 77 | #include "SoftwareSerial.h" 78 | #include "Cereal.h" 79 | 80 | int i = 0; 81 | 82 | SoftwareSerial sSerial(3, 4); 83 | CerealStream sCereal(sSerial); 84 | 85 | void setup() { 86 | Serial.begin(9600); 87 | sSerial.begin(9600); 88 | } 89 | 90 | void loop() { 91 | // Writes to HardwareSerial0 92 | Cereal.variable("I", i++); 93 | 94 | // Writes to SoftwareSerial RX(3)/TX(4) 95 | sCereal.variable("I", i++); 96 | } 97 | 98 | ``` 99 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react", 4 | [ 5 | "@babel/preset-env", 6 | { 7 | "targets": { 8 | "node": true 9 | } 10 | } 11 | ] 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mladenilic/arduino-cereal/4f947aea161cbe469fde33abf8567f915c3c2f70/images/demo.gif -------------------------------------------------------------------------------- /images/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mladenilic/arduino-cereal/4f947aea161cbe469fde33abf8567f915c3c2f70/images/example.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arduino-cereal", 3 | "version": "1.1.0", 4 | "description": "Drop-in replacement for Arduino serial monitor", 5 | "license": "MIT", 6 | "homepage": "https://github.com/mladenilic/arduino-cereal#readme", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/mladenilic/arduino-cereal.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/mladenilic/arduino-cereal/issues" 13 | }, 14 | "keywords": [ 15 | "arduino", 16 | "serial", 17 | "monitor" 18 | ], 19 | "author": "Mladen Ilic (https://github.com/mladenilic)", 20 | "bin": { 21 | "arduino-cereal": "lib/index.js" 22 | }, 23 | "engines": { 24 | "node": ">=10" 25 | }, 26 | "scripts": { 27 | "test": "xo && ava", 28 | "build": "rm -rf lib/ && babel src -d lib", 29 | "start": "babel-node ./src" 30 | }, 31 | "files": [ 32 | "lib" 33 | ], 34 | "dependencies": { 35 | "ink": "^3.0.8", 36 | "lodash.merge": "^4.6.2", 37 | "meow": "^8.0.0", 38 | "react": "^17.0.1", 39 | "react-redux": "^7.2.2", 40 | "redux": "^4.0.5", 41 | "serialport": "^9.0.2" 42 | }, 43 | "devDependencies": { 44 | "@babel/cli": "^7.12.7", 45 | "@babel/core": "^7.12.7", 46 | "@babel/node": "^7.12.6", 47 | "@babel/preset-env": "^7.12.7", 48 | "@babel/preset-react": "^7.12.7", 49 | "@babel/register": "^7.12.1", 50 | "ava": "^3.13.0", 51 | "chalk": "^4.1.0", 52 | "eslint-config-xo-react": "^0.23.0", 53 | "eslint-plugin-react": "^7.21.5", 54 | "eslint-plugin-react-hooks": "^4.2.0", 55 | "ink-testing-library": "^2.1.0", 56 | "xo": "^0.35.0" 57 | }, 58 | "ava": { 59 | "babel": true, 60 | "require": [ 61 | "@babel/register" 62 | ] 63 | }, 64 | "babel": { 65 | "presets": [ 66 | "@babel/preset-env", 67 | "@babel/preset-react" 68 | ] 69 | }, 70 | "xo": { 71 | "extends": "xo-react", 72 | "space": true, 73 | "rules": { 74 | "react/prop-types": "off", 75 | "object-curly-spacing": "off", 76 | "import/no-anonymous-default-export": "off", 77 | "default-param-last": "off", 78 | "no-unused-expressions": [ 79 | "error", 80 | { 81 | "allowTernary": true 82 | } 83 | ] 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useCallback } from 'react'; 2 | 3 | import { connect } from 'react-redux/lib/alternate-renderers'; 4 | import { setConfig } from './redux/actions/config'; 5 | import { updateVariable, outputVariables } from './redux/actions/variables'; 6 | import { addMessage, outputMessages } from './redux/actions/messages'; 7 | import { setSerialStatus } from './redux/actions/serial'; 8 | 9 | import Serial from './serial'; 10 | 11 | import Box from './components/base/box'; 12 | import Text from './components/base/Text'; 13 | import Header from './components/header'; 14 | import Variables from './components/variables'; 15 | import Monitor from './components/monitor'; 16 | import Input from './components/input'; 17 | 18 | import useInit from './utils/use-init'; 19 | 20 | const App = ({ config, setConfig, updateVariable, outputVariables, addMessage, outputMessages, setSerialStatus }) => { 21 | const serial = useRef(); 22 | const onInput = useCallback((input) => { 23 | serial.current.write(input); 24 | addMessage(`${input}\r\n`, true); 25 | }); 26 | 27 | useInit(() => setConfig(config)); 28 | useInit(() => setInterval(() => { 29 | outputVariables(); 30 | outputMessages(); 31 | }, 1000 / Math.min(Math.max(config.fps || 15, 1), 60))); 32 | 33 | useInit(() => { 34 | if (!config.port) { 35 | return; 36 | } 37 | 38 | serial.current = Serial.connect(config.port, config.baud || 9600) 39 | .on('connect', () => setSerialStatus('success')) 40 | .on('error', () => setSerialStatus('error')) 41 | .on('variable', ([type, name, value, ...options]) => updateVariable(type, name, value, options)) 42 | .on('message', message => addMessage(message)); 43 | }); 44 | 45 | if (!config.port) { 46 | return ERROR: Please specify serial port.; 47 | } 48 | 49 | return ( 50 | 51 |
52 | 53 | 54 | 55 | 56 | 57 | 58 | ); 59 | }; 60 | 61 | export default connect( 62 | null, 63 | { setConfig, updateVariable, outputVariables, addMessage, outputMessages, setSerialStatus } 64 | )(App); 65 | -------------------------------------------------------------------------------- /src/components/base/box.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box as InkBox } from 'ink'; 3 | import { connect } from 'react-redux/lib/alternate-renderers'; 4 | 5 | const Box = ({ theme, children, boxRef, borderColor, ...rest }) => { 6 | const color = borderColor || theme?.colors?.global.border || theme?.colors?.default; 7 | 8 | return {children}; 9 | }; 10 | 11 | export default connect( 12 | state => ({ theme: state?.config?.theme || {} }) 13 | )(Box); 14 | -------------------------------------------------------------------------------- /src/components/base/heading.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { connect } from 'react-redux/lib/alternate-renderers'; 4 | 5 | import Box from './box'; 6 | import Text from './text'; 7 | 8 | const Heading = ({ theme, color, children }) => ( 9 | 10 | [{children}] 11 | 12 | ); 13 | 14 | export default connect( 15 | state => ({ theme: state?.config?.theme || {} }) 16 | )(Heading); 17 | -------------------------------------------------------------------------------- /src/components/base/text.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Text as InkText } from 'ink'; 4 | import { connect } from 'react-redux/lib/alternate-renderers'; 5 | 6 | const Text = ({ theme, color, children, ...rest }) => ( 7 | {children} 8 | ); 9 | 10 | export default connect( 11 | state => ({ theme: state?.config?.theme || {} }) 12 | )(Text); 13 | -------------------------------------------------------------------------------- /src/components/header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Spacer } from 'ink'; 4 | 5 | import Box from './base/box'; 6 | import Text from './base/text'; 7 | import Heading from './base/heading'; 8 | import Status from './header/status'; 9 | 10 | import { connect } from 'react-redux/lib/alternate-renderers'; 11 | 12 | const Header = ({ config }) => ( 13 | 14 | Arduino Cereal Monitor 15 | — Port: {config.port || '/'}, Baud rate: {config.baud} 16 | 17 | 18 | 19 | ); 20 | 21 | export default connect( 22 | state => ({ config: state.config }) 23 | )(Header); 24 | -------------------------------------------------------------------------------- /src/components/header/status.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Text from '../base/text'; 3 | 4 | import { connect } from 'react-redux/lib/alternate-renderers'; 5 | 6 | const circle = String.fromCharCode(11044); 7 | 8 | const Status = ({ theme, serial }) => ( 9 | 10 | Status: 11 | {serial.status === 'pending' ? 'Connecting...' : circle} 12 | 13 | ); 14 | 15 | export default connect( 16 | state => ({ 17 | theme: state?.config?.theme || {}, 18 | serial: state.serial 19 | }) 20 | )(Status); 21 | -------------------------------------------------------------------------------- /src/components/input.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from 'react'; 2 | 3 | import { useInput } from 'ink'; 4 | 5 | import Box from './base/box'; 6 | import Text from './base/text'; 7 | 8 | import { connect } from 'react-redux/lib/alternate-renderers'; 9 | 10 | const Input = ({ onInput, settings }) => { 11 | if (!settings.enabled) { 12 | return null; 13 | } 14 | 15 | const [value, setValue] = useState(''); 16 | const postValue = useCallback((value) => { 17 | if (!onInput || value.length === 0) { 18 | return; 19 | } 20 | 21 | setValue(''); 22 | onInput(value); 23 | }, [onInput]); 24 | 25 | useInput((input, key) => { 26 | if (key.return) { 27 | return postValue(value); 28 | } 29 | 30 | if (key.backspace || key.delete) { 31 | return setValue(value.slice(0, -1)); 32 | } 33 | 34 | setValue(value + input); 35 | }); 36 | 37 | return ( 38 | 39 | {settings.prefix || ''} {value} 40 | 41 | ); 42 | }; 43 | 44 | export default connect( 45 | state => ({ settings: state.config?.theme?.modules?.input || {} }) 46 | )(Input); 47 | -------------------------------------------------------------------------------- /src/components/monitor.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | 3 | import { Newline, Spacer, measureElement } from 'ink'; 4 | 5 | import Box from './base/box'; 6 | import Text from './base/text'; 7 | import Heading from './base/heading'; 8 | import Timestamp from './monitor/timestamp'; 9 | import Direction from './monitor/direction'; 10 | 11 | import { connect } from 'react-redux/lib/alternate-renderers'; 12 | import { setMessageCount } from '../redux/actions/messages'; 13 | 14 | const Monitor = ({ messages, colors, setMessageCount }) => { 15 | const ref = useRef(); 16 | 17 | useEffect(() => { 18 | const { height } = measureElement(ref.current); 19 | setMessageCount(Math.max(height, 1)); 20 | }, [setMessageCount]); 21 | 22 | return ( 23 | 24 | Monitor 25 | 26 | 27 | 28 | {messages.map((message, index) => ( 29 | 30 | 31 | 32 | {message.text} 33 | {index === messages.length - 1 ? '' : } 34 | 35 | ))} 36 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | export default connect( 43 | state => ({ 44 | messages: state.messages.output, 45 | colors: state.config.theme?.colors?.monitor || {} 46 | }), 47 | { setMessageCount } 48 | )(Monitor); 49 | -------------------------------------------------------------------------------- /src/components/monitor/direction.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Text from '../base/text'; 3 | import {connect} from "react-redux/lib/alternate-renderers"; 4 | 5 | const Direction = ({ sent, characters, colors }) => sent ? 6 | {characters.sent} : 7 | {characters.received} ; 8 | 9 | export default connect( 10 | state => ({ 11 | characters: state.config?.theme?.modules?.monitor?.direction || {}, 12 | colors: state.config.theme?.colors?.monitor?.direction || {} 13 | }) 14 | )(Direction); 15 | -------------------------------------------------------------------------------- /src/components/monitor/timestamp.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Text from '../base/text'; 3 | 4 | import { connect } from 'react-redux/lib/alternate-renderers'; 5 | 6 | const Timestamp = ({ monitor, colors, time, ...props }) => { 7 | if (!monitor.timestamp) { 8 | return null; 9 | } 10 | 11 | return [{time.toLocaleTimeString(monitor.timestampLocale)}]; 12 | }; 13 | 14 | export default connect( 15 | state => ({ 16 | monitor: state.config?.theme?.modules?.monitor || {}, 17 | colors: state.config.theme?.colors?.monitor || {} 18 | }) 19 | )(Timestamp); 20 | -------------------------------------------------------------------------------- /src/components/variables.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Box from './base/box'; 4 | import Heading from './base/heading'; 5 | 6 | import Variable from './variables/variable'; 7 | import Range from './variables/range'; 8 | import Flag from './variables/flag'; 9 | 10 | import { connect } from 'react-redux/lib/alternate-renderers'; 11 | 12 | const variableComponent = type => ({ 13 | 0: Variable, 14 | 1: Range, 15 | 2: Flag 16 | }[type] || Variable); 17 | 18 | const Variables = ({ variables = {}, settings }) => { 19 | if (!settings.enabled) { 20 | return null; 21 | } 22 | 23 | return ( 24 | 25 | Variables 26 | 27 | {Object.entries(variables).map(([name, variable]) => { 28 | const Component = variableComponent(variable.type); 29 | 30 | return ; 31 | })} 32 | 33 | 34 | ); 35 | }; 36 | 37 | export default connect( 38 | state => ({ 39 | variables: state.variables.output, 40 | settings: state.config?.theme?.modules?.variables || {} 41 | }) 42 | )(Variables); 43 | -------------------------------------------------------------------------------- /src/components/variables/base/gauge.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Text from '../../base/text'; 4 | import Box from '../../base/box'; 5 | import { connect } from 'react-redux/lib/alternate-renderers'; 6 | 7 | const clamp = (x, min, max) => Math.min(Math.max(x, min), max); 8 | 9 | const Gauge = ({ value, min, max, length = 20, characters, colors }) => { 10 | const completed = Math.round(length * value / (max - min)); 11 | 12 | return ( 13 | 14 | {min}| 15 | {(characters.fill || '▇').repeat(clamp(completed, 0, length))} 16 | {(characters.empty || '-').repeat(clamp(length - completed, 0, length))} 17 | |{max} 18 | 19 | ); 20 | }; 21 | 22 | export default connect( 23 | state => ({ 24 | characters: state.config?.theme?.modules?.variables?.range || {}, 25 | colors: state.config?.theme?.colors?.variables?.range || {} 26 | }) 27 | )(Gauge); 28 | 29 | -------------------------------------------------------------------------------- /src/components/variables/base/indicator.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Text from '../../base/text'; 4 | import { connect } from 'react-redux/lib/alternate-renderers'; 5 | 6 | const Indicator = ({ value, colors, characters }) => ( 7 | [{value ? characters.on : characters.off}] 8 | ); 9 | 10 | export default connect( 11 | state => ({ 12 | characters: state.config?.theme?.modules?.variables?.flag || {}, 13 | colors: state.config?.theme?.colors?.variables?.flag || {} 14 | }) 15 | )(Indicator); 16 | 17 | -------------------------------------------------------------------------------- /src/components/variables/base/name.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Box from '../../base/box'; 4 | import Text from '../../base/text'; 5 | import { connect } from 'react-redux/lib/alternate-renderers'; 6 | 7 | const VariableName = ({ name, theme }) => ( 8 | 9 | {name} 10 | : 11 | 12 | ); 13 | 14 | export default connect( 15 | state => ({ theme: state.config?.theme || {} }) 16 | )(VariableName); 17 | -------------------------------------------------------------------------------- /src/components/variables/base/value.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Text from '../../base/text'; 4 | import { connect } from 'react-redux/lib/alternate-renderers'; 5 | 6 | const VariableValue = ({ value, theme }) => {value}; 7 | 8 | export default connect( 9 | state => ({ theme: state.config?.theme || {} }) 10 | )(VariableValue); 11 | -------------------------------------------------------------------------------- /src/components/variables/flag.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Box from '../base/box'; 4 | import Indicator from './base/indicator'; 5 | import VariableName from './base/name'; 6 | import VariableValue from './base/value'; 7 | 8 | const Flag = React.memo(({ variable }) => { 9 | const value = Boolean(Number.parseInt(variable.value, 10)); 10 | 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | }); 19 | 20 | export default Flag; 21 | -------------------------------------------------------------------------------- /src/components/variables/range.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Box from '../base/box'; 4 | import Text from '../base/text'; 5 | import VariableName from './base/name'; 6 | import VariableValue from './base/value'; 7 | import Gauge from './base/gauge'; 8 | 9 | const Range = React.memo(({ variable }) => { 10 | const [min, max] = variable.options.map(a => Number.parseFloat(a)); 11 | const value = Number.parseFloat(variable.value); 12 | 13 | return ( 14 | 15 | 16 | 17 | () 18 | 19 | ); 20 | }); 21 | 22 | export default Range; 23 | -------------------------------------------------------------------------------- /src/components/variables/variable.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Box from '../base/box'; 4 | import VariableName from './base/name'; 5 | import VariableValue from './base/value'; 6 | 7 | const Variable = React.memo(({ variable }) => ( 8 | 9 | 10 | 11 | 12 | )); 13 | 14 | export default Variable; 15 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import React from 'react'; 3 | import { render } from 'ink'; 4 | 5 | import { Provider } from 'react-redux/lib/alternate-renderers'; 6 | import store from './redux/store'; 7 | import loadUserConfig from './utils/config'; 8 | 9 | import App from './app'; 10 | import cli from './utils/cli'; 11 | 12 | import Theme from './theme'; 13 | 14 | const userConfig = loadUserConfig(); 15 | const theme = Theme.load(cli.flags.theme, userConfig.theme || {}); 16 | const config = Object.assign(userConfig, cli.flags, { theme }); 17 | 18 | const ArduinoCereal = ({ config }) => ( 19 | 20 | 21 | 22 | ); 23 | 24 | render(); 25 | -------------------------------------------------------------------------------- /src/redux/actions/config.js: -------------------------------------------------------------------------------- 1 | import * as types from './types'; 2 | 3 | const setConfig = config => ({ 4 | type: types.SET_CONFIG, 5 | config 6 | }); 7 | 8 | export { setConfig }; 9 | -------------------------------------------------------------------------------- /src/redux/actions/messages.js: -------------------------------------------------------------------------------- 1 | import * as types from './types'; 2 | 3 | const setMessageCount = count => ({ 4 | type: types.SET_MESSAGE_COUNT, 5 | count 6 | }); 7 | 8 | const addMessage = (message, sent = false, time = new Date()) => ({ 9 | type: types.ADD_MESSAGE, 10 | message, 11 | sent, 12 | time, 13 | }); 14 | 15 | const outputMessages = () => ({ 16 | type: types.OUTPUT_MESSAGES 17 | }); 18 | 19 | export { setMessageCount, addMessage, outputMessages }; 20 | -------------------------------------------------------------------------------- /src/redux/actions/serial.js: -------------------------------------------------------------------------------- 1 | import * as types from './types'; 2 | 3 | const setSerialStatus = status => ({ 4 | type: types.SET_SERIAL_STATUS, 5 | status 6 | }); 7 | 8 | export { setSerialStatus }; 9 | -------------------------------------------------------------------------------- /src/redux/actions/types.js: -------------------------------------------------------------------------------- 1 | export const SET_CONFIG = 'SET_CONFIG'; 2 | 3 | export const SET_SERIAL_STATUS = 'SET_SERIAL_STATUS'; 4 | 5 | export const UPDATE_VARIABLE = 'UPDATE_VARIABLE'; 6 | export const OUTPUT_VARIABLES = 'OUTPUT_VARIABLES'; 7 | 8 | export const ADD_MESSAGE = 'ADD_MESSAGE'; 9 | export const OUTPUT_MESSAGES = 'OUTPUT_MESSAGES'; 10 | export const SET_MESSAGE_COUNT = 'SET_MESSAGE_COUNT'; 11 | -------------------------------------------------------------------------------- /src/redux/actions/variables.js: -------------------------------------------------------------------------------- 1 | import * as types from './types'; 2 | 3 | const updateVariable = (type, name, value, options) => ({ 4 | type: types.UPDATE_VARIABLE, 5 | name, 6 | variable: { name, type, value, options } 7 | }); 8 | 9 | const outputVariables = () => ({ 10 | type: types.OUTPUT_VARIABLES 11 | }); 12 | 13 | export { updateVariable, outputVariables }; 14 | -------------------------------------------------------------------------------- /src/redux/reducers/config.js: -------------------------------------------------------------------------------- 1 | import * as types from '../actions/types'; 2 | 3 | const initial = { 4 | baud: 9600, 5 | port: undefined, 6 | fps: 15, 7 | theme: {} 8 | }; 9 | 10 | export default (state = initial, action) => { 11 | switch (action.type) { 12 | case types.SET_CONFIG: 13 | return { 14 | ...state, 15 | ...action.config 16 | }; 17 | default: 18 | return state; 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/redux/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import config from './config'; 4 | import messages from './messages'; 5 | import serial from './serial'; 6 | import variables from './variables'; 7 | 8 | export default combineReducers({ 9 | config, 10 | messages, 11 | serial, 12 | variables 13 | }); 14 | -------------------------------------------------------------------------------- /src/redux/reducers/messages.js: -------------------------------------------------------------------------------- 1 | import * as types from '../actions/types'; 2 | 3 | const initial = { 4 | count: 10, 5 | delimiter: '\r\n', 6 | raw: [], 7 | output: [], 8 | terminated: true, 9 | dirty: false 10 | }; 11 | 12 | export default (state = initial, action) => { 13 | switch (action.type) { 14 | case types.ADD_MESSAGE: { 15 | if (action.message.length === 0) { 16 | return state; 17 | } 18 | 19 | const { message, time, sent } = action; 20 | const prefix = state.terminated ? '' : (state.raw.pop()?.text || ''); 21 | const messages = (prefix + message).split(state.delimiter).map(text => ({ text, time, sent })); 22 | const terminated = message.endsWith(state.delimiter); 23 | 24 | terminated && messages.pop(); 25 | 26 | return { 27 | ...state, 28 | terminated, 29 | raw: [...state.raw, ...messages].slice(-state.count), 30 | dirty: true 31 | }; 32 | } 33 | 34 | case types.OUTPUT_MESSAGES: 35 | if (!state.dirty) { 36 | return state; 37 | } 38 | 39 | return { 40 | ...state, 41 | output: [...state.raw] 42 | }; 43 | case types.SET_MESSAGE_COUNT: 44 | return { 45 | ...state, 46 | count: action.count 47 | }; 48 | default: 49 | return state; 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /src/redux/reducers/serial.js: -------------------------------------------------------------------------------- 1 | import * as types from '../actions/types'; 2 | 3 | const initial = { 4 | status: 'pending' 5 | }; 6 | 7 | export default (state = initial, action) => { 8 | switch (action.type) { 9 | case types.SET_SERIAL_STATUS: 10 | return { 11 | ...state, 12 | status: action.status 13 | }; 14 | default: 15 | return state; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /src/redux/reducers/variables.js: -------------------------------------------------------------------------------- 1 | import * as types from '../actions/types'; 2 | 3 | const initial = { 4 | raw: {}, 5 | output: {}, 6 | dirty: false 7 | }; 8 | 9 | export default (state = initial, action) => { 10 | switch (action.type) { 11 | case types.UPDATE_VARIABLE: { 12 | const { variable } = action; 13 | if (state.raw[action.name]?.value === variable.value) { 14 | return state; 15 | } 16 | 17 | return { 18 | ...state, 19 | raw: { 20 | ...state.raw, 21 | ...{ [action.name]: variable } 22 | }, 23 | dirty: true 24 | }; 25 | } 26 | 27 | case types.OUTPUT_VARIABLES: 28 | if (!state.dirty) { 29 | return state; 30 | } 31 | 32 | return { 33 | ...state, 34 | output: { ...state.raw } 35 | }; 36 | default: 37 | return state; 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/redux/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'redux'; 2 | import reducers from '../reducers'; 3 | 4 | export default createStore(reducers); 5 | -------------------------------------------------------------------------------- /src/serial/index.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'events'; 2 | 3 | import SerialPort from 'serialport'; 4 | import CerealParser from './parser'; 5 | 6 | export default class Serial extends EventEmitter { 7 | constructor(stream) { 8 | super(); 9 | 10 | this.stream = stream; 11 | this.stream.pipe(new CerealParser()).on('data', this._data.bind(this)); 12 | this.stream.open(this._open.bind(this)); 13 | } 14 | 15 | write(data) { 16 | return this.stream.write(data) 17 | } 18 | 19 | _open(error) { 20 | error === null ? this.emit('connect') : this.emit('error', error); 21 | } 22 | 23 | _data(data) { 24 | this.emit(data.variable ? 'variable' : 'message', data.value); 25 | } 26 | 27 | static connect(port, baud) { 28 | return new this(new SerialPort(port, { baudRate: baud, autoOpen: false })); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/serial/parser.js: -------------------------------------------------------------------------------- 1 | import { Transform } from 'stream'; 2 | 3 | export default class CerealParser extends Transform { 4 | constructor(options = {}) { 5 | options.objectMode = true; 6 | 7 | super(options); 8 | 9 | this.delimiter = Buffer.from('c0', 'hex'); 10 | this.valueDelimiter = Buffer.from('c1', 'hex'); 11 | this.buffer = Buffer.alloc(0); 12 | this.varaible = false; 13 | } 14 | 15 | _transform(chunk, encoding, cb) { 16 | this.buffer = Buffer.concat([this.buffer, chunk]); 17 | 18 | let position; 19 | while ((position = this.buffer.indexOf(this.delimiter)) !== -1) { 20 | const value = this.buffer.slice(0, position).toString('utf-8'); 21 | if (value.length > 0) { 22 | this.push({ 23 | variable: this.varaible, 24 | value: this.varaible ? value.split(this.valueDelimiter) : value 25 | }); 26 | } 27 | 28 | this.varaible = !this.varaible; 29 | this.buffer = this.buffer.slice(position + this.delimiter.length); 30 | } 31 | 32 | if (!this.varaible) { 33 | this._flushBuffer(); 34 | } 35 | 36 | cb(); 37 | } 38 | 39 | _flush(cb) { 40 | this._flushBuffer(); 41 | cb(); 42 | } 43 | 44 | _flushBuffer() { 45 | this.push({ variable: false, value: this.buffer.toString('utf-8') }); 46 | this.buffer = Buffer.alloc(0); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/theme/index.js: -------------------------------------------------------------------------------- 1 | import merge from 'lodash.merge'; 2 | 3 | import light from './presets/light'; 4 | import dark from './presets/dark'; 5 | import black from './presets/black'; 6 | import white from './presets/white'; 7 | 8 | const presets = { light, dark, black, white }; 9 | export default class Theme { 10 | static load(name, overrides) { 11 | return merge(presets[name] || presets.dark, overrides); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/theme/presets/black.js: -------------------------------------------------------------------------------- 1 | export default { 2 | modules: { 3 | monitor: { 4 | timestamp: true, 5 | timestampLocale: 'en-GB', 6 | 7 | direction: { 8 | received: '<<', 9 | sent: '>>' 10 | } 11 | }, 12 | 13 | variables: { 14 | enabled: true, 15 | range: { 16 | fill: '▇', 17 | empty: '-' 18 | }, 19 | flag: { 20 | on: '▣', 21 | off: '▢' 22 | } 23 | }, 24 | 25 | input: { 26 | enabled: true, 27 | prefix: '$>' 28 | } 29 | }, 30 | 31 | colors: { 32 | default: '#000', 33 | 34 | global: { 35 | heading: '#000', 36 | border: '#000' 37 | }, 38 | 39 | status: { 40 | success: '#0f0', 41 | error: '#f00' 42 | }, 43 | 44 | monitor: { 45 | timestamp: '#000', 46 | message: '#000', 47 | 48 | direction: { 49 | received: '#000', 50 | sent: '#000' 51 | } 52 | }, 53 | 54 | variables: { 55 | name: '#000', 56 | value: '#000', 57 | 58 | range: { 59 | fill: '#000', 60 | empty: '#000' 61 | }, 62 | flag: { 63 | on: '#000', 64 | off: '#000' 65 | } 66 | } 67 | } 68 | }; 69 | -------------------------------------------------------------------------------- /src/theme/presets/dark.js: -------------------------------------------------------------------------------- 1 | export default { 2 | modules: { 3 | monitor: { 4 | timestamp: true, 5 | timestampLocale: 'en-GB', 6 | 7 | direction: { 8 | received: '<<', 9 | sent: '>>' 10 | } 11 | }, 12 | 13 | variables: { 14 | enabled: true, 15 | range: { 16 | fill: '▇', 17 | empty: '-' 18 | }, 19 | flag: { 20 | on: '▣', 21 | off: '▢' 22 | } 23 | }, 24 | 25 | input: { 26 | enabled: true, 27 | prefix: '$>' 28 | } 29 | }, 30 | 31 | colors: { 32 | default: '#839496', 33 | 34 | global: { 35 | heading: '#b58900', 36 | border: '#268bd2' 37 | }, 38 | 39 | status: { 40 | success: '#859900', 41 | error: '#dc322f' 42 | }, 43 | 44 | monitor: { 45 | timestamp: '#b58900', 46 | message: '#839496', 47 | 48 | direction: { 49 | received: '#b58900', 50 | sent: '#268bd2' 51 | } 52 | }, 53 | 54 | variables: { 55 | name: '#839496', 56 | value: '#839496', 57 | 58 | range: { 59 | fill: '#b58900', 60 | empty: '#839496' 61 | }, 62 | flag: { 63 | on: '#b58900', 64 | off: '#b58900' 65 | } 66 | } 67 | } 68 | }; 69 | -------------------------------------------------------------------------------- /src/theme/presets/light.js: -------------------------------------------------------------------------------- 1 | export default { 2 | modules: { 3 | monitor: { 4 | timestamp: true, 5 | timestampLocale: 'en-GB', 6 | 7 | direction: { 8 | received: '<<', 9 | sent: '>>' 10 | } 11 | }, 12 | 13 | variables: { 14 | enabled: true, 15 | range: { 16 | fill: '▇', 17 | empty: '-' 18 | }, 19 | flag: { 20 | on: '▣', 21 | off: '▢' 22 | } 23 | }, 24 | 25 | input: { 26 | enabled: true, 27 | prefix: '$>' 28 | } 29 | }, 30 | 31 | colors: { 32 | default: '#222222', 33 | 34 | global: { 35 | heading: '#876600', 36 | border: '#104061' 37 | }, 38 | 39 | status: { 40 | success: '#859900', 41 | error: '#dc322f' 42 | }, 43 | 44 | monitor: { 45 | timestamp: '#876600', 46 | message: '#222222', 47 | 48 | direction: { 49 | received: '#876600', 50 | sent: '#104061' 51 | } 52 | }, 53 | 54 | variables: { 55 | name: '#222222', 56 | value: '#876600', 57 | 58 | range: { 59 | fill: '#876600', 60 | empty: '#104061' 61 | }, 62 | flag: { 63 | on: '#104061', 64 | off: '#222222' 65 | } 66 | } 67 | } 68 | }; 69 | -------------------------------------------------------------------------------- /src/theme/presets/white.js: -------------------------------------------------------------------------------- 1 | export default { 2 | modules: { 3 | monitor: { 4 | timestamp: true, 5 | timestampLocale: 'en-GB', 6 | 7 | direction: { 8 | received: '<<', 9 | sent: '>>' 10 | } 11 | }, 12 | 13 | variables: { 14 | enabled: true, 15 | range: { 16 | fill: '▇', 17 | empty: '-' 18 | }, 19 | flag: { 20 | on: '▣', 21 | off: '▢' 22 | } 23 | }, 24 | 25 | input: { 26 | enabled: true, 27 | prefix: '$>' 28 | } 29 | }, 30 | 31 | colors: { 32 | default: '#fff', 33 | 34 | global: { 35 | heading: '#fff', 36 | border: '#fff' 37 | }, 38 | 39 | status: { 40 | success: '#0f0', 41 | error: '#f00' 42 | }, 43 | 44 | monitor: { 45 | timestamp: '#fff', 46 | message: '#fff', 47 | 48 | direction: { 49 | received: '#fff', 50 | sent: '#fff' 51 | } 52 | }, 53 | 54 | variables: { 55 | name: '#fff', 56 | value: '#fff', 57 | 58 | range: { 59 | fill: '#fff', 60 | empty: '#fff' 61 | }, 62 | flag: { 63 | on: '#fff', 64 | off: '#fff' 65 | } 66 | } 67 | } 68 | }; 69 | -------------------------------------------------------------------------------- /src/utils/cli.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import meow from 'meow'; 4 | 5 | export default meow(` 6 | Usage 7 | $ arduino-cereal 8 | 9 | Options 10 | --port Serial port 11 | --baud Baud rate (default: 9600) 12 | --theme Theme name (light, dark, black, white) 13 | --fps Screen render rate (default: 15) 14 | `); 15 | -------------------------------------------------------------------------------- /src/utils/config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | 4 | export default () => { 5 | const configPath = path.join(process.cwd(), './cereal.config.json'); 6 | return fs.existsSync(configPath) ? require(configPath) : {}; 7 | }; 8 | -------------------------------------------------------------------------------- /src/utils/use-init.js: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | 3 | export default (cb = () => {}) => { 4 | const called = useRef(false); 5 | if (called.current) { 6 | return; 7 | } 8 | 9 | cb(); 10 | called.current = true; 11 | }; 12 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import chalk from 'chalk'; 3 | import test from 'ava'; 4 | import {render} from 'ink-testing-library'; 5 | import App from '../src/app'; 6 | 7 | test('greet unknown user', t => { 8 | const {lastFrame} = render(); 9 | 10 | t.is(lastFrame(), chalk`Baud rate: {green 9600}`); 11 | }); 12 | 13 | test('greet user with a name', t => { 14 | const {lastFrame} = render(); 15 | 16 | t.is(lastFrame(), chalk`Baud rate: {green 19200}`); 17 | }); 18 | --------------------------------------------------------------------------------