/** * */ package org.goko.controller.tinyg.commons; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.goko.controller.tinyg.commons.configuration.AbstractTinyGConfiguration; import org.goko.controller.tinyg.commons.configuration.TinyGGroupSettings; import org.goko.core.common.GkUtils; import org.goko.core.common.applicative.logging.IApplicativeLogService; import org.goko.core.common.buffer.ByteCommandBuffer; import org.goko.core.common.exception.GkException; import org.goko.core.common.measure.quantity.Angle; import org.goko.core.common.measure.quantity.AngleUnit; import org.goko.core.common.measure.quantity.Length; import org.goko.core.common.measure.quantity.Speed; import org.goko.core.common.measure.quantity.SpeedUnit; import org.goko.core.common.measure.units.Unit; import org.goko.core.connection.DataPriority; import org.goko.core.connection.EnumConnectionEvent; import org.goko.core.connection.IConnectionDataListener; import org.goko.core.connection.IConnectionListener; import org.goko.core.connection.serial.ISerialConnectionService; import org.goko.core.gcode.element.ICoordinateSystem; import org.goko.core.gcode.rs274ngcv3.context.CoordinateSystem; import org.goko.core.gcode.rs274ngcv3.context.EnumDistanceMode; import org.goko.core.gcode.rs274ngcv3.context.EnumMotionMode; import org.goko.core.gcode.rs274ngcv3.context.EnumPlane; import org.goko.core.gcode.rs274ngcv3.context.EnumUnit; import org.goko.core.log.GkLog; import org.goko.core.math.Tuple6b; import com.eclipsesource.json.JsonObject; import com.eclipsesource.json.JsonValue; /** * @author Psyko * @date 6 janv. 2017 */ public abstract class AbstractTinyGCommunicator<C extends AbstractTinyGConfiguration<C>, S extends ITinyGControllerService<C>> implements IConnectionDataListener, IConnectionListener{ /** LOG */ private static final GkLog LOG = GkLog.getLogger(AbstractTinyGCommunicator.class); /** The connection service */ private ISerialConnectionService connectionService; /** Buffer for incoming data */ private ByteCommandBuffer incomingBuffer; /** End line characters */ private List<Character> endLineCharacters; /** Connected flag */ private boolean connected; /** The running TinyG service*/ private S controllerService; /** UI Log service */ private IApplicativeLogService applicativeLogService; /** * Constructor */ public AbstractTinyGCommunicator() { this.incomingBuffer = new ByteCommandBuffer((byte) '\n'); this.endLineCharacters = new ArrayList<Character>(); setEndLineCharacters('\n'); } /** (inheritDoc) * @see org.goko.core.connection.IConnectionListener#onConnectionEvent(org.goko.core.connection.EnumConnectionEvent) */ @Override public final void onConnectionEvent(EnumConnectionEvent event) throws GkException { if(event == EnumConnectionEvent.CONNECTED){ connected = true; onConnected(); }else if(event == EnumConnectionEvent.DISCONNECTED){ connected = false; onDisconnected(); } } /** (inheritDoc) * @see org.goko.core.connection.IConnectionDataListener#onDataReceived(java.util.List) */ @Override public final void onDataReceived(List<Byte> data) throws GkException { incomingBuffer.addAll(data); while(incomingBuffer.hasNext()){ try { String next = GkUtils.toString(incomingBuffer.unstackNextCommand()); handleIncomingData(next); } catch (GkException e) { LOG.error(e); } } } /** (inheritDoc) * @see org.goko.core.connection.IConnectionDataListener#onDataSent(java.util.List) */ @Override public final void onDataSent(List<Byte> data) throws GkException { } /** * @return the connectionService */ public ISerialConnectionService getConnectionService() { return connectionService; } /** * @param connectionService the connectionService to set * @throws GkException */ public void setConnectionService(ISerialConnectionService connectionService) throws GkException { if(this.connectionService != null){ this.connectionService.removeConnectionListener(this); this.connectionService.removeInputDataListener(this); this.connectionService.removeOutputDataListener(this); } this.connectionService = connectionService; connectionService.addConnectionListener(this); System.err.println("Adding listener "+this); } /** * Set the end line characters * @param chars the end line characters */ public void setEndLineCharacters(char... chars){ this.endLineCharacters = Arrays.asList(ArrayUtils.toObject(chars)); this.incomingBuffer.setCommandDelimiter(chars[chars.length - 1]); } /** * Add the end line character at the end of the given list * @param list the list */ protected final void addEndLineCharacter(List<Byte> list){ for (char endChar : endLineCharacters) { list.add(new Byte((byte) endChar)); } } /** * Sends the given JsonValue over the connection service * @param data the JsonValue to send * @param useEndLineCharacter <code>true</code> to append end line characters before sending, <code>false</code> otherwise * @throws GkException GkException */ public final void send(JsonValue data, boolean useEndLineCharacter) throws GkException{ send(data.toString(), useEndLineCharacter); } /** * Sends the given string as a list of byte over the connection service * @param data the String to send * @param useEndLineCharacter <code>true</code> to append end line characters before sending, <code>false</code> otherwise * @throws GkException GkException */ public final void send(String data, boolean useEndLineCharacter) throws GkException{ List<Byte> lstBytes = GkUtils.toBytesList(data); if(useEndLineCharacter){ addEndLineCharacter(lstBytes); } getConnectionService().send(lstBytes); } /** * Sends the given list of byte over the connection service * @param lstByte the list of byte to send * @param useEndLineCharacter <code>true</code> to append end line characters before sending, <code>false</code> otherwise * @throws GkException GkException */ public final void send(List<Byte> lstByte, boolean useEndLineCharacter) throws GkException{ if(useEndLineCharacter){ addEndLineCharacter(lstByte); } getConnectionService().send(lstByte); } /** * Sends the given GCode over the connection service * @param gcode the gcode to send * @throws GkException GkException */ public final void sendGCode(String gcode) throws GkException{ send(gcode, true); } /** * Sends the given byte over the connection service * @param lstByte the list of byte to send * @param useEndLineCharacter <code>true</code> to append end line characters before sending, <code>false</code> otherwise * @throws GkException GkException */ public final void send(byte byteCommand, boolean useEndLineCharacter) throws GkException{ List<Byte> lstByte = Arrays.asList(byteCommand); send(lstByte, useEndLineCharacter); } /** * Sends the given JSonValue over the connection service with high priority * @param data the JsonValue to send * @param useEndLineCharacter <code>true</code> to append end line characters before sending, <code>false</code> otherwise * @throws GkException GkException */ public final void sendImmediately(JsonValue data, boolean useEndLineCharacter) throws GkException{ sendImmediately(data.toString(), useEndLineCharacter); } /** * Sends the given list of byte over the connection service with high priority * @param lstByte the list of byte to send * @param useEndLineCharacter <code>true</code> to append end line characters before sending, <code>false</code> otherwise * @throws GkException GkException */ public final void sendImmediately(String data, boolean useEndLineCharacter) throws GkException{ List<Byte> lstBytes = GkUtils.toBytesList(data); if(useEndLineCharacter){ addEndLineCharacter(lstBytes); } getConnectionService().send(lstBytes, DataPriority.IMPORTANT); } /** * Sends the given list of byte over the connection service with high priority * @param lstByte the list of byte to send * @param useEndLineCharacter <code>true</code> to append end line characters before sending, <code>false</code> otherwise * @throws GkException GkException */ public final void sendImmediately(List<Byte> lstByte, boolean useEndLineCharacter) throws GkException{ if(useEndLineCharacter){ addEndLineCharacter(lstByte); } getConnectionService().send(lstByte, DataPriority.IMPORTANT); } /** * Sends the given list of byte over the connection service with high priority * @param lstByte the list of byte to send * @param useEndLineCharacter <code>true</code> to append end line characters before sending, <code>false</code> otherwise * @throws GkException GkException */ public final void sendImmediately(byte byteCommand, boolean useEndLineCharacter) throws GkException{ List<Byte> lstByte = Arrays.asList(byteCommand); sendImmediately(lstByte, useEndLineCharacter); } /** * Handdle incoming datas * @param data the received data * @throws GkException GkException */ protected void handleIncomingData(String data) throws GkException{ String trimmedData = StringUtils.trim(data); if(StringUtils.isNotEmpty(trimmedData)){ if(TinyGJsonUtils.isJsonFormat(trimmedData)){ JsonObject response = null; try{ response = JsonObject.readFrom(trimmedData); }catch(Exception e){ String msg = "Error while parsing JSon for string '"+trimmedData+"'"+System.lineSeparator()+e.getMessage(); LOG.error(msg); handleMalformedJson(trimmedData); return; } JsonValue footerBody = response.get(TinyG.FOOTER); ITinyGStatus status = null; if(footerBody != null){ status = getResponseFooter(footerBody); } JsonValue responseBody = response.get(TinyG.RESPONSE_ENVELOPE); if(responseBody != null){ handleResponse((JsonObject) responseBody, status); } JsonValue statusReport = response.get(TinyG.STATUS_REPORT); if(statusReport != null){ handleStatusReport((JsonObject) statusReport); } JsonValue queueReport = response.get(TinyG.QUEUE_REPORT); if(queueReport != null){ handleQueueReport((JsonValue)queueReport); } JsonValue errorReport = response.get(TinyG.ERROR_REPORT); if(errorReport != null){ handleErrorReport((JsonObject)errorReport); } }else{ handleNonJsonData(trimmedData); } } } protected abstract ITinyGStatus getResponseFooter(JsonValue footerBody) throws GkException; protected abstract void handleResponse(JsonObject responseBody, ITinyGStatus status) throws GkException; protected abstract void handleStatusReport(JsonObject statusReportBody) throws GkException; protected abstract void handleQueueReport(JsonValue queueReportBody) throws GkException; protected abstract void handleErrorReport(JsonObject errorReportBody) throws GkException; protected abstract void handleNonJsonData(String data) throws GkException; protected abstract void handleMalformedJson(String data); protected abstract void onConnected() throws GkException; protected abstract void onDisconnected() throws GkException; /** * @return the incomingBuffer */ public ByteCommandBuffer getIncomingBuffer() { return incomingBuffer; } /** * @return the connected */ public boolean isConnected() { return connected; } // Defaults implementations /** * Finds the units declaration in the status report * @param statusReport the status report * @return {@link EnumGCodeCommandUnit} * @throws GkException GkException */ protected EnumUnit findUnits(JsonObject statusReport) throws GkException{ JsonValue unitReport = statusReport.get(TinyG.STATUS_REPORT_UNITS); if(unitReport != null){ int units = unitReport.asInt(); if(units == 1){ return EnumUnit.MILLIMETERS; }else{ return EnumUnit.INCHES; } } return controllerService.getGCodeContext().getUnit(); } /** * Update the current work position using the given status report * @param statusReport the status report * @param unit the unit in use * @return Tuple6b * @throws GkException */ protected Tuple6b findWorkPosition(JsonObject statusReport, EnumUnit unit) throws GkException{ Tuple6b workPosition = new Tuple6b(controllerService.getGCodeContext().getPosition()); JsonValue newPositionX = statusReport.get(TinyG.STATUS_REPORT_WORK_POSITION_X); JsonValue newPositionY = statusReport.get(TinyG.STATUS_REPORT_WORK_POSITION_Y); JsonValue newPositionZ = statusReport.get(TinyG.STATUS_REPORT_WORK_POSITION_Z); JsonValue newPositionA = statusReport.get(TinyG.STATUS_REPORT_WORK_POSITION_A); if(newPositionX != null){ workPosition.setX( Length.valueOf(newPositionX.asBigDecimal(), unit.getUnit())); } if(newPositionY != null){ workPosition.setY( Length.valueOf(newPositionY.asBigDecimal() , unit.getUnit())); } if(newPositionZ != null){ workPosition.setZ( Length.valueOf(newPositionZ.asBigDecimal() , unit.getUnit())); } if(newPositionA != null){ workPosition.setA( Angle.valueOf(newPositionA.asBigDecimal() , AngleUnit.DEGREE_ANGLE)); } return workPosition; } /** * Update the current work position using the given status report * @param statusReport the status report * @param unit the unit in use * @return Tuple6b * @throws GkException */ protected Tuple6b findMachinePosition(JsonObject statusReport, EnumUnit unit) throws GkException{ Tuple6b machinePosition = new Tuple6b(controllerService.getGCodeContext().getMachinePosition()); JsonValue newPositionX = statusReport.get(TinyG.STATUS_REPORT_MACHINE_POSITION_X); JsonValue newPositionY = statusReport.get(TinyG.STATUS_REPORT_MACHINE_POSITION_Y); JsonValue newPositionZ = statusReport.get(TinyG.STATUS_REPORT_MACHINE_POSITION_Z); JsonValue newPositionA = statusReport.get(TinyG.STATUS_REPORT_MACHINE_POSITION_A); if(newPositionX != null){ machinePosition.setX( Length.valueOf(newPositionX.asBigDecimal(), unit.getUnit())); } if(newPositionY != null){ machinePosition.setY( Length.valueOf(newPositionY.asBigDecimal() , unit.getUnit())); } if(newPositionZ != null){ machinePosition.setZ( Length.valueOf(newPositionZ.asBigDecimal() , unit.getUnit())); } if(newPositionA != null){ machinePosition.setA( Angle.valueOf(newPositionA.asBigDecimal() , AngleUnit.DEGREE_ANGLE)); } return machinePosition; } /** * Finds the current distance mode from the given status report * @param statusReport the status report * @return EnumDistanceMode * @throws GkException GkException */ protected EnumDistanceMode findDistanceMode(JsonObject statusReport) throws GkException{ JsonValue distReport = statusReport.get(TinyG.STATUS_REPORT_DISTANCE_MODE); if(distReport != null){ int dist = distReport.asInt(); if(dist == 0){ return EnumDistanceMode.ABSOLUTE; }else{ return EnumDistanceMode.RELATIVE; } } return controllerService.getGCodeContext().getDistanceMode(); } /** * Finds the current velocity mode from the given status report * @param statusReport the status report * @param unit the unit in use * @return Speed * @throws GkException GkException */ protected Speed findVelocity(JsonObject statusReport, EnumUnit unit){ JsonValue velocityReport = statusReport.get(TinyG.STATUS_REPORT_VELOCITY); if(velocityReport != null){ Unit<Speed> speedUnit = SpeedUnit.MILLIMETRE_PER_MINUTE; if(EnumUnit.INCHES.equals(unit)){ speedUnit = SpeedUnit.INCH_PER_MINUTE; } return Speed.valueOf(velocityReport.asBigDecimal(), speedUnit); } return null; } /** * Finds the current feedrate mode from the given status report * @param statusReport the status report * @param unit the unit in use * @return Speed * @throws GkException GkException */ protected Speed findFeedrate(JsonObject feedrate, EnumUnit unit){ JsonValue feedrateReport = feedrate.get(TinyG.STATUS_REPORT_FEEDRATE); if(feedrateReport != null){ Unit<Speed> speedUnit = SpeedUnit.MILLIMETRE_PER_MINUTE; if(EnumUnit.INCHES.equals(unit)){ speedUnit = SpeedUnit.INCH_PER_MINUTE; } return Speed.valueOf(feedrateReport.asBigDecimal().setScale(3, BigDecimal.ROUND_HALF_EVEN), speedUnit); } return null; } /** * Finds the current coordinate system mode from the given status report * 0=g53, 1=g54, 2=g55, 3=g56, 4=g57, 5=g58, 6=g59 * @param statusReport the status report * @return EnumCoordinateSystem * @throws GkException GkException */ protected CoordinateSystem findCoordinateSystem(JsonObject statusReport){ CoordinateSystem coordinateSystem = null; JsonValue coordReport = statusReport.get(TinyG.STATUS_REPORT_COORDINATES); if(coordReport != null){ int units = coordReport.asInt(); switch(units){ case 0: coordinateSystem = CoordinateSystem.G53; break; case 1: coordinateSystem = CoordinateSystem.G54; break; case 2: coordinateSystem = CoordinateSystem.G55; break; case 3: coordinateSystem = CoordinateSystem.G56; break; case 4: coordinateSystem = CoordinateSystem.G57; break; case 5: coordinateSystem = CoordinateSystem.G58; break; case 6: coordinateSystem = CoordinateSystem.G59; break; } } return coordinateSystem; } /** * Finds the current plane from the given status report * 0=XY, 1=XZ, 2=YZ * @param statusReport the status report * @return EnumPlane * @throws GkException GkException */ protected EnumPlane findPlane(JsonObject statusReport) { EnumPlane enumPlane = null; JsonValue coordReport = statusReport.get(TinyG.STATUS_REPORT_PLANE); if(coordReport != null){ int units = coordReport.asInt(); switch(units){ case 0: enumPlane = EnumPlane.XY_PLANE; break; case 1: enumPlane = EnumPlane.XZ_PLANE; break; case 2: enumPlane = EnumPlane.YZ_PLANE; break; } } return enumPlane; } /** * Finds the current motion mode from the given status report * 0=traverse, 1=straight feed, 2=cw arc, 3=ccw arc * @param statusReport the status report * @return EnumMotionMode * @throws GkException GkException */ protected EnumMotionMode findMotionMode(JsonObject statusReport) { EnumMotionMode enumMotionMode = null; JsonValue coordReport = statusReport.get(TinyG.STATUS_REPORT_MOTION_MODE); if(coordReport != null){ int units = coordReport.asInt(); switch(units){ case 0: enumMotionMode = EnumMotionMode.RAPID; break; case 1: enumMotionMode = EnumMotionMode.FEEDRATE; break; case 2: enumMotionMode = EnumMotionMode.ARC_CLOCKWISE; break; case 3: enumMotionMode = EnumMotionMode.ARC_COUNTERCLOCKWISE; break; } } return enumMotionMode; } /** * Update the configuration by sending it through the communicator * @throws GkException GkException */ public void sendConfigurationUpdate(AbstractTinyGConfiguration<?> configuration) throws GkException { for(TinyGGroupSettings group: configuration.getGroups()){ JsonObject jsonGroup = TinyGJsonUtils.toCompleteJson(group); if(jsonGroup != null){ send( jsonGroup.toString(), true ); } } } /** * Build a JSon object using the gioven header * @param header the header * @return a JsonValue */ protected JsonValue buildJsonQuery(String header){ return buildJsonQuery(header, StringUtils.EMPTY); } /** * Build a JSon object using the given header and value * @param header the header * @return a JsonValue */ protected JsonValue buildJsonQuery(String header, String value){ JsonObject query = new JsonObject(); query.add(header, value); return query; } /** * Forces TinyG to use JSon mode * @throws GkException GkException */ public void forceJsonMode() throws GkException{ send(new JsonObject().add(TinyG.JSON_SYNTAX, TinyG.JSON_SYNTAX_STRICT), true); } /** * Sends a status report query * @throws GkException GkException */ public void requestStatusReport() throws GkException{ if(isConnected()){ send(buildJsonQuery(TinyG.STATUS_REPORT), true); } } /** * Sends a queue report query * @throws GkException GkException */ public void requestQueueReport() throws GkException{ if(isConnected()){ send(buildJsonQuery(TinyG.QUEUE_REPORT), true); } } /** * Sends a coordinate system update query * @param coordinateSystem the coordinate system to update * @throws GkException GkException */ public void requestCoordinateSystemUpdate(ICoordinateSystem coordinateSystem)throws GkException{ if(isConnected()){ send(buildJsonQuery(coordinateSystem.getCode()), true); } } /** * Sends the request for a configuration update * @throws GkException GkException */ public void requestConfigurationUpdate() throws GkException{ if(isConnected()){ C cfg = getControllerService().getConfiguration(); List<TinyGGroupSettings> lstGroups = cfg.getGroups(); if(CollectionUtils.isNotEmpty(lstGroups)){ for (TinyGGroupSettings groupSettings : lstGroups) { send(buildJsonQuery(groupSettings.getGroupIdentifier()), true); } } } } /** * Sends requests for coordinate system update * @throws GkException GkException */ public void requestCoordinateSystemUpdate() throws GkException{ send(buildJsonQuery("G55"), true); send(buildJsonQuery("G56"), true); send(buildJsonQuery("G57"), true); send(buildJsonQuery("G58"), true); send(buildJsonQuery("G59"), true); } /** * @return the controllerService */ public S getControllerService() { return controllerService; } /** * @param controllerService the controllerService to set */ public void setControllerService(S controllerService) { this.controllerService = controllerService; } /** * @return the applicativeLogService */ public IApplicativeLogService getApplicativeLogService() { return applicativeLogService; } /** * @param applicativeLogService the applicativeLogService to set */ public void setApplicativeLogService(IApplicativeLogService applicativeLogService) { this.applicativeLogService = applicativeLogService; } }