import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { StyleSheet } from 'aphrodite/no-important'; import css from '../utils/css'; import SignalLegend from './SignalLegend'; import Signal from '../models/can/signal'; import { shade } from '../utils/color'; import DbcUtils from '../utils/dbc'; /* AddSignals component draws an 8x8 matrix representing the bytes in a CAN message, alongside a signal legend. Dragging on the matrix either extends or creates a signal, which is configurable in the legend. */ const Styles = StyleSheet.create({ bit: { margin: 0, padding: 9, userSelect: 'none', cursor: 'pointer', textAlign: 'center', position: 'relative' }, bitSelectedStyle: { backgroundColor: 'rgba(0,119,158,0.5)' }, byteIndex: { backgroundColor: '#ededed' }, bitSignificance: { fontSize: 12, display: 'block', position: 'absolute', bottom: 0, left: 0, right: 0, margin: '0 auto' }, highlightedSignalTitle: { backgroundColor: 'rgba(0,0,0,0.2)' } }); export default class AddSignals extends Component { static propTypes = { message: PropTypes.object, onConfirmedSignalChange: PropTypes.func, messageIndex: PropTypes.number, onSignalPlotChange: PropTypes.func, plottedSignalUids: PropTypes.array, selectedMessageKey: PropTypes.string }; constructor(props) { super(props); let signals = {}; if (props.message && props.message.frame && props.message.frame.signals) { signals = this.copySignals(props.message.frame.signals); } this.state = { bits: [], signals, signalStyles: this.calcSignalStyles(signals), highlightedSignal: null, dragStartBit: null, dragSignal: null, dragCurrentBit: null, maxMessageBytes: 8, }; } copySignals(signals) { return Object.entries(signals).reduce( (signalsCopy, [signalName, signal]) => { signalsCopy[signalName] = Object.assign(Object.create(signal), signal); return signalsCopy; }, {} ); } componentDidMount() { this.componentDidUpdate({}, {}); } componentDidUpdate(prevProps) { if (!prevProps.message || prevProps.message.address !== this.props.message.address || prevProps.selectedMessageKey !== this.props.selectedMessageKey) { const signals = this.props.message.frame ? this.props.message.frame.signals : {}; this.setState({ signals: this.copySignals(signals), maxMessageBytes: DbcUtils.maxMessageSize(this.props.message) }, this.updateSignalStyles); } } shouldComponentUpdate(nextProps, nextState) { return ( nextProps.selectedMessageKey !== this.props.selectedMessageKey || nextProps.message.address !== this.props.message.address || nextProps.message.hexData !== this.props.message.hexData || nextProps.messageIndex !== this.props.messageIndex || JSON.stringify(nextProps.plottedSignalUids) !== JSON.stringify(this.props.plottedSignalUids) || JSON.stringify(this.state) !== JSON.stringify(nextState) ); } signalColorStyle(signal) { const colors = signal.getColors(this.props.selectedMessageKey); let colorRgbStr; let backgroundColor; if (this.state && this.state.highlightedSignal === signal.name) { // when signal highlighted, // darkened background and lightened text. const darkenedColors = shade(colors, -0.5); const lightenedColors = shade(colors, 0.9); colorRgbStr = `rgb(${lightenedColors.join(',')})`; backgroundColor = `rgba(${darkenedColors.join(',')},0.5)`; } else { const colorsCommaSep = colors.join(','); colorRgbStr = `rgb(${colorsCommaSep})`; backgroundColor = `rgba(${colorsCommaSep},0.2)`; } const style = StyleSheet.create({ signal: { color: colorRgbStr, backgroundColor } }).signal; return style; } updateSignalStyles = () => { const signalStyles = this.calcSignalStyles(this.state.signals); this.setState({ signalStyles }); }; calcSignalStyles(signals) { const signalStyles = {}; Object.values(signals).forEach((signal) => { signalStyles[signal.name] = this.signalColorStyle(signal); }); return signalStyles; } signalForBit(bitIdx) { // bitIdx in [0,64) // returns instance of Signal return Object.values(this.state.signals).filter( (signal) => signal.bitDescription(bitIdx) !== null )[0]; } onSignalHover = (signal) => { if (!signal) return; this.setState({ highlightedSignal: signal.name }, this.updateSignalStyles); }; signalBitIndex(bitIdx, signal) { // todo does this work for both big and little endian? let { startBit } = signal; if (!signal.isLittleEndian) { startBit = DbcUtils.bigEndianBitIndex(startBit); } return bitIdx - startBit; } onBitHover = (bitIdx, signal) => { let { dragStartBit, signals, dragSignal } = this.state; if (dragStartBit !== null) { if (dragSignal !== null) { signals = this.copySignals(signals); dragSignal = Object.assign(Object.create(dragSignal), dragSignal); if (dragStartBit === dragSignal.startBit && dragSignal.size > 1) { if (!dragSignal.isLittleEndian) { // should not be able to drag the msb past the lsb const hoveredBigEndian = DbcUtils.bigEndianBitIndex(bitIdx); const lsbBigEndian = dragSignal.lsbBitNumber(); if (hoveredBigEndian > lsbBigEndian) { return; } } else { // should not be able to drag the lsb past the msb if (bitIdx > dragSignal.msbBitIndex()) { return; } } const diff = bitIdx - dragStartBit; if (dragSignal.isLittleEndian) { dragSignal.size -= diff; } else if (dragSignal.bitDescription(bitIdx) === null) { dragSignal.size += Math.abs(diff); } else { dragSignal.size -= Math.abs(diff); } dragSignal.startBit += diff; signals[dragSignal.name] = dragSignal; dragStartBit = dragSignal.startBit; } else if (dragSignal.size === 1) { // 1-bit signals can be dragged in either direction if (Math.floor(bitIdx / 8) === Math.floor(dragStartBit / 8)) { if (bitIdx > dragStartBit) { if (dragSignal.isLittleEndian) { dragSignal.size = bitIdx - dragSignal.startBit; } else { dragSignal.startBit = bitIdx; dragSignal.size = bitIdx - dragStartBit + 1; dragStartBit = bitIdx; } } else if (dragSignal.isLittleEndian) { dragSignal.startBit = bitIdx; dragSignal.size = dragStartBit - bitIdx + 1; dragStartBit = bitIdx; } else { dragSignal.size = dragStartBit - bitIdx + 1; dragStartBit = bitIdx; } } signals[dragSignal.name] = dragSignal; } else if ( dragSignal.isLittleEndian && dragStartBit === dragSignal.msbBitIndex() ) { if (bitIdx < dragSignal.startBit) { // should not be able to drag the MSB past the LSB return; } const diff = bitIdx - dragStartBit; if (dragSignal.bitDescription(bitIdx) === null) { dragSignal.size += Math.abs(diff); } else { dragSignal.size -= Math.abs(diff); } signals[dragSignal.name] = dragSignal; dragStartBit = dragSignal.msbBitIndex(); } else if ( !dragSignal.isLittleEndian && dragStartBit === dragSignal.lsbBitIndex() ) { const diff = bitIdx - dragStartBit; if (dragSignal.bitDescription(bitIdx) === null) { dragSignal.size += Math.abs(diff); } else { dragSignal.size -= Math.abs(diff); } signals[dragSignal.name] = dragSignal; dragStartBit = dragSignal.lsbBitIndex(); } this.setState({ signals, dragSignal, dragCurrentBit: bitIdx, dragStartBit }); } else { this.setState({ dragCurrentBit: bitIdx }); } } if (signal) { this.onSignalHover(signal); } }; onSignalHoverEnd = (signal) => { if (!signal) return; this.setState({ highlightedSignal: null }, this.updateSignalStyles); }; nextNewSignalName() { const existingNames = Object.keys(this.state.signals); let signalNum = 1; let signalName; do { signalName = `NEW_SIGNAL_${signalNum}`; signalNum++; } while (existingNames.indexOf(signalName) !== -1); return signalName; } onBitMouseDown(dragStartBit, dragSignal) { this.setState({ dragStartBit, dragSignal: dragSignal || null }); } createSignal({ startBit, size, isLittleEndian }) { const signal = new Signal({ name: this.nextNewSignalName(), startBit, size, isLittleEndian }); let { signals } = this.state; signals = { ...signals }; signals[signal.name] = signal; this.setState({ signals }, this.propagateUpSignalChange); } createSignalIfNotExtendingOne(dragStartBit, dragEndBit) { if (this.state.dragSignal === null) { // check for overlapping bits for (let i = dragStartBit; i <= dragEndBit; i++) { if (this.signalForBit(i) !== undefined) { // Don't create signal if a signal is already defined in the selected range. return; } } const isDragAcrossSingleByte = Math.floor(dragEndBit / 8) === Math.floor(dragStartBit / 8); const isDragDirectionUp = !isDragAcrossSingleByte && dragEndBit < dragStartBit; let isLittleEndian; if (isDragAcrossSingleByte || !isDragDirectionUp) { isLittleEndian = dragStartBit % 8 < 4; } else { isLittleEndian = dragStartBit % 8 >= 4; } let size; let startBit = dragStartBit; if (isDragAcrossSingleByte) { size = Math.abs(dragEndBit - dragStartBit) + 1; } else if (isLittleEndian) { if (dragEndBit > dragStartBit) { startBit = dragStartBit; size = dragEndBit - dragStartBit + 1; } else { startBit = dragEndBit; size = dragStartBit - dragEndBit + 1; } } else { if (dragEndBit < dragStartBit) { startBit = dragEndBit; } size = Math.abs( DbcUtils.bigEndianBitIndex(dragEndBit) - DbcUtils.bigEndianBitIndex(dragStartBit) ) + 1; } this.createSignal({ startBit, size, isLittleEndian }); } } onBitMouseUp(dragEndBit, signal) { if (this.state.dragStartBit !== null) { const { dragStartBit } = this.state; if (dragEndBit !== dragStartBit) { // one-bit signal requires double click // see onBitDoubleClick this.createSignalIfNotExtendingOne(dragStartBit, dragEndBit); } this.propagateUpSignalChange(); this.resetDragState(); } } byteValueHex(byteIdx) { const { entries } = this.props.message; if (this.props.messageIndex < entries.length) { const entry = entries[this.props.messageIndex]; return entry.hexData.substr(byteIdx * 2, 2); } return '--'; } bitValue(byteIdx, byteBitIdx) { const { entries } = this.props.message; if (this.props.messageIndex < entries.length) { const entry = entries[this.props.messageIndex]; const data = Buffer.from(entry.hexData, 'hex'); if (byteIdx >= data.length) { return '-'; } const byte = data.readInt8(byteIdx); return (byte >> byteBitIdx) & 1; } return '-'; } bitIsContainedInSelection(bitIdx, isLittleEndian = false) { const { dragStartBit, dragCurrentBit } = this.state; if (isLittleEndian || dragStartBit % 8 < 4) { return ( dragStartBit !== null && dragCurrentBit !== null && bitIdx >= dragStartBit && bitIdx <= dragCurrentBit ); } const bigEndianStartBit = DbcUtils.bigEndianBitIndex(dragStartBit); const bigEndianCurrentBit = DbcUtils.bigEndianBitIndex(dragCurrentBit); const bigEndianBitIdx = DbcUtils.bigEndianBitIndex(bitIdx); return ( dragStartBit !== null && dragCurrentBit !== null && bigEndianBitIdx >= bigEndianStartBit && bigEndianBitIdx <= bigEndianCurrentBit ); } onBitDoubleClick(startBit, signal) { if (signal === undefined) { this.createSignal({ startBit, size: 1, isLittleEndian: false }); } } renderBitMatrix() { const { message } = this.props; const rows = []; let rowCount; if (message.frame && message.frame.size) { rowCount = Math.floor((message.frame.size * 8) / 8); } else { rowCount = this.state.maxMessageBytes; } for (let i = 0; i < rowCount; i++) { const rowBits = []; rowBits.push({i.toString()}); for (let j = 7; j >= 0; j--) { const bitIdx = i * 8 + j; const signal = this.signalForBit(bitIdx); let bitStyle = null; let bitSignificance = ''; if (signal) { bitStyle = this.state.signalStyles[signal.name] || null; const bitDesc = signal.bitDescription(bitIdx); bitSignificance = bitDesc.isMsb ? 'msb' : bitDesc.isLsb ? 'lsb' : ''; } else if (this.bitIsContainedInSelection(bitIdx)) { bitStyle = Styles.bitSelectedStyle; } const className = css('bit', Styles.bit, bitStyle); const bitValue = this.bitValue(i, j); rowBits.push( this.onBitHover(bitIdx, signal)} onMouseLeave={() => this.onSignalHoverEnd(signal)} onMouseDown={this.onBitMouseDown.bind(this, bitIdx, signal)} onMouseUp={this.onBitMouseUp.bind(this, bitIdx, signal)} onDoubleClick={() => this.onBitDoubleClick(bitIdx, signal)} > {bitValue} {bitSignificance} ); } rowBits.push({this.byteValueHex(i)}); rows.push({rowBits}); } return (
{rows}
); } resetDragState = () => { this.setState({ dragStartBit: null, dragSignal: null, dragCurrentBit: null }); }; onTentativeSignalChange = (signal) => { // Tentative signal changes are not propagated up // but their effects are displayed in the bitmatrix const { signals } = this.state; signals[signal.name] = signal; this.setState({ signals }); }; onSignalChange = (signal) => { const { signals } = this.state; if (signal.name in signals && signals[signal.name].uid !== signal.uid) { return false; } for (const signalName in signals) { if (signals[signalName].uid === signal.uid) { delete signals[signalName]; } } signals[signal.name] = signal; this.setState({ signals }, this.propagateUpSignalChange); return true; }; onSignalRemove = (signal) => { const { signals } = this.state; delete signals[signal.name]; this.setState({ signals }, this.propagateUpSignalChange); }; propagateUpSignalChange() { const { signals } = this.state; this.props.onConfirmedSignalChange( this.props.message, this.copySignals(signals) ); } onSignalPlotChange = (shouldPlot, signalUid) => { const { message } = this.props; this.props.onSignalPlotChange(shouldPlot, message.id, signalUid); }; render() { const { selectedMessageKey } = this.props; return (
{Object.keys(this.state.signals).length === 0 ? (

Double click or drag to add a signal

) : null} {this.props.message.entries[this.props.messageIndex] ? (

time: {' '} {this.props.message.entries[ this.props.messageIndex ].relTime.toFixed(3)}

) : null} {this.renderBitMatrix()}
); } }