diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/RileyLinkCommunicationManager.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/RileyLinkCommunicationManager.java index a0df5790b9..52e1c5608c 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/RileyLinkCommunicationManager.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/RileyLinkCommunicationManager.java @@ -63,17 +63,33 @@ public abstract class RileyLinkCommunicationManager { // All pump communications go through this function. protected E sendAndListen(RLMessage msg, int timeout_ms, Class clazz) throws RileyLinkCommunicationException { + return sendAndListen(msg, timeout_ms, null, clazz); + } + + protected E sendAndListen(RLMessage msg, int timeout_ms, Integer extendPreamble_ms, Class clazz) + throws RileyLinkCommunicationException { + return sendAndListen(msg, timeout_ms, 0, extendPreamble_ms, clazz); + } + + // For backward compatibility + protected E sendAndListen(RLMessage msg, int timeout_ms, int repeatCount, Integer extendPreamble_ms, Class clazz) + throws RileyLinkCommunicationException { + return sendAndListen(msg, timeout_ms, repeatCount, 0, extendPreamble_ms, clazz); + } + + protected E sendAndListen(RLMessage msg, int timeout_ms, int repeatCount, int retryCount, Integer extendPreamble_ms, Class clazz) + throws RileyLinkCommunicationException { if (showPumpMessages) { if (isLogEnabled()) LOG.info("Sent:" + ByteUtil.shortHexString(msg.getTxData())); } - RFSpyResponse rfSpyResponse = rfspy.transmitThenReceive(new RadioPacket(msg.getTxData()), timeout_ms); + RFSpyResponse rfSpyResponse = rfspy.transmitThenReceive(new RadioPacket(msg.getTxData()), + (byte)0, (byte)repeatCount, (byte)0, (byte)0, timeout_ms, (byte)retryCount, extendPreamble_ms); RadioResponse radioResponse = rfSpyResponse.getRadioResponse(); - - E response = createResponseMessage(rfSpyResponse.getRadioResponse().getPayload(), clazz); + E response = createResponseMessage(radioResponse.getPayload(), clazz); if (response.isValid()) { // Mark this as the last time we heard from the pump. @@ -399,7 +415,9 @@ public abstract class RileyLinkCommunicationManager { lastGoodReceiverCommunicationTime = System.currentTimeMillis(); SP.putLong(RileyLinkConst.Prefs.LastGoodDeviceCommunicationTime, lastGoodReceiverCommunicationTime); - pumpStatus.setLastCommunicationToNow(); + if(pumpStatus != null) { + pumpStatus.setLastCommunicationToNow(); + } } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/ble/RileyLinkCommunicationException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/ble/RileyLinkCommunicationException.java index e694609571..c27de0a7e6 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/ble/RileyLinkCommunicationException.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/hw/rileylink/ble/RileyLinkCommunicationException.java @@ -7,11 +7,9 @@ import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.defs.Rile */ public class RileyLinkCommunicationException extends Exception { - String extendedErrorText; private RileyLinkBLEError errorCode; - public RileyLinkCommunicationException(RileyLinkBLEError errorCode, String extendedErrorText) { super(errorCode.getDescription()); @@ -27,4 +25,7 @@ public class RileyLinkCommunicationException extends Exception { // this.extendedErrorText = extendedErrorText; } + public RileyLinkBLEError getErrorCode() { + return errorCode; + } } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/utils/ByteUtil.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/utils/ByteUtil.java index 5451689ebe..ea42cef210 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/utils/ByteUtil.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/common/utils/ByteUtil.java @@ -1,5 +1,6 @@ package info.nightscout.androidaps.plugins.pump.common.utils; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; @@ -28,6 +29,14 @@ public class ByteUtil { return (b < 0) ? b + 256 : b; } + public static byte[] getBytesFromInt16(int value) { + byte[] array = getBytesFromInt(value); + return new byte[] {array[2], array[3]}; + } + + public static byte[] getBytesFromInt(int value) { + return ByteBuffer.allocate(4).putInt(value).array(); + } /* For Reference: static void System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length) */ diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/OmnipodManager.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/OmnipodManager.java new file mode 100644 index 0000000000..ab40f1e1a4 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/OmnipodManager.java @@ -0,0 +1,213 @@ +package info.nightscout.androidaps.plugins.pump.omnipod; + +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.joda.time.Duration; + +import java.util.EnumSet; +import java.util.TimeZone; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import info.nightscout.androidaps.data.Profile; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationService; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.AcknowledgeAlertsAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.BolusAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.CancelDeliveryAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.DeactivatePodAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.GetPodInfoAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.GetStatusAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.InsertCannulaAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.PairAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.PrimeAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.SetBasalScheduleAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.SetTempBasalAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.service.InsertCannulaService; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.service.PairService; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.service.PrimeService; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.service.SetTempBasalService; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfo; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.DeliveryType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.SetupProgress; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BasalSchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BasalScheduleMapper; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; +import info.nightscout.androidaps.utils.SP; + +public class OmnipodManager { + private final OmnipodCommunicationService communicationService; + private PodSessionState podState; + + public OmnipodManager(OmnipodCommunicationService communicationService, PodSessionState podState) { + if (communicationService == null) { + throw new IllegalArgumentException("Communication service cannot be null"); + } + this.communicationService = communicationService; + this.podState = podState; + } + + public OmnipodManager(OmnipodCommunicationService communicationService) { + this(communicationService, null); + } + + public OmnipodCommunicationService getCommunicationService() { + return communicationService; + } + + public T getPodInfo(PodInfoType podInfoType) { + if (!isInitialized()) { + throw new IllegalStateException("Pod should be initialized first"); + } + PodInfoResponse podInfoResponse = communicationService.executeAction(new GetPodInfoAction(podState, podInfoType)); + return podInfoResponse.getPodInfo(); + } + + public StatusResponse getStatus() { + if (podState == null) { + throw new IllegalStateException("Pod should be paired first"); + } + return communicationService.executeAction(new GetStatusAction(podState)); + } + + public void acknowledgeAlerts() { + if (!isInitialized()) { + throw new IllegalStateException("Pod should be initialized first"); + } + communicationService.executeAction(new AcknowledgeAlertsAction(podState, podState.getActiveAlerts())); + } + + public void pairAndPrime() { + if (podState == null) { + podState = communicationService.executeAction(new PairAction(new PairService())); + } + if (podState.getSetupProgress().isBefore(SetupProgress.PRIMING_FINISHED)) { + communicationService.executeAction(new PrimeAction(new PrimeService(), podState)); + + executeDelayed(() -> { + StatusResponse delayedStatusResponse = communicationService.executeAction(new GetStatusAction(podState)); + PrimeAction.updatePrimingStatus(podState, delayedStatusResponse); + }, OmnipodConst.POD_PRIME_DURATION); + } else { + throw new IllegalStateException("Illegal setup state: " + podState.getSetupProgress().name()); + } + } + + public void insertCannula(Profile profile) { + if (podState == null || podState.getSetupProgress().isBefore(SetupProgress.PRIMING_FINISHED)) { + throw new IllegalArgumentException("Pod should be paired and primed first"); + } else if (podState.getSetupProgress().isAfter(SetupProgress.CANNULA_INSERTING)) { + throw new IllegalStateException("Illegal setup state: " + podState.getSetupProgress().name()); + } + + communicationService.executeAction(new InsertCannulaAction(new InsertCannulaService(), podState, + BasalScheduleMapper.mapProfileToBasalSchedule(profile))); + + executeDelayed(() -> { + StatusResponse delayedStatusResponse = communicationService.executeAction(new GetStatusAction(podState)); + InsertCannulaAction.updateCannulaInsertionStatus(podState, delayedStatusResponse); + }, OmnipodConst.POD_CANNULA_INSERTION_DURATION); + } + + public void setBasalSchedule(BasalSchedule basalSchedule, boolean confidenceReminder) { + if (!isInitialized()) { + throw new IllegalStateException("Pod should be initialized first"); + } + communicationService.executeAction(new SetBasalScheduleAction(podState, basalSchedule, + confidenceReminder, podState.getScheduleOffset(), true)); + } + + public void setTempBasal(double rate, Duration duration) { + if (!isInitialized()) { + throw new IllegalStateException("Pod should be initialized first"); + } + communicationService.executeAction(new SetTempBasalAction(new SetTempBasalService(), + podState, rate, duration, true, true)); + } + + public void cancelTempBasal() { + if (!isInitialized()) { + throw new IllegalStateException("Pod should be initialized first"); + } + communicationService.executeAction(new CancelDeliveryAction(podState, DeliveryType.TEMP_BASAL, true)); + } + + public void bolus(double units) { + if (!isInitialized()) { + throw new IllegalStateException("Pod should be initialized first"); + } + communicationService.executeAction(new BolusAction(podState, units, true, true)); + } + + public void cancelBolus() { + if (!isInitialized()) { + throw new IllegalStateException("Pod should be initialized first"); + } + communicationService.executeAction(new CancelDeliveryAction(podState, DeliveryType.BOLUS, true)); + } + + public void suspendDelivery() { + if (!isInitialized()) { + throw new IllegalStateException("Pod should be initialized first"); + } + communicationService.executeAction(new CancelDeliveryAction(podState, EnumSet.allOf(DeliveryType.class), true)); + } + + public void resumeDelivery() { + if (!isInitialized()) { + throw new IllegalStateException("Pod should be initialized first"); + } + communicationService.executeAction(new SetBasalScheduleAction(podState, podState.getBasalSchedule(), + true, podState.getScheduleOffset(), true)); + } + + public void setTime() { + if (!isInitialized()) { + throw new IllegalStateException("Pod should be initialized first"); + } + // Suspend delivery + communicationService.executeAction(new CancelDeliveryAction(podState, EnumSet.allOf(DeliveryType.class), false)); + + // Joda seems to cache the default time zone, so we use the JVM's + DateTimeZone.setDefault(DateTimeZone.forTimeZone(TimeZone.getDefault())); + podState.setTimeZone(DateTimeZone.getDefault()); + + // Resume delivery + communicationService.executeAction(new SetBasalScheduleAction(podState, podState.getBasalSchedule(), + true, podState.getScheduleOffset(), true)); + } + + public DateTime getTime() { + return podState.getTime(); + } + + public void deactivatePod() { + if (podState == null) { + throw new IllegalStateException("Pod should be paired first"); + } + communicationService.executeAction(new DeactivatePodAction(podState, true)); + resetPodState(); + } + + public boolean isInitialized() { + return podState != null && podState.getSetupProgress() == SetupProgress.COMPLETED; + } + + public String getPodStateAsString() { + return podState == null ? "null" : podState.toString(); + } + + public void resetPodState() { + podState = null; + SP.remove(OmnipodConst.Prefs.PodState); + } + + private void executeDelayed(Runnable r, Duration timeout) { + ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1); + scheduledExecutorService.schedule(r, timeout.getMillis(), TimeUnit.MILLISECONDS); + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/OmnipodCommunicationService.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/OmnipodCommunicationService.java new file mode 100644 index 0000000000..2a93c00f1d --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/OmnipodCommunicationService.java @@ -0,0 +1,246 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collections; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkCommunicationManager; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.RFSpy; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.RileyLinkCommunicationException; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.data.RLMessage; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.defs.RLMessageType; +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.defs.RileyLinkBLEError; +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.OmnipodAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.OmnipodMessage; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.OmnipodPacket; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.ErrorResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoFaultEvent; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.ErrorResponseType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PacketType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodState; +import info.nightscout.androidaps.plugins.pump.omnipod.exception.NotEnoughDataException; +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; +import info.nightscout.androidaps.plugins.pump.omnipod.exception.PodFaultException; +import info.nightscout.androidaps.plugins.pump.omnipod.exception.PodReturnedErrorResponseException; + +/** + * Created by andy on 6/29/18. + */ + +public class OmnipodCommunicationService extends RileyLinkCommunicationManager { + + private static final Logger LOG = LoggerFactory.getLogger(OmnipodCommunicationService.class); + + public OmnipodCommunicationService(RFSpy rfspy) { + super(rfspy); + } + + @Override + protected void configurePumpSpecificSettings() { + } + + @Override + public boolean tryToConnectToDevice() { + // TODO + return false; + } + + @Override + public byte[] createPumpMessageContent(RLMessageType type) { + return new byte[0]; + } + + @Override + public E createResponseMessage(byte[] payload, Class clazz) { + return (E) new OmnipodPacket(payload); + } + + public T sendCommand(Class responseClass, PodState podState, MessageBlock command) { + OmnipodMessage message = new OmnipodMessage(podState.getAddress(), Collections.singletonList(command), podState.getMessageNumber()); + return exchangeMessages(responseClass, podState, message); + } + + // Convenience method + public T executeAction(OmnipodAction action) { + return action.execute(this); + } + + public T exchangeMessages(Class responseClass, PodState podState, OmnipodMessage message) { + return exchangeMessages(responseClass, podState, message, null, null); + } + + public synchronized T exchangeMessages(Class responseClass, PodState podState, OmnipodMessage message, Integer addressOverride, Integer ackAddressOverride) { + for (int i = 0; 2 > i; i++) { + + if (podState.hasNonceState() && message.isNonceResyncable()) { + podState.advanceToNextNonce(); + } + + MessageBlock responseMessageBlock = transportMessages(podState, message, addressOverride, ackAddressOverride); + + if (responseMessageBlock instanceof StatusResponse) { + podState.updateFromStatusResponse((StatusResponse) responseMessageBlock); + } + + if (responseClass.isInstance(responseMessageBlock)) { + return (T) responseMessageBlock; + } else { + if (responseMessageBlock.getType() == MessageBlockType.ERROR_RESPONSE) { + ErrorResponse error = (ErrorResponse) responseMessageBlock; + if (error.getErrorResponseType() == ErrorResponseType.BAD_NONCE) { + podState.resyncNonce(error.getNonceSearchKey(), message.getSentNonce(), message.getSequenceNumber()); + message.resyncNonce(podState.getCurrentNonce()); + } else { + throw new PodReturnedErrorResponseException((ErrorResponse) responseMessageBlock); + } + } else if (responseMessageBlock.getType() == MessageBlockType.POD_INFO_RESPONSE && ((PodInfoResponse) responseMessageBlock).getSubType() == PodInfoType.FAULT_EVENT) { + PodInfoFaultEvent faultEvent = ((PodInfoResponse) responseMessageBlock).getPodInfo(); + LOG.error("Pod fault: " + faultEvent.getFaultEventCode().name()); + podState.setFaultEvent(faultEvent); + throw new PodFaultException(faultEvent); + } else { + throw new OmnipodException("Unexpected response type: " + responseMessageBlock.toString()); + } + } + } + + throw new OmnipodException("Nonce resync failed"); + } + + private MessageBlock transportMessages(PodState podState, OmnipodMessage message, Integer addressOverride, Integer ackAddressOverride) { + int packetAddress = podState.getAddress(); + if (addressOverride != null) { + packetAddress = addressOverride; + } + + boolean firstPacket = true; + byte[] encodedMessage = message.getEncoded(); + + OmnipodPacket response = null; + while (encodedMessage.length > 0) { + PacketType packetType = firstPacket ? PacketType.PDM : PacketType.CON; + OmnipodPacket packet = new OmnipodPacket(packetAddress, packetType, podState.getPacketNumber(), encodedMessage); + byte[] encodedMessageInPacket = packet.getEncodedMessage(); + //getting the data remaining to be sent + encodedMessage = ByteUtil.substring(encodedMessage, encodedMessageInPacket.length, encodedMessage.length - encodedMessageInPacket.length); + firstPacket = false; + try { + response = exchangePackets(podState, packet); + } catch (Exception ex) { + throw new OmnipodException("Failed to exchange packets", ex); + } + //We actually ignore (ack) responses if it is not last packet to send + } + + if (response.getPacketType() == PacketType.ACK) { + podState.increasePacketNumber(1); + throw new OmnipodException("Received ack instead of real response"); + } + + OmnipodMessage receivedMessage = null; + byte[] receivedMessageData = response.getEncodedMessage(); + while (receivedMessage == null) { + try { + receivedMessage = OmnipodMessage.decodeMessage(receivedMessageData); + } catch (NotEnoughDataException ex) { + // Message is (probably) not complete yet + OmnipodPacket ackForCon = createAckPacket(podState, packetAddress, ackAddressOverride); + try { + OmnipodPacket conPacket = exchangePackets(podState, ackForCon, 3, 40); + if (conPacket.getPacketType() != PacketType.CON) { + throw new OmnipodException("Received a non-con packet type: " + conPacket.getPacketType()); + } + receivedMessageData = ByteUtil.concat(receivedMessageData, conPacket.getEncodedMessage()); + } catch (RileyLinkCommunicationException ex2) { + throw new OmnipodException("RileyLink communication failed", ex2); + } + } + } + + podState.increaseMessageNumber(2); + + ackUntilQuiet(podState, packetAddress, ackAddressOverride); + + List messageBlocks = receivedMessage.getMessageBlocks(); + + if (messageBlocks.size() == 0) { + throw new OmnipodException("Not enough data"); + } else if (messageBlocks.size() > 1) { + LOG.error("received more than one message block: " + messageBlocks.toString()); + } + + return messageBlocks.get(0); + } + + private OmnipodPacket createAckPacket(PodState podState, Integer packetAddress, Integer messageAddress) { + int pktAddress = podState.getAddress(); + int msgAddress = podState.getAddress(); + if (packetAddress != null) { + pktAddress = packetAddress; + } + if (messageAddress != null) { + msgAddress = messageAddress; + } + return new OmnipodPacket(pktAddress, PacketType.ACK, podState.getPacketNumber(), ByteUtil.getBytesFromInt(msgAddress)); + } + + private void ackUntilQuiet(PodState podState, Integer packetAddress, Integer messageAddress) { + OmnipodPacket ack = createAckPacket(podState, packetAddress, messageAddress); + boolean quiet = false; + while (!quiet) try { + sendAndListen(ack, 300, 1, 0, 40, OmnipodPacket.class); + } catch (RileyLinkCommunicationException ex) { + if (RileyLinkBLEError.Timeout.equals(ex.getErrorCode())) { + quiet = true; + } else { + LOG.debug("Ignoring exception in ackUntilQuiet: " + ex.getClass().getSimpleName() + ": " + ex.getMessage()); + } + } catch (Exception ex) { + LOG.debug("Ignoring exception in ackUntilQuiet: " + ex.getClass().getSimpleName() + ": " + ex.getMessage()); + } + + podState.increasePacketNumber(1); + } + + private OmnipodPacket exchangePackets(PodState podState, OmnipodPacket packet) throws RileyLinkCommunicationException { + return exchangePackets(podState, packet, 0, 333, 9000, 127); + } + + private OmnipodPacket exchangePackets(PodState podState, OmnipodPacket packet, int repeatCount, int preambleExtensionMilliseconds) throws RileyLinkCommunicationException { + return exchangePackets(podState, packet, repeatCount, 333, 9000, preambleExtensionMilliseconds); + } + + private OmnipodPacket exchangePackets(PodState podState, OmnipodPacket packet, int repeatCount, int responseTimeoutMilliseconds, int exchangeTimeoutMilliseconds, int preambleExtensionMilliseconds) throws RileyLinkCommunicationException { + long timeoutTime = System.currentTimeMillis() + exchangeTimeoutMilliseconds; + + while (System.currentTimeMillis() < timeoutTime) { + OmnipodPacket response = null; + try { + response = sendAndListen(packet, responseTimeoutMilliseconds, repeatCount, 9, preambleExtensionMilliseconds, OmnipodPacket.class); + } catch (Exception ex) { + LOG.debug("Ignoring exception in exchangePackets: " + ex.getClass().getSimpleName() + ": " + ex.getMessage()); + } + if (response == null || !response.isValid()) { + continue; + } + if (response.getAddress() != packet.getAddress()) { + continue; + } + if (response.getSequenceNumber() != ((podState.getPacketNumber() + 1) & 0b11111)) { + continue; + } + + podState.increasePacketNumber(2); + return response; + } + throw new OmnipodException("Timeout when trying to exchange packets"); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/AcknowledgeAlertsAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/AcknowledgeAlertsAction.java new file mode 100644 index 0000000000..f049bc882e --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/AcknowledgeAlertsAction.java @@ -0,0 +1,38 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import java.util.Collections; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationService; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.AcknowledgeAlertsCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertSet; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertSlot; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; + +public class AcknowledgeAlertsAction implements OmnipodAction { + private final PodSessionState podState; + private final AlertSet alerts; + + public AcknowledgeAlertsAction(PodSessionState podState, AlertSet alerts) { + if (podState == null) { + throw new IllegalArgumentException("Pod state cannot be null"); + } + if (alerts == null) { + throw new IllegalArgumentException("Alert set can not be null"); + } else if (alerts.size() == 0) { + throw new IllegalArgumentException("Alert set can not be empty"); + } + this.podState = podState; + this.alerts = alerts; + } + + public AcknowledgeAlertsAction(PodSessionState podState, AlertSlot alertSlot) { + this(podState, new AlertSet(Collections.singletonList(alertSlot))); + } + + @Override + public StatusResponse execute(OmnipodCommunicationService communicationService) { + return communicationService.sendCommand(StatusResponse.class, podState, + new AcknowledgeAlertsCommand(podState.getCurrentNonce(), alerts)); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/BolusAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/BolusAction.java new file mode 100644 index 0000000000..7dd9cedcbb --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/BolusAction.java @@ -0,0 +1,52 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import org.joda.time.Duration; + +import java.util.Arrays; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationService; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.OmnipodMessage; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.BolusExtraCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.SetInsulinScheduleCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BolusDeliverySchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; + +public class BolusAction implements OmnipodAction { + private final PodSessionState podState; + private final double units; + private final Duration timeBetweenPulses; + private final boolean acknowledgementBeep; + private final boolean completionBeep; + + public BolusAction(PodSessionState podState, double units, Duration timeBetweenPulses, + boolean acknowledgementBeep, boolean completionBeep) { + if (podState == null) { + throw new IllegalArgumentException("Pod state cannot be null"); + } + if (timeBetweenPulses == null) { + throw new IllegalArgumentException("Time between pulses cannot be null"); + } + this.podState = podState; + this.units = units; + this.timeBetweenPulses = timeBetweenPulses; + this.acknowledgementBeep = acknowledgementBeep; + this.completionBeep = completionBeep; + } + + public BolusAction(PodSessionState podState, double units, boolean acknowledgementBeep, boolean completionBeep) { + this(podState, units, Duration.standardSeconds(2), acknowledgementBeep, completionBeep); + } + + @Override + public StatusResponse execute(OmnipodCommunicationService communicationService) { + BolusDeliverySchedule bolusDeliverySchedule = new BolusDeliverySchedule(units, timeBetweenPulses); + SetInsulinScheduleCommand setInsulinScheduleCommand = new SetInsulinScheduleCommand( + podState.getCurrentNonce(), bolusDeliverySchedule); + BolusExtraCommand bolusExtraCommand = new BolusExtraCommand(units, timeBetweenPulses, + acknowledgementBeep, completionBeep); + OmnipodMessage primeBolusMessage = new OmnipodMessage(podState.getAddress(), + Arrays.asList(setInsulinScheduleCommand, bolusExtraCommand), podState.getMessageNumber()); + return communicationService.exchangeMessages(StatusResponse.class, podState, primeBolusMessage); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/CancelDeliveryAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/CancelDeliveryAction.java new file mode 100644 index 0000000000..197d65f826 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/CancelDeliveryAction.java @@ -0,0 +1,44 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import java.util.EnumSet; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationService; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.CancelDeliveryCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.BeepType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.DeliveryType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; + +public class CancelDeliveryAction implements OmnipodAction { + private final PodSessionState podState; + private final EnumSet deliveryTypes; + private final BeepType beepType; + + public CancelDeliveryAction(PodSessionState podState, EnumSet deliveryTypes, + boolean acknowledgementBeep) { + if (podState == null) { + throw new IllegalArgumentException("Pod state cannot be null"); + } + if (deliveryTypes == null) { + throw new IllegalArgumentException("Delivery types cannot be null"); + } + this.podState = podState; + this.deliveryTypes = deliveryTypes; + if (acknowledgementBeep) { + beepType = BeepType.BIP_BIP; + } else { + beepType = BeepType.NO_BEEP; + } + } + + public CancelDeliveryAction(PodSessionState podState, DeliveryType deliveryType, + boolean acknowledgementBeep) { + this(podState, EnumSet.of(deliveryType), acknowledgementBeep); + } + + @Override + public StatusResponse execute(OmnipodCommunicationService communicationService) { + return communicationService.sendCommand(StatusResponse.class, podState, + new CancelDeliveryCommand(podState.getCurrentNonce(), beepType, deliveryTypes)); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/ConfigureAlertsAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/ConfigureAlertsAction.java new file mode 100644 index 0000000000..3cbae137a1 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/ConfigureAlertsAction.java @@ -0,0 +1,35 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationService; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.ConfigureAlertsCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertConfiguration; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; + +public class ConfigureAlertsAction implements OmnipodAction { + private final PodSessionState podState; + private final List alertConfigurations; + + public ConfigureAlertsAction(PodSessionState podState, List alertConfigurations) { + if (podState == null) { + throw new IllegalArgumentException("Pod state cannot be null"); + } + if (alertConfigurations == null) { + throw new IllegalArgumentException("Alert configurations cannot be null"); + } + this.podState = podState; + this.alertConfigurations = alertConfigurations; + } + + @Override + public StatusResponse execute(OmnipodCommunicationService communicationService) { + ConfigureAlertsCommand configureAlertsCommand = new ConfigureAlertsCommand(podState.getCurrentNonce(), alertConfigurations); + StatusResponse statusResponse = communicationService.sendCommand(StatusResponse.class, podState, configureAlertsCommand); + for (AlertConfiguration alertConfiguration : alertConfigurations) { + podState.putConfiguredAlert(alertConfiguration.getAlertSlot(), alertConfiguration.getAlertType()); + } + return statusResponse; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/DeactivatePodAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/DeactivatePodAction.java new file mode 100644 index 0000000000..6f9b08ffbd --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/DeactivatePodAction.java @@ -0,0 +1,33 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import java.util.EnumSet; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationService; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.DeactivatePodCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.DeliveryType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; + +public class DeactivatePodAction implements OmnipodAction { + private final PodSessionState podState; + private final boolean acknowledgementBeep; + + public DeactivatePodAction(PodSessionState podState, boolean acknowledgementBeep) { + if (podState == null) { + throw new IllegalArgumentException("Pod state cannot be null"); + } + this.podState = podState; + this.acknowledgementBeep = acknowledgementBeep; + } + + @Override + public StatusResponse execute(OmnipodCommunicationService communicationService) { + if (!podState.isSuspended() && !podState.hasFaultEvent()) { + communicationService.executeAction(new CancelDeliveryAction(podState, + EnumSet.allOf(DeliveryType.class), acknowledgementBeep)); + } + + DeactivatePodCommand deactivatePodCommand = new DeactivatePodCommand(podState.getCurrentNonce()); + return communicationService.sendCommand(StatusResponse.class, podState, deactivatePodCommand); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/GetPodInfoAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/GetPodInfoAction.java new file mode 100644 index 0000000000..618046d38e --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/GetPodInfoAction.java @@ -0,0 +1,28 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationService; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.GetStatusCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; + +public class GetPodInfoAction implements OmnipodAction { + private final PodSessionState podState; + private final PodInfoType podInfoType; + + public GetPodInfoAction(PodSessionState podState, PodInfoType podInfoType) { + if (podState == null) { + throw new IllegalArgumentException("Pod state cannot be null"); + } + if (podInfoType == null) { + throw new IllegalArgumentException("Pod info type cannot be null"); + } + this.podState = podState; + this.podInfoType = podInfoType; + } + + @Override + public PodInfoResponse execute(OmnipodCommunicationService communicationService) { + return communicationService.sendCommand(PodInfoResponse.class, podState, new GetStatusCommand(podInfoType)); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/GetStatusAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/GetStatusAction.java new file mode 100644 index 0000000000..302fa55577 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/GetStatusAction.java @@ -0,0 +1,23 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationService; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.GetStatusCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; + +public class GetStatusAction implements OmnipodAction { + private final PodSessionState podState; + + public GetStatusAction(PodSessionState podState) { + if (podState == null) { + throw new IllegalArgumentException("Pod state cannot be null"); + } + this.podState = podState; + } + + @Override + public StatusResponse execute(OmnipodCommunicationService communicationService) { + return communicationService.sendCommand(StatusResponse.class, podState, new GetStatusCommand(PodInfoType.NORMAL)); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/InsertCannulaAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/InsertCannulaAction.java new file mode 100644 index 0000000000..b1739503e9 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/InsertCannulaAction.java @@ -0,0 +1,73 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationService; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.service.InsertCannulaService; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.SetupProgress; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BasalSchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; + +public class InsertCannulaAction implements OmnipodAction { + private static final Logger LOG = LoggerFactory.getLogger(InsertCannulaAction.class); + + private final PodSessionState podState; + private final InsertCannulaService service; + private final BasalSchedule initialBasalSchedule; + + public InsertCannulaAction(InsertCannulaService insertCannulaService, PodSessionState podState, BasalSchedule initialBasalSchedule) { + if (insertCannulaService == null) { + throw new IllegalArgumentException("Insert cannula service cannot be null"); + } + if (podState == null) { + throw new IllegalArgumentException("Pod state cannot be null"); + } + if (initialBasalSchedule == null) { + throw new IllegalArgumentException("Initial basal schedule cannot be null"); + } + this.service = insertCannulaService; + this.podState = podState; + this.initialBasalSchedule = initialBasalSchedule; + } + + public static void updateCannulaInsertionStatus(PodSessionState podState, StatusResponse statusResponse) { + if (podState.getSetupProgress().equals(SetupProgress.CANNULA_INSERTING) && + statusResponse.getPodProgressStatus().isReadyForDelivery()) { + if (LOG.isDebugEnabled()) { + LOG.debug("Updating SetupProgress from CANNULA_INSERTING to COMPLETED"); + } + podState.setSetupProgress(SetupProgress.COMPLETED); + } + } + + @Override + public StatusResponse execute(OmnipodCommunicationService communicationService) { + if (podState.getSetupProgress().isBefore(SetupProgress.PRIMING_FINISHED)) { + throw new IllegalStateException("Pod should be primed first"); + } + + if (podState.getSetupProgress().isBefore(SetupProgress.INITIAL_BASAL_SCHEDULE_SET)) { + service.programInitialBasalSchedule(communicationService, podState, initialBasalSchedule); + podState.setSetupProgress(SetupProgress.INITIAL_BASAL_SCHEDULE_SET); + } + if (podState.getSetupProgress().isBefore(SetupProgress.STARTING_INSERT_CANNULA)) { + service.executeExpirationRemindersAlertCommand(communicationService, podState); + podState.setSetupProgress(SetupProgress.STARTING_INSERT_CANNULA); + } + + if (podState.getSetupProgress().isBefore(SetupProgress.CANNULA_INSERTING)) { + StatusResponse statusResponse = service.executeInsertionBolusCommand(communicationService, podState); + podState.setSetupProgress(SetupProgress.CANNULA_INSERTING); + return statusResponse; + } else if (podState.getSetupProgress().equals(SetupProgress.CANNULA_INSERTING)) { + // Check status + StatusResponse statusResponse = communicationService.executeAction(new GetStatusAction(podState)); + updateCannulaInsertionStatus(podState, statusResponse); + return statusResponse; + } else { + throw new IllegalStateException("Illegal setup progress: " + podState.getSetupProgress().name()); + } + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/OmnipodAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/OmnipodAction.java new file mode 100644 index 0000000000..5cdcf12f76 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/OmnipodAction.java @@ -0,0 +1,7 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationService; + +public interface OmnipodAction { + T execute(OmnipodCommunicationService communicationService); +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/PairAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/PairAction.java new file mode 100644 index 0000000000..6b271dd98b --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/PairAction.java @@ -0,0 +1,54 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; + +import java.util.Random; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationService; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.service.PairService; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.VersionResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.SetupProgress; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSetupState; + +public class PairAction implements OmnipodAction { + private final PairService service; + private final int address; + + public PairAction(PairService pairService, int address) { + if (pairService == null) { + throw new IllegalArgumentException("Pair service cannot be null"); + } + this.service = pairService; + this.address = address; + } + + public PairAction(PairService service) { + this(service, generateRandomAddress()); + } + + private static int generateRandomAddress() { + return 0x1f000000 | (new Random().nextInt() & 0x000fffff); + } + + @Override + public PodSessionState execute(OmnipodCommunicationService communicationService) { + PodSetupState setupState = new PodSetupState(address, 0x00, 0x00); + + VersionResponse assignAddressResponse = service.executeAssignAddressCommand(communicationService, setupState); + + DateTimeZone timeZone = DateTimeZone.getDefault(); + DateTime activationDate = DateTime.now(timeZone); + + VersionResponse confirmPairingResponse = service.executeConfigurePodCommand(communicationService, setupState, + assignAddressResponse.getLot(), assignAddressResponse.getTid(), activationDate); + + PodSessionState podState = new PodSessionState(timeZone, address, activationDate, confirmPairingResponse.getPiVersion(), + confirmPairingResponse.getPmVersion(), confirmPairingResponse.getLot(), confirmPairingResponse.getTid(), + setupState.getPacketNumber(), setupState.getMessageNumber()); + podState.setSetupProgress(SetupProgress.POD_CONFIGURED); + + return podState; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/PrimeAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/PrimeAction.java new file mode 100644 index 0000000000..e0b3f15187 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/PrimeAction.java @@ -0,0 +1,63 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationService; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.service.PrimeService; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodProgressStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.SetupProgress; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; + +public class PrimeAction implements OmnipodAction { + private static final Logger LOG = LoggerFactory.getLogger(PrimeAction.class); + + private final PrimeService service; + private final PodSessionState podState; + + public PrimeAction(PrimeService primeService, PodSessionState podState) { + if (primeService == null) { + throw new IllegalArgumentException("Prime service cannot be null"); + } + if (podState == null) { + throw new IllegalArgumentException("Pod state cannot be null"); + } + this.service = primeService; + this.podState = podState; + } + + public static void updatePrimingStatus(PodSessionState podState, StatusResponse statusResponse) { + if (podState.getSetupProgress().equals(SetupProgress.PRIMING) && statusResponse.getPodProgressStatus().equals(PodProgressStatus.READY_FOR_BASAL_SCHEDULE)) { + if (LOG.isDebugEnabled()) { + LOG.debug("Updating SetupProgress from PRIMING to PRIMING_FINISHED"); + } + podState.setSetupProgress(SetupProgress.PRIMING_FINISHED); + } + } + + @Override + public StatusResponse execute(OmnipodCommunicationService communicationService) { + if (podState.getSetupProgress().isBefore(SetupProgress.POD_CONFIGURED)) { + throw new IllegalStateException("Pod should be paired first"); + } + if (podState.getSetupProgress().isBefore(SetupProgress.STARTING_PRIME)) { + service.executeDisableTab5Sub16FaultConfigCommand(communicationService, podState); + service.executeFinishSetupReminderAlertCommand(communicationService, podState); + podState.setSetupProgress(SetupProgress.STARTING_PRIME); + } + + if (podState.getSetupProgress().isBefore(SetupProgress.PRIMING)) { + StatusResponse statusResponse = service.executePrimeBolusCommand(communicationService, podState); + podState.setSetupProgress(SetupProgress.PRIMING); + return statusResponse; + } else if (podState.getSetupProgress().equals(SetupProgress.PRIMING)) { + // Check status + StatusResponse statusResponse = communicationService.executeAction(new GetStatusAction(podState)); + updatePrimingStatus(podState, statusResponse); + return statusResponse; + } else { + throw new IllegalStateException("Illegal setup progress: " + podState.getSetupProgress().name()); + } + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/SetBasalScheduleAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/SetBasalScheduleAction.java new file mode 100644 index 0000000000..ec95bc2ee7 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/SetBasalScheduleAction.java @@ -0,0 +1,52 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import org.joda.time.Duration; + +import java.util.Arrays; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationService; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.OmnipodMessage; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.BasalScheduleExtraCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.SetInsulinScheduleCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BasalSchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; + +public class SetBasalScheduleAction implements OmnipodAction { + private final PodSessionState podState; + private final BasalSchedule basalSchedule; + private final boolean confidenceReminder; + private final Duration scheduleOffset; + private final boolean acknowledgementBeep; + + public SetBasalScheduleAction(PodSessionState podState, BasalSchedule basalSchedule, + boolean confidenceReminder, Duration scheduleOffset, boolean acknowledgementBeep) { + if (podState == null) { + throw new IllegalArgumentException("Pod state cannot be null"); + } + if (basalSchedule == null) { + throw new IllegalArgumentException("Basal schedule cannot be null"); + } + if (scheduleOffset == null) { + throw new IllegalArgumentException("Schedule offset cannot be null"); + } + this.podState = podState; + this.basalSchedule = basalSchedule; + this.confidenceReminder = confidenceReminder; + this.scheduleOffset = scheduleOffset; + this.acknowledgementBeep = acknowledgementBeep; + } + + @Override + public StatusResponse execute(OmnipodCommunicationService communicationService) { + SetInsulinScheduleCommand setBasal = new SetInsulinScheduleCommand(podState.getCurrentNonce(), basalSchedule, scheduleOffset); + BasalScheduleExtraCommand extraCommand = new BasalScheduleExtraCommand(basalSchedule, scheduleOffset, + acknowledgementBeep, confidenceReminder, Duration.ZERO); + OmnipodMessage basalMessage = new OmnipodMessage(podState.getAddress(), Arrays.asList(setBasal, extraCommand), + podState.getMessageNumber()); + + StatusResponse statusResponse = communicationService.exchangeMessages(StatusResponse.class, podState, basalMessage); + podState.setBasalSchedule(basalSchedule); + return statusResponse; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/SetTempBasalAction.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/SetTempBasalAction.java new file mode 100644 index 0000000000..9ed6f23299 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/SetTempBasalAction.java @@ -0,0 +1,50 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import org.joda.time.Duration; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationService; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.service.SetTempBasalService; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.DeliveryStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; + +public class SetTempBasalAction implements OmnipodAction { + private final SetTempBasalService service; + private final PodSessionState podState; + private final double rate; + private final Duration duration; + private final boolean acknowledgementBeep; + private final boolean completionBeep; + + public SetTempBasalAction(SetTempBasalService setTempBasalService, PodSessionState podState, + double rate, Duration duration, boolean acknowledgementBeep, boolean completionBeep) { + if (setTempBasalService == null) { + throw new IllegalArgumentException("Set temp basal service cannot be null"); + } + if (podState == null) { + throw new IllegalArgumentException("Pod state cannot be null"); + } + if (duration == null) { + throw new IllegalArgumentException("Duration cannot be null"); + } + this.service = setTempBasalService; + this.podState = podState; + this.rate = rate; + this.duration = duration; + this.acknowledgementBeep = acknowledgementBeep; + this.completionBeep = completionBeep; + } + + @Override + public StatusResponse execute(OmnipodCommunicationService communicationService) { + StatusResponse statusResponse = service.cancelTempBasal(communicationService, podState); + + if (statusResponse.getDeliveryStatus() != DeliveryStatus.NORMAL) { + throw new IllegalStateException("Illegal delivery status: " + + statusResponse.getDeliveryStatus().name()); + } + + return service.executeTempBasalCommand(communicationService, podState, rate, duration, + acknowledgementBeep, completionBeep); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/service/InsertCannulaService.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/service/InsertCannulaService.java new file mode 100644 index 0000000000..703a252b8e --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/service/InsertCannulaService.java @@ -0,0 +1,56 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action.service; + +import org.joda.time.DateTime; +import org.joda.time.Duration; + +import java.util.Arrays; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationService; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.BolusAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.ConfigureAlertsAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.SetBasalScheduleAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertConfiguration; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertConfigurationFactory; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BasalSchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class InsertCannulaService { + public StatusResponse programInitialBasalSchedule(OmnipodCommunicationService communicationService, + PodSessionState podState, BasalSchedule basalSchedule) { + return communicationService.executeAction(new SetBasalScheduleAction(podState, basalSchedule, + true, podState.getScheduleOffset(), false)); + } + + public StatusResponse executeExpirationRemindersAlertCommand(OmnipodCommunicationService communicationService, + PodSessionState podState) { + DateTime endOfServiceTime = podState.getActivatedAt().plus(OmnipodConst.SERVICE_DURATION); + + Duration timeUntilExpirationAdvisoryAlarm = new Duration(DateTime.now(), + endOfServiceTime.minus(OmnipodConst.END_OF_SERVICE_IMMINENT_WINDOW).minus(OmnipodConst.EXPIRATION_ADVISORY_WINDOW)); + Duration timeUntilShutdownImminentAlarm = new Duration(DateTime.now(), + endOfServiceTime.minus(OmnipodConst.END_OF_SERVICE_IMMINENT_WINDOW)); + + AlertConfiguration expirationAdvisoryAlertConfiguration = AlertConfigurationFactory.createExpirationAdvisoryAlertConfiguration( + timeUntilExpirationAdvisoryAlarm, OmnipodConst.EXPIRATION_ADVISORY_WINDOW); + AlertConfiguration shutdownImminentAlertConfiguration = AlertConfigurationFactory.createShutdownImminentAlertConfiguration( + timeUntilShutdownImminentAlarm); + AlertConfiguration autoOffAlertConfiguration = AlertConfigurationFactory.createAutoOffAlertConfiguration( + false, Duration.ZERO); + + List alertConfigurations = Arrays.asList( // + expirationAdvisoryAlertConfiguration, // + shutdownImminentAlertConfiguration, // + autoOffAlertConfiguration // + ); + + return new ConfigureAlertsAction(podState, alertConfigurations).execute(communicationService); + } + + public StatusResponse executeInsertionBolusCommand(OmnipodCommunicationService communicationService, PodSessionState podState) { + return communicationService.executeAction(new BolusAction(podState, OmnipodConst.POD_CANNULA_INSERTION_BOLUS_UNITS, + Duration.standardSeconds(1), false, false)); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/service/PairService.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/service/PairService.java new file mode 100644 index 0000000000..63ff8ef6bb --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/service/PairService.java @@ -0,0 +1,45 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action.service; + +import org.joda.time.DateTime; + +import java.util.Collections; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationService; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.OmnipodMessage; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.AssignAddressCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.ConfigurePodCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.VersionResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodProgressStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSetupState; +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class PairService { + public VersionResponse executeAssignAddressCommand(OmnipodCommunicationService communicationService, PodSetupState setupState) { + AssignAddressCommand assignAddress = new AssignAddressCommand(setupState.getAddress()); + OmnipodMessage assignAddressMessage = new OmnipodMessage(OmnipodConst.DEFAULT_ADDRESS, + Collections.singletonList(assignAddress), setupState.getMessageNumber()); + + return communicationService.exchangeMessages(VersionResponse.class, setupState, assignAddressMessage, + OmnipodConst.DEFAULT_ADDRESS, setupState.getAddress()); + } + + public VersionResponse executeConfigurePodCommand(OmnipodCommunicationService communicationService, + PodSetupState setupState, int lot, int tid, DateTime activationDate) { + // at this point for an unknown reason PDM starts counting messages from 0 again + setupState.setMessageNumber(0x00); + + ConfigurePodCommand configurePodCommand = new ConfigurePodCommand(setupState.getAddress(), activationDate, + lot, tid); + OmnipodMessage message = new OmnipodMessage(OmnipodConst.DEFAULT_ADDRESS, + Collections.singletonList(configurePodCommand), setupState.getMessageNumber()); + VersionResponse configurePodResponse = communicationService.exchangeMessages(VersionResponse.class, setupState, + message, OmnipodConst.DEFAULT_ADDRESS, setupState.getAddress()); + + if (configurePodResponse.getPodProgressStatus() != PodProgressStatus.PAIRING_SUCCESS) { + throw new OmnipodException("Pairing failed, state: " + configurePodResponse.getPodProgressStatus().name()); + } + + return configurePodResponse; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/service/PrimeService.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/service/PrimeService.java new file mode 100644 index 0000000000..8744435410 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/service/PrimeService.java @@ -0,0 +1,37 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action.service; + +import org.joda.time.Duration; + +import java.util.Collections; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationService; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.BolusAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.ConfigureAlertsAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.OmnipodMessage; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.FaultConfigCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertConfiguration; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertConfigurationFactory; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class PrimeService { + + public StatusResponse executeDisableTab5Sub16FaultConfigCommand(OmnipodCommunicationService communicationService, PodSessionState podState) { + FaultConfigCommand faultConfigCommand = new FaultConfigCommand(podState.getCurrentNonce(), (byte) 0x00, (byte) 0x00); + OmnipodMessage faultConfigMessage = new OmnipodMessage(podState.getAddress(), + Collections.singletonList(faultConfigCommand), podState.getMessageNumber()); + return communicationService.exchangeMessages(StatusResponse.class, podState, faultConfigMessage); + } + + public StatusResponse executeFinishSetupReminderAlertCommand(OmnipodCommunicationService communicationService, PodSessionState podState) { + AlertConfiguration finishSetupReminderAlertConfiguration = AlertConfigurationFactory.createFinishSetupReminderAlertConfiguration(); + return communicationService.executeAction(new ConfigureAlertsAction(podState, + Collections.singletonList(finishSetupReminderAlertConfiguration))); + } + + public StatusResponse executePrimeBolusCommand(OmnipodCommunicationService communicationService, PodSessionState podState) { + return communicationService.executeAction(new BolusAction(podState, OmnipodConst.POD_PRIME_BOLUS_UNITS, + Duration.standardSeconds(1), false, false)); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/service/SetTempBasalService.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/service/SetTempBasalService.java new file mode 100644 index 0000000000..540217f4c6 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/service/SetTempBasalService.java @@ -0,0 +1,33 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action.service; + +import org.joda.time.Duration; + +import java.util.Arrays; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationService; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.CancelDeliveryAction; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.OmnipodMessage; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.SetInsulinScheduleCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command.TempBasalExtraCommand; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.DeliveryType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; + +public class SetTempBasalService { + public StatusResponse cancelTempBasal(OmnipodCommunicationService communicationService, PodSessionState podState) { + return communicationService.executeAction(new CancelDeliveryAction(podState, DeliveryType.TEMP_BASAL, false)); + } + + public StatusResponse executeTempBasalCommand(OmnipodCommunicationService communicationService, + PodSessionState podState, double rate, Duration duration, + boolean acknowledgementBeep, boolean completionBeep) { + List messageBlocks = Arrays.asList( // + new SetInsulinScheduleCommand(podState.getCurrentNonce(), rate, duration), + new TempBasalExtraCommand(rate, duration, acknowledgementBeep, completionBeep, Duration.ZERO)); + + OmnipodMessage message = new OmnipodMessage(podState.getAddress(), messageBlocks, podState.getMessageNumber()); + return communicationService.exchangeMessages(StatusResponse.class, podState, message); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/IRawRepresentable.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/IRawRepresentable.java new file mode 100644 index 0000000000..2eec2afaf8 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/IRawRepresentable.java @@ -0,0 +1,5 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message; + +public interface IRawRepresentable { + byte[] getRawData(); +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/MessageBlock.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/MessageBlock.java new file mode 100644 index 0000000000..dba0035df7 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/MessageBlock.java @@ -0,0 +1,31 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; + +public abstract class MessageBlock { + protected byte[] encodedData = new byte[0]; + + public MessageBlock() { + } + + public abstract MessageBlockType getType(); + + //This method returns raw message representation + //It should be rewritten in a derived class if raw representation of a concrete message + //is something else than just message type concatenated with message data + public byte[] getRawData() { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + try { + stream.write(this.getType().getValue()); + stream.write((byte) encodedData.length); + stream.write(encodedData); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + return stream.toByteArray(); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/NonceResyncableMessageBlock.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/NonceResyncableMessageBlock.java new file mode 100644 index 0000000000..6170164366 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/NonceResyncableMessageBlock.java @@ -0,0 +1,7 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message; + +public abstract class NonceResyncableMessageBlock extends MessageBlock { + public abstract int getNonce(); + + public abstract void setNonce(int nonce); +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/OmnipodMessage.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/OmnipodMessage.java new file mode 100644 index 0000000000..36d4268cd0 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/OmnipodMessage.java @@ -0,0 +1,128 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationService; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; +import info.nightscout.androidaps.plugins.pump.omnipod.exception.CrcMismatchException; +import info.nightscout.androidaps.plugins.pump.omnipod.exception.NotEnoughDataException; +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmniCRC; + +public class OmnipodMessage { + + private static final Logger LOG = LoggerFactory.getLogger(OmnipodCommunicationService.class); + private final int address; + private final List messageBlocks; + private final int sequenceNumber; + + public OmnipodMessage(int address, List messageBlocks, int sequenceNumber) { + this.address = address; + this.messageBlocks = messageBlocks; + this.sequenceNumber = sequenceNumber; + } + + public static OmnipodMessage decodeMessage(byte[] data) { + if (data.length < 10) { + throw new NotEnoughDataException("Not enough data"); + } + + int address = ByteUtil.toInt((int) data[0], (int) data[1], (int) data[2], + (int) data[3], ByteUtil.BitConversion.BIG_ENDIAN); + byte b9 = data[4]; + int bodyLength = ByteUtil.convertUnsignedByteToInt(data[5]); + if (data.length - 8 < bodyLength) { + throw new NotEnoughDataException("not enough data: " + ByteUtil.shortHexString(data)); + } + int sequenceNumber = (((int) b9 >> 2) & 0b11111); + int crc = ByteUtil.toInt(data[data.length - 2], data[data.length - 1]); + int calculatedCrc = OmniCRC.crc16(ByteUtil.substring(data, 0, data.length - 2)); + if (crc != calculatedCrc) { + throw new CrcMismatchException("CRC mismatch"); + } + List blocks = decodeBlocks(ByteUtil.substring(data, 6, data.length - 6 - 2)); + if (blocks == null || blocks.size() == 0) { + throw new OmnipodException("No blocks decoded"); + } + + OmnipodMessage result = new OmnipodMessage(address, blocks, sequenceNumber); + return result; + } + + private static List decodeBlocks(byte[] data) { + List blocks = new ArrayList<>(); + int index = 0; + while (index < data.length) { + try { + MessageBlockType blockType = MessageBlockType.fromByte(data[index]); + MessageBlock block = blockType.decode(ByteUtil.substring(data, index)); + blocks.add(block); + int blockLength = block.getRawData().length; + index += blockLength; + } catch (Exception ex) { + throw new OmnipodException("Failed to decode blocks", ex); + } + } + + return blocks; + } + + public byte[] getEncoded() { + byte[] encodedData = new byte[0]; + for (MessageBlock messageBlock : messageBlocks) { + encodedData = ByteUtil.concat(encodedData, messageBlock.getRawData()); + } + + byte[] header = new byte[0]; + //right before the message blocks we have 6 bits of seqNum and 10 bits of length + header = ByteUtil.concat(header, ByteUtil.getBytesFromInt(address)); + header = ByteUtil.concat(header, (byte) (((sequenceNumber & 0x1F) << 2) + ((encodedData.length >> 8) & 0x03))); + header = ByteUtil.concat(header, (byte) (encodedData.length & 0xFF)); + encodedData = ByteUtil.concat(header, encodedData); + String myString = ByteUtil.shortHexString(encodedData); + int crc = OmniCRC.crc16(encodedData); + encodedData = ByteUtil.concat(encodedData, ByteUtil.substring(ByteUtil.getBytesFromInt(crc), 2, 2)); + return encodedData; + } + + public List getMessageBlocks() { + return messageBlocks; + } + + public int getSequenceNumber() { + return sequenceNumber; + } + + @Override + public String toString() { + return "OmnipodMessage{" + + "address=" + address + + ", encoded=" + ByteUtil.shortHexString(getEncoded()) + + ", sequenceNumber=" + sequenceNumber + + '}'; + } + + public boolean isNonceResyncable() { + return messageBlocks.size() > 0 && (messageBlocks.get(0) instanceof NonceResyncableMessageBlock); + } + + public int getSentNonce() { + if (!isNonceResyncable()) { + throw new UnsupportedOperationException("Message is not nonce resyncable"); + } + return ((NonceResyncableMessageBlock) messageBlocks.get(0)).getNonce(); + } + + public void resyncNonce(int nonce) { + for (MessageBlock messageBlock : messageBlocks) { + if (messageBlock instanceof NonceResyncableMessageBlock) { + ((NonceResyncableMessageBlock) messageBlock).setNonce(nonce); + } + } + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/OmnipodPacket.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/OmnipodPacket.java new file mode 100644 index 0000000000..714c0abb4b --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/OmnipodPacket.java @@ -0,0 +1,85 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message; + +import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.ble.data.RLMessage; +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PacketType; +import info.nightscout.androidaps.plugins.pump.omnipod.exception.CrcMismatchException; +import info.nightscout.androidaps.plugins.pump.omnipod.exception.OmnipodException; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmniCRC; + +/** + * Created by andy on 6/1/18. + */ +public class OmnipodPacket implements RLMessage { + private int packetAddress = 0; + private PacketType packetType = PacketType.INVALID; + private int sequenceNumber = 0; + private byte[] encodedMessage = null; + private boolean valid = false; + + public OmnipodPacket(byte[] encoded) { + if (encoded.length < 7) { + return; + } + this.packetAddress = ByteUtil.toInt((int) encoded[0], (int) encoded[1], + (int) encoded[2], (int) encoded[3], ByteUtil.BitConversion.BIG_ENDIAN); + try { + this.packetType = PacketType.fromByte((byte) (((int) encoded[4] & 0xFF) >> 5)); + } catch (IllegalArgumentException ex) { + throw new OmnipodException("Invalid packet type", ex); + } + this.sequenceNumber = (encoded[4] & 0b11111); + byte crc = OmniCRC.crc8(ByteUtil.substring(encoded, 0, encoded.length - 1)); + if (crc != encoded[encoded.length - 1]) { + throw new CrcMismatchException("CRC mismatch: " + + ByteUtil.shortHexString(new byte[]{crc}) + " <> " + + ByteUtil.shortHexString(new byte[]{encoded[encoded.length - 1]}) + + " (packetType=" + packetType.name() + ",packetLength=" + encoded.length + ")"); + } + this.encodedMessage = ByteUtil.substring(encoded, 5, encoded.length - 1 - 5); + valid = true; + } + + public OmnipodPacket(int packetAddress, PacketType packetType, int packetNumber, byte[] encodedMessage) { + this.packetAddress = packetAddress; + this.packetType = packetType; + this.sequenceNumber = packetNumber; + this.encodedMessage = encodedMessage; + if (encodedMessage.length > packetType.getMaxBodyLength()) { + this.encodedMessage = ByteUtil.substring(encodedMessage, 0, packetType.getMaxBodyLength()); + } + this.valid = true; + } + + public PacketType getPacketType() { + return packetType; + } + + public int getAddress() { + return packetAddress; + } + + public int getSequenceNumber() { + return sequenceNumber; + } + + public byte[] getEncodedMessage() { + return encodedMessage; + } + + @Override + public byte[] getTxData() { + byte[] output = new byte[0]; + output = ByteUtil.concat(output, ByteUtil.getBytesFromInt(this.packetAddress)); + output = ByteUtil.concat(output, (byte) ((this.packetType.getValue() << 5) + (sequenceNumber & 0b11111))); + output = ByteUtil.concat(output, encodedMessage); + output = ByteUtil.concat(output, OmniCRC.crc8(output)); + return output; + } + + @Override + public boolean isValid() { + return valid; + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/AcknowledgeAlertsCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/AcknowledgeAlertsCommand.java new file mode 100644 index 0000000000..546bd4c5df --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/AcknowledgeAlertsCommand.java @@ -0,0 +1,46 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import java.util.Collections; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.NonceResyncableMessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertSet; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertSlot; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; + +public class AcknowledgeAlertsCommand extends NonceResyncableMessageBlock { + + private final AlertSet alerts; + private int nonce; + + public AcknowledgeAlertsCommand(int nonce, AlertSet alerts) { + this.nonce = nonce; + this.alerts = alerts; + encode(); + } + + public AcknowledgeAlertsCommand(int nonce, AlertSlot alertSlot) { + this(nonce, new AlertSet(Collections.singletonList(alertSlot))); + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.ACKNOWLEDGE_ALERT; + } + + private void encode() { + encodedData = ByteUtil.getBytesFromInt(nonce); + encodedData = ByteUtil.concat(encodedData, alerts.getRawValue()); + } + + @Override + public int getNonce() { + return nonce; + } + + @Override + public void setNonce(int nonce) { + this.nonce = nonce; + encode(); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/AssignAddressCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/AssignAddressCommand.java new file mode 100644 index 0000000000..b69c418ef0 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/AssignAddressCommand.java @@ -0,0 +1,25 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import java.nio.ByteBuffer; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; + +public class AssignAddressCommand extends MessageBlock { + private final int address; + + public AssignAddressCommand(int address) { + this.address = address; + encodedData = ByteBuffer.allocate(4).putInt(this.address).array(); + } + + public int getAddress() { + return address; + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.ASSIGN_ADDRESS; + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BasalScheduleExtraCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BasalScheduleExtraCommand.java new file mode 100644 index 0000000000..175b17e809 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BasalScheduleExtraCommand.java @@ -0,0 +1,114 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import org.joda.time.Duration; + +import java.util.ArrayList; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BasalSchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.RateEntry; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class BasalScheduleExtraCommand extends MessageBlock { + private final boolean acknowledgementBeep; + private final boolean completionBeep; + private final Duration programReminderInterval; + private final byte currentEntryIndex; + private final double remainingPulses; + // We use a double for the delay between pulses because the Joda time API lacks precision for our calculations + private final double delayUntilNextTenthOfPulseInSeconds; + private final List rateEntries; + + public BasalScheduleExtraCommand(boolean acknowledgementBeep, boolean completionBeep, + Duration programReminderInterval, byte currentEntryIndex, + double remainingPulses, double delayUntilNextTenthOfPulseInSeconds, List rateEntries) { + + this.acknowledgementBeep = acknowledgementBeep; + this.completionBeep = completionBeep; + this.programReminderInterval = programReminderInterval; + this.currentEntryIndex = currentEntryIndex; + this.remainingPulses = remainingPulses; + this.delayUntilNextTenthOfPulseInSeconds = delayUntilNextTenthOfPulseInSeconds; + this.rateEntries = rateEntries; + encode(); + } + + public BasalScheduleExtraCommand(BasalSchedule schedule, Duration scheduleOffset, + boolean acknowledgementBeep, boolean completionBeep, Duration programReminderInterval) { + rateEntries = new ArrayList<>(); + this.acknowledgementBeep = acknowledgementBeep; + this.completionBeep = completionBeep; + this.programReminderInterval = programReminderInterval; + Duration scheduleOffsetNearestSecond = Duration.standardSeconds(Math.round(scheduleOffset.getMillis() / 1000.0)); + + BasalSchedule mergedSchedule = new BasalSchedule(schedule.adjacentEqualRatesMergedEntries()); + List durations = mergedSchedule.getDurations(); + + for (BasalSchedule.BasalScheduleDurationEntry entry : durations) { + rateEntries.addAll(RateEntry.createEntries(entry.getRate(), entry.getDuration())); + } + + BasalSchedule.BasalScheduleLookupResult entryLookupResult = mergedSchedule.lookup(scheduleOffsetNearestSecond); + currentEntryIndex = (byte) entryLookupResult.getIndex(); + double timeRemainingInEntryInSeconds = entryLookupResult.getStartTime().minus(scheduleOffsetNearestSecond.minus(entryLookupResult.getDuration())).getMillis() / 1000.0; + double rate = mergedSchedule.rateAt(scheduleOffsetNearestSecond); + int pulsesPerHour = (int) Math.round(rate / OmnipodConst.POD_PULSE_SIZE); + double timeBetweenPulses = 3600.0 / pulsesPerHour; + delayUntilNextTenthOfPulseInSeconds = (timeRemainingInEntryInSeconds % (timeBetweenPulses / 10.0)); + remainingPulses = pulsesPerHour * (timeRemainingInEntryInSeconds - delayUntilNextTenthOfPulseInSeconds) / 3600.0 + 0.1; + + encode(); + } + + private void encode() { + byte beepOptions = (byte) ((programReminderInterval.getStandardMinutes() & 0x3f) + (completionBeep ? 1 << 6 : 0) + (acknowledgementBeep ? 1 << 7 : 0)); + + encodedData = new byte[]{ + beepOptions, + currentEntryIndex + }; + + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt16((int) Math.round(remainingPulses * 10))); + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt((int) Math.round(delayUntilNextTenthOfPulseInSeconds * 1000 * 1000))); + + for (RateEntry entry : rateEntries) { + encodedData = ByteUtil.concat(encodedData, entry.getRawData()); + } + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.BASAL_SCHEDULE_EXTRA; + } + + public boolean isAcknowledgementBeep() { + return acknowledgementBeep; + } + + public boolean isCompletionBeep() { + return completionBeep; + } + + public Duration getProgramReminderInterval() { + return programReminderInterval; + } + + public byte getCurrentEntryIndex() { + return currentEntryIndex; + } + + public double getRemainingPulses() { + return remainingPulses; + } + + public double getDelayUntilNextTenthOfPulseInSeconds() { + return delayUntilNextTenthOfPulseInSeconds; + } + + public List getRateEntries() { + return new ArrayList<>(rateEntries); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BeepConfigCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BeepConfigCommand.java new file mode 100644 index 0000000000..6e77a7f5d1 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BeepConfigCommand.java @@ -0,0 +1,48 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import org.joda.time.Duration; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.BeepType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; + +public class BeepConfigCommand extends MessageBlock { + private final BeepType beepType; + private final boolean basalCompletionBeep; + private final Duration basalIntervalBeep; + private final boolean tempBasalCompletionBeep; + private final Duration tempBasalIntervalBeep; + private final boolean bolusCompletionBeep; + private final Duration bolusIntervalBeep; + + public BeepConfigCommand(BeepType beepType, boolean basalCompletionBeep, Duration basalIntervalBeep, + boolean tempBasalCompletionBeep, Duration tempBasalIntervalBeep, + boolean bolusCompletionBeep, Duration bolusIntervalBeep) { + this.beepType = beepType; + this.basalCompletionBeep = basalCompletionBeep; + this.basalIntervalBeep = basalIntervalBeep; + this.tempBasalCompletionBeep = tempBasalCompletionBeep; + this.tempBasalIntervalBeep = tempBasalIntervalBeep; + this.bolusCompletionBeep = bolusCompletionBeep; + this.bolusIntervalBeep = bolusIntervalBeep; + + encode(); + } + + public BeepConfigCommand(BeepType beepType) { + this(beepType, false, Duration.ZERO, false, Duration.ZERO, false, Duration.ZERO); + } + + private void encode() { + encodedData = new byte[]{beepType.getValue()}; + encodedData = ByteUtil.concat(encodedData, (byte) ((basalCompletionBeep ? (1 << 6) : 0) + (basalIntervalBeep.getStandardMinutes() & 0x3f))); + encodedData = ByteUtil.concat(encodedData, (byte) ((tempBasalCompletionBeep ? (1 << 6) : 0) + (tempBasalIntervalBeep.getStandardMinutes() & 0x3f))); + encodedData = ByteUtil.concat(encodedData, (byte) ((bolusCompletionBeep ? (1 << 6) : 0) + (bolusIntervalBeep.getStandardMinutes() & 0x3f))); + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.BEEP_CONFIG; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BolusExtraCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BolusExtraCommand.java new file mode 100644 index 0000000000..6e2a5cbbbb --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BolusExtraCommand.java @@ -0,0 +1,62 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import org.joda.time.Duration; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class BolusExtraCommand extends MessageBlock { + private final boolean acknowledgementBeep; + private final boolean completionBeep; + private final Duration programReminderInterval; + private final double units; + private final Duration timeBetweenPulses; + private final double squareWaveUnits; + private final Duration squareWaveDuration; + + public BolusExtraCommand(double units, boolean acknowledgementBeep, boolean completionBeep) { + this(units, Duration.standardSeconds(2), acknowledgementBeep, completionBeep); + } + + public BolusExtraCommand(double units, Duration timeBetweenPulses, boolean acknowledgementBeep, boolean completionBeep) { + this(units, 0.0, Duration.ZERO, acknowledgementBeep, completionBeep, Duration.ZERO, timeBetweenPulses); + } + + public BolusExtraCommand(double units, double squareWaveUnits, Duration squareWaveDuration, + boolean acknowledgementBeep, boolean completionBeep, + Duration programReminderInterval, Duration timeBetweenPulses) { + if (units <= 0D) { + throw new IllegalArgumentException("Units should be > 0"); + } else if (units > OmnipodConst.MAX_BOLUS) { + throw new IllegalArgumentException("Units exceeds max bolus"); + } + this.units = units; + this.squareWaveUnits = squareWaveUnits; + this.squareWaveDuration = squareWaveDuration; + this.acknowledgementBeep = acknowledgementBeep; + this.completionBeep = completionBeep; + this.programReminderInterval = programReminderInterval; + this.timeBetweenPulses = timeBetweenPulses; + encode(); + } + + private void encode() { + byte beepOptions = (byte) ((programReminderInterval.getStandardMinutes() & 0x3f) + (completionBeep ? 1 << 6 : 0) + (acknowledgementBeep ? 1 << 7 : 0)); + + int squareWavePulseCountCountX10 = (int) Math.round(squareWaveUnits * 200); + int timeBetweenExtendedPulses = squareWavePulseCountCountX10 > 0 ? (int) squareWaveDuration.getMillis() * 100 / squareWavePulseCountCountX10 : 0; + + encodedData = ByteUtil.concat(encodedData, beepOptions); + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt16((int) Math.round(units * 200))); + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt((int) timeBetweenPulses.getMillis() * 100)); + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt16(squareWavePulseCountCountX10)); + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt(timeBetweenExtendedPulses)); + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.BOLUS_EXTRA; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/CancelDeliveryCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/CancelDeliveryCommand.java new file mode 100644 index 0000000000..bd950c200f --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/CancelDeliveryCommand.java @@ -0,0 +1,62 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import java.util.EnumSet; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.NonceResyncableMessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.BeepType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.DeliveryType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; + +public class CancelDeliveryCommand extends NonceResyncableMessageBlock { + + private final BeepType beepType; + private final EnumSet deliveryTypes; + private int nonce; + + public CancelDeliveryCommand(int nonce, BeepType beepType, EnumSet deliveryTypes) { + this.nonce = nonce; + this.beepType = beepType; + this.deliveryTypes = deliveryTypes; + encode(); + } + + public CancelDeliveryCommand(int nonce, BeepType beepType, DeliveryType deliveryType) { + this(nonce, beepType, EnumSet.of(deliveryType)); + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.CANCEL_DELIVERY; + } + + private void encode() { + encodedData = new byte[5]; + System.arraycopy(ByteUtil.getBytesFromInt(nonce), 0, encodedData, 0, 4); + byte beepTypeValue = beepType.getValue(); + if (beepTypeValue > 8) { + beepTypeValue = 0; + } + encodedData[4] = (byte) ((beepTypeValue & 0x0F) << 4); + if (deliveryTypes.contains(DeliveryType.BASAL)) { + encodedData[4] |= 1; + } + if (deliveryTypes.contains(DeliveryType.TEMP_BASAL)) { + encodedData[4] |= 2; + } + if (deliveryTypes.contains(DeliveryType.BOLUS)) { + encodedData[4] |= 4; + } + } + + @Override + public int getNonce() { + return nonce; + } + + @Override + public void setNonce(int nonce) { + this.nonce = nonce; + encode(); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/ConfigureAlertsCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/ConfigureAlertsCommand.java new file mode 100644 index 0000000000..82c27d28cd --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/ConfigureAlertsCommand.java @@ -0,0 +1,42 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.NonceResyncableMessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertConfiguration; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; + +public class ConfigureAlertsCommand extends NonceResyncableMessageBlock { + private final List configurations; + private int nonce; + + public ConfigureAlertsCommand(int nonce, List configurations) { + this.nonce = nonce; + this.configurations = configurations; + encode(); + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.CONFIGURE_ALERTS; + } + + private void encode() { + encodedData = ByteUtil.getBytesFromInt(nonce); + for (AlertConfiguration config : configurations) { + encodedData = ByteUtil.concat(encodedData, config.getRawData()); + } + } + + @Override + public int getNonce() { + return nonce; + } + + @Override + public void setNonce(int nonce) { + this.nonce = nonce; + encode(); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/ConfigurePodCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/ConfigurePodCommand.java new file mode 100644 index 0000000000..7962bd710f --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/ConfigurePodCommand.java @@ -0,0 +1,46 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import org.joda.time.DateTime; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; + +public class ConfigurePodCommand extends MessageBlock { + + private static final byte PACKET_TIMEOUT_LIMIT = 0x04; + + private final int lot; + private final int tid; + private final DateTime date; + private final int address; + + public ConfigurePodCommand(int address, DateTime date, int lot, int tid) { + this.address = address; + this.lot = lot; + this.tid = tid; + this.date = date; + encode(); + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.SETUP_POD; + } + + private void encode() { + encodedData = new byte[0]; + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt(address)); + encodedData = ByteUtil.concat(encodedData, new byte[]{ // + (byte) 0x14, // unknown + PACKET_TIMEOUT_LIMIT, // + (byte) date.monthOfYear().get(), // + (byte) date.dayOfMonth().get(), // + (byte) (date.year().get() - 2000), // + (byte) date.hourOfDay().get(), // + (byte) date.minuteOfHour().get() // + }); + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt(lot)); + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt(tid)); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/DeactivatePodCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/DeactivatePodCommand.java new file mode 100644 index 0000000000..4c0cf4ad2b --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/DeactivatePodCommand.java @@ -0,0 +1,34 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.NonceResyncableMessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; + +public class DeactivatePodCommand extends NonceResyncableMessageBlock { + private int nonce; + + public DeactivatePodCommand(int nonce) { + this.nonce = nonce; + encode(); + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.DEACTIVATE_POD; + } + + private void encode() { + encodedData = ByteUtil.getBytesFromInt(nonce); + } + + @Override + public int getNonce() { + return nonce; + } + + @Override + public void setNonce(int nonce) { + this.nonce = nonce; + encode(); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/FaultConfigCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/FaultConfigCommand.java new file mode 100644 index 0000000000..d342210119 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/FaultConfigCommand.java @@ -0,0 +1,41 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.NonceResyncableMessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; + +public class FaultConfigCommand extends NonceResyncableMessageBlock { + private final byte tab5sub16; + private final byte tab5sub17; + private int nonce; + + public FaultConfigCommand(int nonce, byte tab5sub16, byte tab5sub17) { + this.nonce = nonce; + this.tab5sub16 = tab5sub16; + this.tab5sub17 = tab5sub17; + + encode(); + } + + private void encode() { + encodedData = ByteUtil.getBytesFromInt(nonce); + encodedData = ByteUtil.concat(encodedData, tab5sub16); + encodedData = ByteUtil.concat(encodedData, tab5sub17); + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.FAULT_CONFIG; + } + + @Override + public int getNonce() { + return nonce; + } + + @Override + public void setNonce(int nonce) { + this.nonce = nonce; + encode(); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/GetStatusCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/GetStatusCommand.java new file mode 100644 index 0000000000..e49ac08e9d --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/GetStatusCommand.java @@ -0,0 +1,23 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; + +public class GetStatusCommand extends MessageBlock { + private final PodInfoType podInfoType; + + public GetStatusCommand(PodInfoType podInfoType) { + this.podInfoType = podInfoType; + encode(); + } + + private void encode() { + encodedData = new byte[]{podInfoType.getValue()}; + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.GET_STATUS; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/SetInsulinScheduleCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/SetInsulinScheduleCommand.java new file mode 100644 index 0000000000..f1c28762b3 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/SetInsulinScheduleCommand.java @@ -0,0 +1,89 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import org.joda.time.Duration; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.NonceResyncableMessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BasalDeliverySchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BasalDeliveryTable; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BasalSchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BolusDeliverySchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.DeliverySchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.TempBasalDeliverySchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class SetInsulinScheduleCommand extends NonceResyncableMessageBlock { + + private final DeliverySchedule schedule; + private int nonce; + + // Bolus + public SetInsulinScheduleCommand(int nonce, BolusDeliverySchedule schedule) { + this.nonce = nonce; + this.schedule = schedule; + encode(); + } + + // Basal schedule + public SetInsulinScheduleCommand(int nonce, BasalSchedule schedule, Duration scheduleOffset) { + int scheduleOffsetInSeconds = (int) scheduleOffset.getStandardSeconds(); + + BasalDeliveryTable table = new BasalDeliveryTable(schedule); + double rate = schedule.rateAt(scheduleOffset); + byte segment = (byte) (scheduleOffsetInSeconds / BasalDeliveryTable.SEGMENT_DURATION); + int segmentOffset = scheduleOffsetInSeconds % BasalDeliveryTable.SEGMENT_DURATION; + + int timeRemainingInSegment = BasalDeliveryTable.SEGMENT_DURATION - segmentOffset; + + double timeBetweenPulses = 3600 / (rate / OmnipodConst.POD_PULSE_SIZE); + + double offsetToNextTenth = timeRemainingInSegment % (timeBetweenPulses / 10.0); + + int pulsesRemainingInSegment = (int) ((timeRemainingInSegment + timeBetweenPulses / 10.0 - offsetToNextTenth) / timeBetweenPulses); + + this.nonce = nonce; + this.schedule = new BasalDeliverySchedule(segment, timeRemainingInSegment, pulsesRemainingInSegment, table); + encode(); + } + + // Temp basal + public SetInsulinScheduleCommand(int nonce, double tempBasalRate, Duration duration) { + if (tempBasalRate < 0D) { + throw new IllegalArgumentException("Rate should be >= 0"); + } else if (tempBasalRate > OmnipodConst.MAX_BASAL_RATE) { + throw new IllegalArgumentException("Rate exceeds max basal rate"); + } + if (duration.isLongerThan(OmnipodConst.MAX_TEMP_BASAL_DURATION)) { + throw new IllegalArgumentException("Duration exceeds max temp basal duration"); + } + int pulsesPerHour = (int) Math.round(tempBasalRate / OmnipodConst.POD_PULSE_SIZE); + int pulsesPerSegment = pulsesPerHour / 2; + this.nonce = nonce; + this.schedule = new TempBasalDeliverySchedule(BasalDeliveryTable.SEGMENT_DURATION, pulsesPerSegment, new BasalDeliveryTable(tempBasalRate, duration)); + encode(); + } + + private void encode() { + encodedData = ByteUtil.getBytesFromInt(nonce); + encodedData = ByteUtil.concat(encodedData, schedule.getType().getValue()); + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt16(schedule.getChecksum())); + encodedData = ByteUtil.concat(encodedData, schedule.getRawData()); + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.SET_INSULIN_SCHEDULE; + } + + @Override + public int getNonce() { + return nonce; + } + + @Override + public void setNonce(int nonce) { + this.nonce = nonce; + encode(); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/TempBasalExtraCommand.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/TempBasalExtraCommand.java new file mode 100644 index 0000000000..3d072ebc96 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/TempBasalExtraCommand.java @@ -0,0 +1,95 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import org.joda.time.Duration; + +import java.util.ArrayList; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.RateEntry; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class TempBasalExtraCommand extends MessageBlock { + private final boolean acknowledgementBeep; + private final boolean completionBeep; + private final Duration programReminderInterval; + private final double remainingPulses; + // We use a double for the delay until next pulse because the Joda time API lacks precision for our calculations + private final double delayUntilNextPulse; + private final List rateEntries; + + public TempBasalExtraCommand(double rate, Duration duration, boolean acknowledgementBeep, boolean completionBeep, + Duration programReminderInterval) { + if (rate < 0D) { + throw new IllegalArgumentException("Rate should be >= 0"); + } else if (rate > OmnipodConst.MAX_BASAL_RATE) { + throw new IllegalArgumentException("Rate exceeds max basal rate"); + } + if (duration.isLongerThan(OmnipodConst.MAX_TEMP_BASAL_DURATION)) { + throw new IllegalArgumentException("Duration exceeds max temp basal duration"); + } + + this.acknowledgementBeep = acknowledgementBeep; + this.completionBeep = completionBeep; + this.programReminderInterval = programReminderInterval; + + rateEntries = RateEntry.createEntries(rate, duration); + + RateEntry currentRateEntry = rateEntries.get(0); + remainingPulses = currentRateEntry.getTotalPulses(); + delayUntilNextPulse = currentRateEntry.getDelayBetweenPulsesInSeconds(); + + encode(); + } + + private void encode() { + byte beepOptions = (byte) ((programReminderInterval.getStandardMinutes() & 0x3f) + (completionBeep ? 1 << 6 : 0) + (acknowledgementBeep ? 1 << 7 : 0)); + + encodedData = new byte[]{ + beepOptions, + (byte) 0x00 + }; + + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt16((int) Math.round(remainingPulses * 10))); + if (remainingPulses == 0) { + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt((int) (delayUntilNextPulse * 1000 * 100) * 10)); + } else { + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt((int) (delayUntilNextPulse * 1000 * 100))); + } + + for (RateEntry entry : rateEntries) { + encodedData = ByteUtil.concat(encodedData, entry.getRawData()); + } + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.TEMP_BASAL_EXTRA; + } + + public boolean isAcknowledgementBeep() { + return acknowledgementBeep; + } + + public boolean isCompletionBeep() { + return completionBeep; + } + + public Duration getProgramReminderInterval() { + return programReminderInterval; + } + + public double getRemainingPulses() { + return remainingPulses; + } + + public double getDelayUntilNextPulse() { + return delayUntilNextPulse; + } + + public List getRateEntries() { + return new ArrayList<>(rateEntries); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/ErrorResponse.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/ErrorResponse.java new file mode 100644 index 0000000000..18f02475e1 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/ErrorResponse.java @@ -0,0 +1,50 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.ErrorResponseType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; + +public class ErrorResponse extends MessageBlock { + private static final int MESSAGE_LENGTH = 5; + + private final ErrorResponseType errorResponseType; + private final int nonceSearchKey; + + public ErrorResponse(byte[] encodedData) { + if (encodedData.length < MESSAGE_LENGTH) { + throw new IllegalArgumentException("Not enough data"); + } + this.encodedData = ByteUtil.substring(encodedData, 2, MESSAGE_LENGTH - 2); + + ErrorResponseType errorResponseType = null; + try { + errorResponseType = ErrorResponseType.fromByte(encodedData[2]); + } catch (IllegalArgumentException ex) { + } + + this.errorResponseType = errorResponseType; + this.nonceSearchKey = ByteUtil.makeUnsignedShort((int) encodedData[3], (int) encodedData[4]); + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.ERROR_RESPONSE; + } + + public ErrorResponseType getErrorResponseType() { + return errorResponseType; + } + + public int getNonceSearchKey() { + return nonceSearchKey; + } + + @Override + public String toString() { + return "ErrorResponse{" + + "errorResponseType=" + errorResponseType + + ", nonceSearchKey=" + nonceSearchKey + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/StatusResponse.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/StatusResponse.java new file mode 100644 index 0000000000..8a2e64fdd3 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/StatusResponse.java @@ -0,0 +1,119 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response; + +import org.joda.time.Duration; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertSet; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.DeliveryStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodProgressStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class StatusResponse extends MessageBlock { + private static final int MESSAGE_LENGTH = 10; + + private final DeliveryStatus deliveryStatus; + private final PodProgressStatus podProgressStatus; + private final Duration timeActive; + private final Double reservoirLevel; + private final double insulin; + private final double insulinNotDelivered; + private final byte podMessageCounter; + private final AlertSet alerts; + + public StatusResponse(byte[] encodedData) { + if (encodedData.length < MESSAGE_LENGTH) { + throw new IllegalArgumentException("Not enough data"); + } + this.encodedData = ByteUtil.substring(encodedData, 1, MESSAGE_LENGTH - 1); + + this.deliveryStatus = DeliveryStatus.fromByte((byte) (ByteUtil.convertUnsignedByteToInt(encodedData[1]) >>> 4)); + this.podProgressStatus = PodProgressStatus.fromByte((byte) (encodedData[1] & 0x0F)); + + int minutes = ((encodedData[7] & 0x7F) << 6) | ((encodedData[8] & 0xFC) >>> 2); + this.timeActive = Duration.standardMinutes(minutes); + + int highInsulinBits = (encodedData[2] & 0xF) << 9; + int middleInsulinBits = ByteUtil.convertUnsignedByteToInt(encodedData[3]) << 1; + int lowInsulinBits = ByteUtil.convertUnsignedByteToInt(encodedData[4]) >>> 7; + this.insulin = OmnipodConst.POD_PULSE_SIZE * (highInsulinBits | middleInsulinBits | lowInsulinBits); + this.podMessageCounter = (byte) ((encodedData[4] >>> 3) & 0xf); + + this.insulinNotDelivered = OmnipodConst.POD_PULSE_SIZE * (((encodedData[4] & 0x03) << 8) | ByteUtil.convertUnsignedByteToInt(encodedData[5])); + this.alerts = new AlertSet((byte) (((encodedData[6] & 0x7f) << 1) | (ByteUtil.convertUnsignedByteToInt(encodedData[7]) >>> 7))); + + double reservoirValue = (((encodedData[8] & 0x3) << 8) + ByteUtil.convertUnsignedByteToInt(encodedData[9])) * OmnipodConst.POD_PULSE_SIZE; + if (reservoirValue > OmnipodConst.MAX_RESERVOIR_READING) { + reservoirLevel = null; + } else { + reservoirLevel = reservoirValue; + } + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.STATUS_RESPONSE; + } + + public DeliveryStatus getDeliveryStatus() { + return deliveryStatus; + } + + public PodProgressStatus getPodProgressStatus() { + return podProgressStatus; + } + + public Duration getTimeActive() { + return timeActive; + } + + public Double getReservoirLevel() { + return reservoirLevel; + } + + public double getInsulin() { + return insulin; + } + + public double getInsulinNotDelivered() { + return insulinNotDelivered; + } + + public byte getPodMessageCounter() { + return podMessageCounter; + } + + public AlertSet getAlerts() { + return alerts; + } + + public byte[] getRawData() { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + try { + stream.write(this.getType().getValue()); + stream.write(encodedData); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + return stream.toByteArray(); + } + + @Override + public String toString() { + return "StatusResponse{" + + "deliveryStatus=" + deliveryStatus + + ", podProgressStatus=" + podProgressStatus + + ", timeActive=" + timeActive + + ", reservoirLevel=" + reservoirLevel + + ", insulin=" + insulin + + ", insulinNotDelivered=" + insulinNotDelivered + + ", podMessageCounter=" + podMessageCounter + + ", alerts=" + alerts + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/VersionResponse.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/VersionResponse.java new file mode 100644 index 0000000000..a8c9e9403d --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/VersionResponse.java @@ -0,0 +1,91 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.FirmwareVersion; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodProgressStatus; + +public class VersionResponse extends MessageBlock { + private final PodProgressStatus podProgressStatus; + private final FirmwareVersion pmVersion; + private final FirmwareVersion piVersion; + private final int lot; + private final int tid; + private final int address; + + public VersionResponse(byte[] encodedData) { + int length = ByteUtil.convertUnsignedByteToInt(encodedData[1]) + 2; + this.encodedData = ByteUtil.substring(encodedData, 2, length - 2); + + boolean extraByte; + byte[] truncatedData; + + switch (length) { + case 0x17: + truncatedData = ByteUtil.substring(encodedData, 2); + extraByte = true; + break; + case 0x1D: + truncatedData = ByteUtil.substring(encodedData, 9); + extraByte = false; + break; + default: + throw new IllegalArgumentException("Unrecognized VersionResponse message length: " + length); + } + + this.podProgressStatus = PodProgressStatus.fromByte(truncatedData[7]); + this.pmVersion = new FirmwareVersion(truncatedData[0], truncatedData[1], truncatedData[2]); + this.piVersion = new FirmwareVersion(truncatedData[3], truncatedData[4], truncatedData[5]); + this.lot = ByteUtil.toInt((int) truncatedData[8], (int) truncatedData[9], + (int) truncatedData[10], (int) truncatedData[11], ByteUtil.BitConversion.BIG_ENDIAN); + this.tid = ByteUtil.toInt((int) truncatedData[12], (int) truncatedData[13], + (int) truncatedData[14], (int) truncatedData[15], ByteUtil.BitConversion.BIG_ENDIAN); + + int indexIncrementor = extraByte ? 1 : 0; + + this.address = ByteUtil.toInt((int) truncatedData[16 + indexIncrementor], (int) truncatedData[17 + indexIncrementor], + (int) truncatedData[18 + indexIncrementor], (int) truncatedData[19 + indexIncrementor], ByteUtil.BitConversion.BIG_ENDIAN); + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.VERSION_RESPONSE; + } + + public PodProgressStatus getPodProgressStatus() { + return podProgressStatus; + } + + public FirmwareVersion getPmVersion() { + return pmVersion; + } + + public FirmwareVersion getPiVersion() { + return piVersion; + } + + public int getLot() { + return lot; + } + + public int getTid() { + return tid; + } + + public int getAddress() { + return address; + } + + @Override + public String toString() { + return "VersionResponse{" + + "podProgressStatus=" + podProgressStatus + + ", pmVersion=" + pmVersion + + ", piVersion=" + piVersion + + ", lot=" + lot + + ", tid=" + tid + + ", address=" + address + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfo.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfo.java new file mode 100644 index 0000000000..9002f0d92e --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfo.java @@ -0,0 +1,17 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo; + +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; + +public abstract class PodInfo { + private final byte[] encodedData; + + public PodInfo(byte[] encodedData) { + this.encodedData = encodedData; + } + + public abstract PodInfoType getType(); + + public byte[] getEncodedData() { + return encodedData; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoActiveAlerts.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoActiveAlerts.java new file mode 100644 index 0000000000..1f32777fda --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoActiveAlerts.java @@ -0,0 +1,92 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo; + +import org.joda.time.Duration; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertSlot; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class PodInfoActiveAlerts extends PodInfo { + private static final int MINIMUM_MESSAGE_LENGTH = 11; + + private final byte[] word278; // Unknown use + private final List alertActivations; + + public PodInfoActiveAlerts(byte[] encodedData) { + super(encodedData); + + if (encodedData.length < MINIMUM_MESSAGE_LENGTH) { + throw new IllegalArgumentException("Not enough data"); + } + + word278 = ByteUtil.substring(encodedData, 1, 2); + + alertActivations = new ArrayList<>(); + + for (AlertSlot alertSlot : AlertSlot.values()) { + int valueHighBits = ByteUtil.convertUnsignedByteToInt(encodedData[3 + alertSlot.getValue() * 2]); + int valueLowBits = ByteUtil.convertUnsignedByteToInt(encodedData[4 + alertSlot.getValue() * 2]); + int value = (valueHighBits << 8) | valueLowBits; + if (value != 0) { + alertActivations.add(new AlertActivation(alertSlot, value)); + } + } + } + + @Override + public PodInfoType getType() { + return PodInfoType.ACTIVE_ALERTS; + } + + public byte[] getWord278() { + return word278; + } + + public List getAlertActivations() { + return new ArrayList<>(alertActivations); + } + + @Override + public String toString() { + return "PodInfoActiveAlerts{" + + "word278=" + Arrays.toString(word278) + + ", alertActivations=" + alertActivations + + '}'; + } + + public static class AlertActivation { + private final AlertSlot alertSlot; + private final int value; + + private AlertActivation(AlertSlot alertSlot, int value) { + this.alertSlot = alertSlot; + this.value = value; + } + + public double getValueAsUnits() { + return value * OmnipodConst.POD_PULSE_SIZE; + } + + public Duration getValueAsDuration() { + return Duration.standardMinutes(value); + } + + public AlertSlot getAlertSlot() { + return alertSlot; + } + + @Override + public String toString() { + return "AlertActivation{" + + "alertSlot=" + alertSlot + + ", valueAsUnits=" + getValueAsUnits() + + ", valueAsDuration=" + getValueAsDuration() + + '}'; + } + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoDataLog.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoDataLog.java new file mode 100644 index 0000000000..b8e28169d3 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoDataLog.java @@ -0,0 +1,83 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo; + +import org.joda.time.Duration; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.FaultEventCode; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; + +public class PodInfoDataLog extends PodInfo { + private static final int MINIMUM_MESSAGE_LENGTH = 8; + private final FaultEventCode faultEventCode; + private final Duration timeFaultEvent; + private final Duration timeSinceActivation; + private final byte dataChunkSize; + private final byte maximumNumberOfDwords; + private final List dwords; + + public PodInfoDataLog(byte[] encodedData, int bodyLength) { + super(encodedData); + + if (encodedData.length < MINIMUM_MESSAGE_LENGTH) { + throw new IllegalArgumentException("Not enough data"); + } + + faultEventCode = FaultEventCode.fromByte(encodedData[1]); + timeFaultEvent = Duration.standardMinutes(ByteUtil.toInt(encodedData[2], encodedData[3])); + timeSinceActivation = Duration.standardMinutes(ByteUtil.toInt(encodedData[4], encodedData[5])); + dataChunkSize = encodedData[6]; + maximumNumberOfDwords = encodedData[7]; + + dwords = new ArrayList<>(); + + int numberOfDwords = (bodyLength - 8) / 4; + for (int i = 0; i < numberOfDwords; i++) { + dwords.add(ByteUtil.substring(encodedData, 8 + (4 * i), 4)); + } + } + + @Override + public PodInfoType getType() { + return PodInfoType.DATA_LOG; + } + + public FaultEventCode getFaultEventCode() { + return faultEventCode; + } + + public Duration getTimeFaultEvent() { + return timeFaultEvent; + } + + public Duration getTimeSinceActivation() { + return timeSinceActivation; + } + + public byte getDataChunkSize() { + return dataChunkSize; + } + + public byte getMaximumNumberOfDwords() { + return maximumNumberOfDwords; + } + + public List getDwords() { + return Collections.unmodifiableList(dwords); + } + + @Override + public String toString() { + return "PodInfoDataLog{" + + "faultEventCode=" + faultEventCode + + ", timeFaultEvent=" + timeFaultEvent + + ", timeSinceActivation=" + timeSinceActivation + + ", dataChunkSize=" + dataChunkSize + + ", maximumNumberOfDwords=" + maximumNumberOfDwords + + ", dwords=" + dwords + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoFaultAndInitializationTime.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoFaultAndInitializationTime.java new file mode 100644 index 0000000000..2fd1d90406 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoFaultAndInitializationTime.java @@ -0,0 +1,54 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo; + +import org.joda.time.DateTime; +import org.joda.time.Duration; + +import info.nightscout.androidaps.plugins.pump.omnipod.defs.FaultEventCode; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; + +public class PodInfoFaultAndInitializationTime extends PodInfo { + private static final int MINIMUM_MESSAGE_LENGTH = 17; + private final FaultEventCode faultEventCode; + private final Duration timeFaultEvent; + private final DateTime initializationTime; + + public PodInfoFaultAndInitializationTime(byte[] encodedData) { + super(encodedData); + + if (encodedData.length < MINIMUM_MESSAGE_LENGTH) { + throw new IllegalArgumentException("Not enough data"); + } + + faultEventCode = FaultEventCode.fromByte(encodedData[1]); + timeFaultEvent = Duration.standardMinutes(((encodedData[2] & 0b1) << 8) + encodedData[3]); + // We ignore time zones here because we don't keep the time zone in which the pod was initially set up + // Which is fine because we don't use the initialization time for anything important anyway + initializationTime = new DateTime(2000 + encodedData[14], encodedData[12], encodedData[13], encodedData[15], encodedData[16]); + } + + @Override + public PodInfoType getType() { + return PodInfoType.FAULT_AND_INITIALIZATION_TIME; + } + + public FaultEventCode getFaultEventCode() { + return faultEventCode; + } + + public Duration getTimeFaultEvent() { + return timeFaultEvent; + } + + public DateTime getInitializationTime() { + return initializationTime; + } + + @Override + public String toString() { + return "PodInfoFaultAndInitializationTime{" + + "faultEventCode=" + faultEventCode + + ", timeFaultEvent=" + timeFaultEvent + + ", initializationTime=" + initializationTime + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoFaultEvent.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoFaultEvent.java new file mode 100644 index 0000000000..129ef989bb --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoFaultEvent.java @@ -0,0 +1,172 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo; + +import org.joda.time.Duration; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertSet; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.DeliveryStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.FaultEventCode; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.LogEventErrorCode; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodProgressStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class PodInfoFaultEvent extends PodInfo { + private static final int MINIMUM_MESSAGE_LENGTH = 21; + + private final PodProgressStatus podProgressStatus; + private final DeliveryStatus deliveryStatus; + private final double insulinNotDelivered; + private final byte podMessageCounter; + private final double totalInsulinDelivered; + private final FaultEventCode faultEventCode; + private final Duration faultEventTime; + private final Double reservoirLevel; + private final Duration timeSinceActivation; + private final AlertSet unacknowledgedAlerts; + private final boolean faultAccessingTables; + private final LogEventErrorCode logEventErrorType; + private final PodProgressStatus logEventErrorPodProgressStatus; + private final byte receiverLowGain; + private final byte radioRSSI; + private final PodProgressStatus podProgressStatusAtTimeOfFirstLoggedFaultEvent; + private final byte[] unknownValue; + + public PodInfoFaultEvent(byte[] encodedData) { + super(encodedData); + + if (encodedData.length < MINIMUM_MESSAGE_LENGTH) { + throw new IllegalArgumentException("Not enough data"); + } + + podProgressStatus = PodProgressStatus.fromByte(encodedData[1]); + deliveryStatus = DeliveryStatus.fromByte(encodedData[2]); + insulinNotDelivered = OmnipodConst.POD_PULSE_SIZE * ByteUtil.toInt(encodedData[3], encodedData[4]); + podMessageCounter = encodedData[5]; + totalInsulinDelivered = OmnipodConst.POD_PULSE_SIZE * ByteUtil.toInt(encodedData[6], encodedData[7]); + faultEventCode = FaultEventCode.fromByte(encodedData[8]); + + int minutesSinceActivation = ByteUtil.toInt(encodedData[9], encodedData[10]); + if (minutesSinceActivation == 0xffff) { + faultEventTime = null; + } else { + faultEventTime = Duration.standardMinutes(minutesSinceActivation); + } + + double reservoirValue = ((encodedData[11] & 0x03) << 8) + + ByteUtil.convertUnsignedByteToInt(encodedData[12]) * OmnipodConst.POD_PULSE_SIZE; + if (reservoirValue > OmnipodConst.MAX_RESERVOIR_READING) { + reservoirLevel = null; + } else { + reservoirLevel = reservoirValue; + } + + int minutesActive = ByteUtil.toInt(encodedData[13], encodedData[14]); + timeSinceActivation = Duration.standardMinutes(minutesActive); + + unacknowledgedAlerts = new AlertSet(encodedData[15]); + faultAccessingTables = encodedData[16] == 0x02; + logEventErrorType = LogEventErrorCode.fromByte((byte) (encodedData[17] >>> 4)); + logEventErrorPodProgressStatus = PodProgressStatus.fromByte((byte) (encodedData[17] & 0x0f)); + receiverLowGain = (byte) (ByteUtil.convertUnsignedByteToInt(encodedData[18]) >>> 6); + radioRSSI = (byte) (encodedData[18] & 0x3f); + podProgressStatusAtTimeOfFirstLoggedFaultEvent = PodProgressStatus.fromByte((byte) (encodedData[19] & 0x0f)); + unknownValue = ByteUtil.substring(encodedData, 20, 2); + } + + @Override + public PodInfoType getType() { + return PodInfoType.FAULT_EVENT; + } + + public PodProgressStatus getPodProgressStatus() { + return podProgressStatus; + } + + public DeliveryStatus getDeliveryStatus() { + return deliveryStatus; + } + + public double getInsulinNotDelivered() { + return insulinNotDelivered; + } + + public byte getPodMessageCounter() { + return podMessageCounter; + } + + public double getTotalInsulinDelivered() { + return totalInsulinDelivered; + } + + public FaultEventCode getFaultEventCode() { + return faultEventCode; + } + + public Duration getFaultEventTime() { + return faultEventTime; + } + + public Double getReservoirLevel() { + return reservoirLevel; + } + + public Duration getTimeSinceActivation() { + return timeSinceActivation; + } + + public AlertSet getUnacknowledgedAlerts() { + return unacknowledgedAlerts; + } + + public boolean isFaultAccessingTables() { + return faultAccessingTables; + } + + public LogEventErrorCode getLogEventErrorType() { + return logEventErrorType; + } + + public PodProgressStatus getLogEventErrorPodProgressStatus() { + return logEventErrorPodProgressStatus; + } + + public byte getReceiverLowGain() { + return receiverLowGain; + } + + public byte getRadioRSSI() { + return radioRSSI; + } + + public PodProgressStatus getPodProgressStatusAtTimeOfFirstLoggedFaultEvent() { + return podProgressStatusAtTimeOfFirstLoggedFaultEvent; + } + + public byte[] getUnknownValue() { + return unknownValue; + } + + @Override + public String toString() { + return "PodInfoFaultEvent{" + + "podProgressStatus=" + podProgressStatus + + ", deliveryStatus=" + deliveryStatus + + ", insulinNotDelivered=" + insulinNotDelivered + + ", podMessageCounter=" + podMessageCounter + + ", totalInsulinDelivered=" + totalInsulinDelivered + + ", faultEventCode=" + faultEventCode + + ", faultEventTime=" + faultEventTime + + ", reservoirLevel=" + reservoirLevel + + ", timeSinceActivation=" + timeSinceActivation + + ", unacknowledgedAlerts=" + unacknowledgedAlerts + + ", faultAccessingTables=" + faultAccessingTables + + ", logEventErrorType=" + logEventErrorType + + ", logEventErrorPodProgressStatus=" + logEventErrorPodProgressStatus + + ", receiverLowGain=" + receiverLowGain + + ", radioRSSI=" + radioRSSI + + ", podProgressStatusAtTimeOfFirstLoggedFaultEvent=" + podProgressStatusAtTimeOfFirstLoggedFaultEvent + + ", unknownValue=" + ByteUtil.shortHexString(unknownValue) + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoLowFlashLogDump.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoLowFlashLogDump.java new file mode 100644 index 0000000000..72d4d215c4 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoLowFlashLogDump.java @@ -0,0 +1,50 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; + +public class PodInfoLowFlashLogDump extends PodInfo { + private static final int MINIMUM_MESSAGE_LENGTH = 8; + + private final byte numberOfBytes; + private final byte[] dataFromFlashMemory; + private final int podAddress; + + public PodInfoLowFlashLogDump(byte[] encodedData) { + super(encodedData); + + if (encodedData.length < MINIMUM_MESSAGE_LENGTH) { + throw new IllegalArgumentException("Not enough data"); + } + + numberOfBytes = encodedData[2]; + podAddress = ByteUtil.toInt((int) encodedData[3], (int) encodedData[4], (int) encodedData[5], (int) encodedData[6], ByteUtil.BitConversion.BIG_ENDIAN); + dataFromFlashMemory = ByteUtil.substring(encodedData, 3, ByteUtil.convertUnsignedByteToInt(encodedData[2])); + } + + @Override + public PodInfoType getType() { + return PodInfoType.LOW_FLASH_DUMP_LOG; + } + + public byte getNumberOfBytes() { + return numberOfBytes; + } + + public byte[] getDataFromFlashMemory() { + return dataFromFlashMemory; + } + + public int getPodAddress() { + return podAddress; + } + + @Override + public String toString() { + return "PodInfoLowFlashLogDump{" + + "numberOfBytes=" + numberOfBytes + + ", dataFromFlashMemory=" + ByteUtil.shortHexString(dataFromFlashMemory) + + ", podAddress=" + podAddress + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoOlderHighFlashLogDump.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoOlderHighFlashLogDump.java new file mode 100644 index 0000000000..3292679cc1 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoOlderHighFlashLogDump.java @@ -0,0 +1,46 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; + +public class PodInfoOlderHighFlashLogDump extends PodInfo { + private static final int MINIMUM_MESSAGE_LENGTH = 3; + + private final ArrayList dwords; + + public PodInfoOlderHighFlashLogDump(byte[] encodedData) { + super(encodedData); + + if (encodedData.length < MINIMUM_MESSAGE_LENGTH) { + throw new IllegalArgumentException("Not enough data"); + } + + dwords = new ArrayList<>(); + + int numberOfDwordLogEntries = ByteUtil.toInt(encodedData[1], encodedData[2]); + for (int i = 0; numberOfDwordLogEntries > i; i++) { + byte[] dword = ByteUtil.substring(encodedData, 3 + (4 * i), 4); + dwords.add(dword); + } + } + + @Override + public PodInfoType getType() { + return PodInfoType.OLDER_HIGH_FLASH_LOG_DUMP; + } + + public List getDwords() { + return Collections.unmodifiableList(dwords); + } + + @Override + public String toString() { + return "PodInfoOlderHighFlashLogDump{" + + "dwords=" + dwords + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoRecentHighFlashLogDump.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoRecentHighFlashLogDump.java new file mode 100644 index 0000000000..a022d6103a --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoRecentHighFlashLogDump.java @@ -0,0 +1,55 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; + +public class PodInfoRecentHighFlashLogDump extends PodInfo { + private static final int MINIMUM_MESSAGE_LENGTH = 3; + + private final ArrayList dwords; + + private final int lastEntryIndex; + + public PodInfoRecentHighFlashLogDump(byte[] encodedData, int bodyLength) { + super(encodedData); + + if (encodedData.length < MINIMUM_MESSAGE_LENGTH) { + throw new IllegalArgumentException("Not enough data"); + } + + lastEntryIndex = ByteUtil.toInt(encodedData[1], encodedData[2]); + dwords = new ArrayList<>(); + + int numberOfDwords = (bodyLength - 3) / 4; + + for (int i = 0; numberOfDwords > i; i++) { + byte[] dword = ByteUtil.substring(encodedData, 3 + (4 * i), 4); + dwords.add(dword); + } + } + + @Override + public PodInfoType getType() { + return PodInfoType.RECENT_HIGH_FLASH_LOG_DUMP; + } + + public List getDwords() { + return Collections.unmodifiableList(dwords); + } + + public int getLastEntryIndex() { + return lastEntryIndex; + } + + @Override + public String toString() { + return "PodInfoRecentHighFlashLogDump{" + + "lastEntryIndex=" + lastEntryIndex + + ",dwords=" + dwords + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoResponse.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoResponse.java new file mode 100644 index 0000000000..59322f6209 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoResponse.java @@ -0,0 +1,40 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; + +public class PodInfoResponse extends MessageBlock { + private final PodInfoType subType; + private final PodInfo podInfo; + + public PodInfoResponse(byte[] encodedData) { + int bodyLength = ByteUtil.convertUnsignedByteToInt(encodedData[1]); + + this.encodedData = ByteUtil.substring(encodedData, 2, bodyLength); + subType = PodInfoType.fromByte(encodedData[2]); + podInfo = subType.decode(this.encodedData, bodyLength); + } + + public PodInfoType getSubType() { + return subType; + } + + public T getPodInfo() { + return (T) podInfo; + } + + @Override + public MessageBlockType getType() { + return MessageBlockType.POD_INFO_RESPONSE; + } + + @Override + public String toString() { + return "PodInfoResponse{" + + "subType=" + subType.name() + + ", podInfo=" + podInfo.toString() + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoTestValues.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoTestValues.java new file mode 100644 index 0000000000..006ba876f6 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoTestValues.java @@ -0,0 +1,55 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo; + +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; + +public class PodInfoTestValues extends PodInfo { + private static final int MINIMUM_MESSAGE_LENGTH = 5; + private final byte byte1; + private final byte byte2; + private final byte byte3; + private final byte byte4; + + public PodInfoTestValues(byte[] encodedData) { + super(encodedData); + + if (encodedData.length < MINIMUM_MESSAGE_LENGTH) { + throw new IllegalArgumentException("Not enough data"); + } + + byte1 = encodedData[1]; + byte2 = encodedData[2]; + byte3 = encodedData[3]; + byte4 = encodedData[4]; + } + + @Override + public PodInfoType getType() { + return PodInfoType.HARDCODED_TEST_VALUES; + } + + public byte getByte1() { + return byte1; + } + + public byte getByte2() { + return byte2; + } + + public byte getByte3() { + return byte3; + } + + public byte getByte4() { + return byte4; + } + + @Override + public String toString() { + return "PodInfoTestValues{" + + "byte1=" + byte1 + + ", byte2=" + byte2 + + ", byte3=" + byte3 + + ", byte4=" + byte4 + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertConfiguration.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertConfiguration.java new file mode 100644 index 0000000000..c02255bd99 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertConfiguration.java @@ -0,0 +1,71 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +import org.joda.time.Duration; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class AlertConfiguration { + private final AlertType alertType; + private final AlertSlot alertSlot; + private final boolean active; + private final boolean autoOffModifier; + private final Duration duration; + private final AlertTrigger alertTrigger; + private final BeepRepeat beepRepeat; + private final BeepType beepType; + + public AlertConfiguration(AlertType alertType, AlertSlot alertSlot, boolean active, boolean autoOffModifier, + Duration duration, AlertTrigger alertTrigger, + BeepType beepType, BeepRepeat beepRepeat) { + this.alertType = alertType; + this.alertSlot = alertSlot; + this.active = active; + this.autoOffModifier = autoOffModifier; + this.duration = duration; + this.alertTrigger = alertTrigger; + this.beepRepeat = beepRepeat; + this.beepType = beepType; + } + + public AlertType getAlertType() { + return alertType; + } + + public AlertSlot getAlertSlot() { + return alertSlot; + } + + public byte[] getRawData() { + int firstByte = (alertSlot.getValue() << 4); + firstByte += active ? (1 << 3) : 0; + + if (alertTrigger instanceof UnitsRemainingAlertTrigger) { + firstByte += 1 << 2; + } + + if (autoOffModifier) { + firstByte += 1 << 1; + } + + firstByte += ((int) duration.getStandardMinutes() >>> 8) & 0x1; + + byte[] encodedData = new byte[]{ + (byte) firstByte, + (byte) duration.getStandardMinutes() + }; + + if (alertTrigger instanceof UnitsRemainingAlertTrigger) { + int ticks = (int) (((UnitsRemainingAlertTrigger) alertTrigger).getValue() / OmnipodConst.POD_PULSE_SIZE / 2); + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt16(ticks)); + } else if (alertTrigger instanceof TimerAlertTrigger) { + int durationInMinutes = (int) ((TimerAlertTrigger) alertTrigger).getValue().getStandardMinutes(); + encodedData = ByteUtil.concat(encodedData, ByteUtil.getBytesFromInt16(durationInMinutes)); + } + + encodedData = ByteUtil.concat(encodedData, beepType.getValue()); + encodedData = ByteUtil.concat(encodedData, beepRepeat.getValue()); + + return encodedData; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertConfigurationFactory.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertConfigurationFactory.java new file mode 100644 index 0000000000..8ff303a332 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertConfigurationFactory.java @@ -0,0 +1,27 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +import org.joda.time.Duration; + +public class AlertConfigurationFactory { + public static AlertConfiguration createExpirationAdvisoryAlertConfiguration(Duration timeUntilAlert, Duration duration) { + return new AlertConfiguration(AlertType.EXPIRATION_ADVISORY_ALERT, AlertSlot.SLOT7, true, false, duration, + new TimerAlertTrigger(timeUntilAlert), BeepType.BIP_BEEP_BIP_BEEP_BIP_BEEP_BIP_BEEP, BeepRepeat.EVERY_60_MINUTES); + } + + public static AlertConfiguration createShutdownImminentAlertConfiguration(Duration timeUntilAlert) { + return new AlertConfiguration(AlertType.SHUTDOWN_IMMINENT_ALARM, AlertSlot.SLOT2, true, false, Duration.ZERO, + new TimerAlertTrigger(timeUntilAlert), BeepType.BIP_BEEP_BIP_BEEP_BIP_BEEP_BIP_BEEP, BeepRepeat.EVERY_15_MINUTES); + } + + public static AlertConfiguration createAutoOffAlertConfiguration(boolean active, Duration countdownDuration) { + return new AlertConfiguration(AlertType.AUTO_OFF_ALARM, AlertSlot.SLOT0, active, true, + Duration.standardMinutes(15), new TimerAlertTrigger(countdownDuration), + BeepType.BIP_BEEP_BIP_BEEP_BIP_BEEP_BIP_BEEP, BeepRepeat.EVERY_MINUTE_FOR_15_MINUTES); + } + + public static AlertConfiguration createFinishSetupReminderAlertConfiguration() { + return new AlertConfiguration(AlertType.FINISH_SETUP_REMINDER, AlertSlot.SLOT7, true, false, + Duration.standardMinutes(55), new TimerAlertTrigger(Duration.standardMinutes(5)), + BeepType.BIP_BEEP_BIP_BEEP_BIP_BEEP_BIP_BEEP, BeepRepeat.EVERY_5_MINUTES); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertSet.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertSet.java new file mode 100644 index 0000000000..9d65d7d5aa --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertSet.java @@ -0,0 +1,44 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +import java.util.ArrayList; +import java.util.List; + +public class AlertSet { + private final List alertSlots; + + public AlertSet(byte rawValue) { + alertSlots = new ArrayList<>(); + for (AlertSlot alertSlot : AlertSlot.values()) { + if ((alertSlot.getBitMaskValue() & rawValue) != 0) { + alertSlots.add(alertSlot); + } + } + } + + public AlertSet(List alertSlots) { + this.alertSlots = alertSlots; + } + + public List getAlertSlots() { + return new ArrayList<>(alertSlots); + } + + public int size() { + return alertSlots.size(); + } + + public byte getRawValue() { + byte value = 0; + for (AlertSlot alertSlot : alertSlots) { + value |= alertSlot.getBitMaskValue(); + } + return value; + } + + @Override + public String toString() { + return "AlertSet{" + + "alertSlots=" + alertSlots + + '}'; + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertSlot.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertSlot.java new file mode 100644 index 0000000000..9f76ae0983 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertSlot.java @@ -0,0 +1,35 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum AlertSlot { + SLOT0((byte) 0x00), + SLOT1((byte) 0x01), + SLOT2((byte) 0x02), + SLOT3((byte) 0x03), + SLOT4((byte) 0x04), + SLOT5((byte) 0x05), + SLOT6((byte) 0x06), + SLOT7((byte) 0x07); + + private byte value; + + AlertSlot(byte value) { + this.value = value; + } + + public static AlertSlot fromByte(byte value) { + for (AlertSlot type : values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Unknown AlertSlot: " + value); + } + + public byte getBitMaskValue() { + return (byte) (1 << value); + } + + public byte getValue() { + return value; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertTrigger.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertTrigger.java new file mode 100644 index 0000000000..1dedb458d0 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertTrigger.java @@ -0,0 +1,14 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public abstract class AlertTrigger { + protected T value; + + public AlertTrigger(T value) { + this.value = value; + } + + public T getValue() { + return value; + } +} + diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertType.java new file mode 100644 index 0000000000..085652f6d6 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/AlertType.java @@ -0,0 +1,11 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum AlertType { + WAITING_FOR_PAIRING_REMINDER, + FINISH_SETUP_REMINDER, + EXPIRATION_ALERT, + EXPIRATION_ADVISORY_ALERT, + SHUTDOWN_IMMINENT_ALARM, + LOW_RESERVOIC_ALERT, + AUTO_OFF_ALARM +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/BeepRepeat.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/BeepRepeat.java new file mode 100644 index 0000000000..3562dd6c18 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/BeepRepeat.java @@ -0,0 +1,23 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum BeepRepeat { + ONCE((byte) 0x00), + EVERY_MINUTE_FOR_3_MINUTES_REPEAT_EVERY_60_MINUTES((byte) 0x01), + EVERY_MINUTE_FOR_15_MINUTES((byte) 0x02), + EVERY_MINUTE_FOR_3_MINUTES_REPEAT_EVERY_15_MINUTES((byte) 0x03), + EVERY_3_MINUTES_DELAYED((byte) 0x04), + EVERY_60_MINUTES((byte) 0x05), + EVERY_15_MINUTES((byte) 0x06), + EVERY_15_MINUTES_DELAYED((byte) 0x07), + EVERY_5_MINUTES((byte) 0x08); + + private byte value; + + BeepRepeat(byte value) { + this.value = value; + } + + public byte getValue() { + return value; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/BeepType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/BeepType.java new file mode 100644 index 0000000000..7cf08b8714 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/BeepType.java @@ -0,0 +1,38 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum BeepType { + NO_BEEP((byte) 0x00), + BEEP_BEEP_BEEP_BEEP((byte) 0x01), + BIP_BEEP_BIP_BEEP_BIP_BEEP_BIP_BEEP((byte) 0x02), + BIP_BIP((byte) 0x03), + BEEP((byte) 0x04), + BEEP_BEEP_BEEP((byte) 0x05), + BEEEEEEP((byte) 0x06), + BIP_BIP_BIP_BIP_BIP_BIP((byte) 0x07), + BEEEP_BEEEP((byte) 0x08), + BEEP_BEEP((byte) 0xB), + BEEEP((byte) 0xC), + BIP_BEEEEEP((byte) 0xD), + FIVE_SECONDS_BEEP((byte) 0xE), + BEEP_CONFIG_NO_BEEP((byte) 0xF); + + private byte value; + + BeepType(byte value) { + this.value = value; + } + + public static BeepType fromByte(byte value) { + for (BeepType type : values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Unknown BeepType: " + value); + } + + public byte getValue() { + return value; + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/DeliveryStatus.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/DeliveryStatus.java new file mode 100644 index 0000000000..a85095fddd --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/DeliveryStatus.java @@ -0,0 +1,29 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum DeliveryStatus { + SUSPENDED((byte) 0x00), + NORMAL((byte) 0x01), + TEMP_BASAL_RUNNING((byte) 0x02), + PRIMING((byte) 0x04), + BOLUS_IN_PROGRESS((byte) 0x05), + BOLUS_AND_TEMP_BASAL((byte) 0x06); + + private byte value; + + DeliveryStatus(byte value) { + this.value = value; + } + + public static DeliveryStatus fromByte(byte value) { + for (DeliveryStatus type : values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Unknown DeliveryStatus: " + value); + } + + public byte getValue() { + return value; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/DeliveryType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/DeliveryType.java new file mode 100644 index 0000000000..910031f85e --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/DeliveryType.java @@ -0,0 +1,28 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum DeliveryType { + NONE((byte) 0x00), + BASAL((byte) 0x01), + TEMP_BASAL((byte) 0x02), + BOLUS((byte) 0x04), + EXTENDED_BOLUS((byte) 0x08); + + private byte value; + + DeliveryType(byte value) { + this.value = value; + } + + public static DeliveryType fromByte(byte value) { + for (DeliveryType type : values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Unknown DeliveryType: " + value); + } + + public byte getValue() { + return value; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/ErrorResponseType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/ErrorResponseType.java new file mode 100644 index 0000000000..883ae94821 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/ErrorResponseType.java @@ -0,0 +1,24 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum ErrorResponseType { + BAD_NONCE((byte) 0x14); + + private byte value; + + ErrorResponseType(byte value) { + this.value = value; + } + + public static ErrorResponseType fromByte(byte value) { + for (ErrorResponseType type : values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Unknown ErrorResponseType: " + value); + } + + public byte getValue() { + return value; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/FaultEventCode.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/FaultEventCode.java new file mode 100644 index 0000000000..048c9c4804 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/FaultEventCode.java @@ -0,0 +1,141 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum FaultEventCode { + NO_FAULTS((byte) 0x00), + FAILED_FLASH_ERASE((byte) 0x01), + FAILED_FLASH_STORE((byte) 0x02), + TABLE_CORRUPTION_BASAL_SUBCOMMAND((byte) 0x03), + CORRUPTION_BYTE_720((byte) 0x05), + DATA_CORRUPTION_IN_TEST_RTC_INTERRUPT((byte) 0x06), + RTC_INTERRUPT_HANDLER_INCONSISTENT_STATE((byte) 0x07), + VALUE_GREATER_THAN_8((byte) 0x08), + BF_0_NOT_EQUAL_TO_BF_1((byte) 0x0A), + TABLE_CORRUPTION_TEMP_BASAL_SUBCOMMAND((byte) 0x0B), + RESET_DUE_TO_COP((byte) 0x0D), + RESET_DUE_TO_ILLEGAL_OPCODE((byte) 0x0E), + RESET_DUE_TO_ILLEGAL_ADDRESS((byte) 0x0F), + RESET_DUE_TO_SAWCOP((byte) 0x10), + CORRUPTION_IN_BYTE_866((byte) 0x11), + RESET_DUE_TO_LVD((byte) 0x12), + MESSAGE_LENGTH_TOO_LONG((byte) 0x13), + OCCLUDED((byte) 0x14), + CORRUPTION_IN_WORD_129((byte) 0x15), + CORRUPTION_IN_BYTE_868((byte) 0x16), + CORRUPTION_IN_A_VALIDATED_TABLE((byte) 0x17), + RESERVOIR_EMPTY((byte) 0x18), + BAD_POWER_SWITCH_ARRAY_VALUE_1((byte) 0x19), + BAD_POWER_SWITCH_ARRAY_VALUE_2((byte) 0x1A), + BAD_LOAD_CNTH_VALUE((byte) 0x1B), + EXCEEDED_MAXIMUM_POD_LIFE_80_HRS((byte) 0x1C), + BAD_STATE_COMMAND_1_A_SCHEDULE_PARSE((byte) 0x1D), + UNEXPECTED_STATE_IN_REGISTER_UPON_RESET((byte) 0x1E), + WRONG_SUMMARY_FOR_TABLE_129((byte) 0x1F), + VALIDATE_COUNT_ERROR_WHEN_BOLUSING((byte) 0x20), + BAD_TIMER_VARIABLE_STATE((byte) 0x21), + UNEXPECTED_RTC_MODULE_VALUE_DURING_RESET((byte) 0x22), + PROBLEM_CALIBRATE_TIMER((byte) 0x23), + RTC_INTERRUPT_HANDLER_UNEXPECTED_CALL((byte) 0x26), + MISSING_2_HOUR_ALERT_TO_FILL_TANK((byte) 0x27), + FAULT_EVENT_SETUP_POD((byte) 0x28), + ERROR_MAIN_LOOP_HELPER_0((byte) 0x29), + ERROR_MAIN_LOOP_HELPER_1((byte) 0x2A), + ERROR_MAIN_LOOP_HELPER_2((byte) 0x2B), + ERROR_MAIN_LOOP_HELPER_3((byte) 0x2C), + ERROR_MAIN_LOOP_HELPER_4((byte) 0x2D), + ERROR_MAIN_LOOP_HELPER_5((byte) 0x2E), + ERROR_MAIN_LOOP_HELPER_6((byte) 0x2F), + ERROR_MAIN_LOOP_HELPER_7((byte) 0x30), + INSULIN_DELIVERY_COMMAND_ERROR((byte) 0x31), + BAD_VALUE_STARTUP_TEST((byte) 0x32), + CONNECTED_POD_COMMAND_TIMEOUT((byte) 0x33), + RESET_FROM_UNKNOWN_CAUSE((byte) 0x34), + ERROR_FLASH_INITIALIZATION((byte) 0x36), + BAD_PIEZO_VALUE((byte) 0x37), + UNEXPECTED_VALUE_BYTE_358((byte) 0x38), + PROBLEM_WITH_LOAD_1_AND_2((byte) 0x39), + A_GREATER_THAN_7_IN_MESSAGE((byte) 0x3A), + FAILED_TEST_SAW_RESET((byte) 0x3B), + TEST_IN_PROGRESS((byte) 0x3C), + PROBLEM_WITH_PUMP_ANCHOR((byte) 0x3D), + ERROR_FLASH_WRITE((byte) 0x3E), + ENCODER_COUNT_TOO_HIGH((byte) 0x40), + ENCODER_COUNT_EXCESSIVE_VARIANCE((byte) 0x41), + ENCODER_COUNT_TOO_LOW((byte) 0x42), + ENCODER_COUNT_PROBLEM((byte) 0x43), + CHECK_VOLTAGE_OPEN_WIRE_1((byte) 0x44), + CHECK_VOLTAGE_OPEN_WIRE_2((byte) 0x45), + PROBLEM_WITH_LOAD_1_AND_2_TYPE_46((byte) 0x46), + PROBLEM_WITH_LOAD_1_AND_2_TYPE_47((byte) 0x47), + BAD_TIMER_CALIBRATION((byte) 0x48), + BAD_TIMER_RATIOS((byte) 0x49), + BAD_TIMER_VALUES((byte) 0x4A), + TRIM_ICS_TOO_CLOSE_TO_0_X_1_FF((byte) 0x4B), + PROBLEM_FINDING_BEST_TRIM_VALUE((byte) 0x4C), + BAD_SET_TPM_1_MULTI_CASES_VALUE((byte) 0x4D), + UNEXPECTED_RF_ERROR_FLAG_DURING_RESET((byte) 0x4F), + BAD_CHECK_SDRH_AND_BYTE_11_F_STATE((byte) 0x51), + ISSUE_TXO_KPROCESS_INPUT_BUFFER((byte) 0x52), + WRONG_VALUE_WORD_107((byte) 0x53), + PACKET_FRAME_LENGTH_TOO_LONG((byte) 0x54), + UNEXPECTED_IRQ_HIGHIN_TIMER_TICK((byte) 0x55), + UNEXPECTED_IRQ_LOWIN_TIMER_TICK((byte) 0x56), + BAD_ARG_TO_GET_ENTRY((byte) 0x57), + BAD_ARG_TO_UPDATE_37_A_TABLE((byte) 0x58), + ERROR_UPDATING_37_A_TABLE((byte) 0x59), + OCCLUSION_CHECK_VALUE_TOO_HIGH((byte) 0x5A), + LOAD_TABLE_CORRUPTION((byte) 0x5B), + PRIME_OPEN_COUNT_TOO_LOW((byte) 0x5C), + BAD_VALUE_BYTE_109((byte) 0x5D), + DISABLE_FLASH_SECURITY_FAILED((byte) 0x5E), + CHECK_VOLTAGE_FAILURE((byte) 0x5F), + OCCLUSION_CHECK_STARTUP_1((byte) 0x60), + OCCLUSION_CHECK_STARTUP_2((byte) 0x61), + OCCLUSION_CHECK_TIMEOUTS_1((byte) 0x62), + OCCLUSION_CHECK_TIMEOUTS_2((byte) 0x66), + OCCLUSION_CHECK_TIMEOUTS_3((byte) 0x67), + OCCLUSION_CHECK_PULSE_ISSUE((byte) 0x68), + OCCLUSION_CHECK_BOLUS_PROBLEM((byte) 0x69), + OCCLUSION_CHECK_ABOVE_THRESHOLD((byte) 0x6A), + BASAL_UNDER_INFUSION((byte) 0x80), + BASAL_OVER_INFUSION((byte) 0x81), + TEMP_BASAL_UNDER_INFUSION((byte) 0x82), + TEMP_BASAL_OVER_INFUSION((byte) 0x83), + BOLUS_UNDER_INFUSION((byte) 0x84), + BOLUS_OVER_INFUSION((byte) 0x85), + BASAL_OVER_INFUSION_PULSE((byte) 0x86), + TEMP_BASAL_OVER_INFUSION_PULSE((byte) 0x87), + BOLUS_OVER_INFUSION_PULSE((byte) 0x88), + IMMEDIATE_BOLUS_OVER_INFUSION_PULSE((byte) 0x89), + EXTENDED_BOLUS_OVER_INFUSION_PULSE((byte) 0x8A), + CORRUPTION_OF_TABLES((byte) 0x8B), + BAD_INPUT_TO_VERIFY_AND_START_PUMP((byte) 0x8D), + BAD_PUMP_REQ_5_STATE((byte) 0x8E), + COMMAND_1_A_PARSE_UNEXPECTED_FAILED((byte) 0x8F), + BAD_VALUE_FOR_TABLES((byte) 0x90), + BAD_PUMP_REQ_1_STATE((byte) 0x91), + BAD_PUMP_REQ_2_STATE((byte) 0x92), + BAD_PUMP_REQ_3_STATE((byte) 0x93), + BAD_VALUE_FIELD_6_IN_0_X_1_A((byte) 0x95), + BAD_STATE_IN_CLEAR_BOLUS_IST_2_AND_VARS((byte) 0x96), + BAD_STATE_IN_MAYBE_INC_33_D((byte) 0x97), + VALUES_DO_NOT_MATCH_OR_ARE_GREATER_THAN_0_X_97((byte) 0x98); + + private byte value; + + FaultEventCode(byte value) { + this.value = value; + } + + public static FaultEventCode fromByte(byte value) { + for (FaultEventCode type : values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Unknown FaultEventCode: " + value); + } + + public byte getValue() { + return value; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/FirmwareVersion.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/FirmwareVersion.java new file mode 100644 index 0000000000..27dab2c091 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/FirmwareVersion.java @@ -0,0 +1,32 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +import java.util.Locale; + +public class FirmwareVersion { + private final int major; + private final int minor; + private final int patch; + + public FirmwareVersion(int major, int minor, int patch) { + this.major = major; + this.minor = minor; + this.patch = patch; + } + + @Override + public String toString() { + return String.format(Locale.getDefault(), "%d.%d.%d", major, minor, patch); + } + + public int getMajor() { + return major; + } + + public int getMinor() { + return minor; + } + + public int getPatch() { + return patch; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/LogEventErrorCode.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/LogEventErrorCode.java new file mode 100644 index 0000000000..1cf366fd61 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/LogEventErrorCode.java @@ -0,0 +1,24 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum LogEventErrorCode { + NONE((byte) 0x00), + IMMEDIATE_BOLUS_IN_PROGRESS((byte) 0x01), + INTERNAL_2_BIT_VARIABLE_SET_AND_MANIPULATED_IN_MAIN_LOOP_ROUTINES_2((byte) 0x02), + INTERNAL_2_BIT_VARIABLE_SET_AND_MANIPULATED_IN_MAIN_LOOP_ROUTINES_3((byte) 0x03), + INSULIN_STATE_TABLE_CORRUPTION((byte) 0x04); + + private byte value; + + LogEventErrorCode(byte value) { + this.value = value; + } + + public static LogEventErrorCode fromByte(byte value) { + for (LogEventErrorCode type : values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Unknown LogEventErrorCode: " + value); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/MessageBlockType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/MessageBlockType.java new file mode 100644 index 0000000000..a7ced69305 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/MessageBlockType.java @@ -0,0 +1,63 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +import org.apache.commons.lang3.NotImplementedException; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.MessageBlock; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.ErrorResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.VersionResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoResponse; + +public enum MessageBlockType { + VERSION_RESPONSE(0x01), + POD_INFO_RESPONSE(0x02), + SETUP_POD(0x03), + ERROR_RESPONSE(0x06), + ASSIGN_ADDRESS(0x07), + FAULT_CONFIG(0x08), + GET_STATUS(0x0e), + ACKNOWLEDGE_ALERT(0x11), + BASAL_SCHEDULE_EXTRA(0x13), + TEMP_BASAL_EXTRA(0x16), + BOLUS_EXTRA(0x17), + CONFIGURE_ALERTS(0x19), + SET_INSULIN_SCHEDULE(0x1a), + DEACTIVATE_POD(0x1c), + STATUS_RESPONSE(0x1d), + BEEP_CONFIG(0x1e), + CANCEL_DELIVERY(0x1f); + + private byte value; + + MessageBlockType(int value) { + this.value = (byte) value; + } + + public static MessageBlockType fromByte(byte value) { + for (MessageBlockType type : values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Unknown MessageBlockType: " + value); + } + + public byte getValue() { + return value; + } + + public MessageBlock decode(byte[] encodedData) { + switch (this) { + case VERSION_RESPONSE: + return new VersionResponse(encodedData); + case ERROR_RESPONSE: + return new ErrorResponse(encodedData); + case POD_INFO_RESPONSE: + return new PodInfoResponse(encodedData); + case STATUS_RESPONSE: + return new StatusResponse(encodedData); + default: + throw new NotImplementedException(this.name()); + } + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/NonceState.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/NonceState.java new file mode 100644 index 0000000000..eca7dbf791 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/NonceState.java @@ -0,0 +1,51 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public class NonceState { + private final long[] table = new long[21]; + private int index; + + public NonceState(int lot, int tid) { + initializeTable(lot, tid, (byte) 0x00); + } + + public NonceState(int lot, int tid, byte seed) { + initializeTable(lot, tid, seed); + } + + private void initializeTable(int lot, int tid, byte seed) { + table[0] = (long) (lot & 0xFFFF) + 0x55543DC3L + (((long) (lot) & 0xFFFFFFFFL) >> 16); + table[0] = table[0] & 0xFFFFFFFFL; + table[1] = (tid & 0xFFFF) + 0xAAAAE44EL + (((long) (tid) & 0xFFFFFFFFL) >> 16); + table[1] = table[1] & 0xFFFFFFFFL; + index = 0; + table[0] += seed; + for (int i = 0; i < 16; i++) { + table[2 + i] = generateEntry(); + } + index = (int) ((table[0] + table[1]) & 0X0F); + } + + private int generateEntry() { + table[0] = (((table[0] >> 16) + (table[0] & 0xFFFF) * 0x5D7FL) & 0xFFFFFFFFL); + table[1] = (((table[1] >> 16) + (table[1] & 0xFFFF) * 0x8CA0L) & 0xFFFFFFFFL); + return (int) ((table[1] + (table[0] << 16)) & 0xFFFFFFFFL); + } + + public int getCurrentNonce() { + return (int) table[(2 + index)]; + } + + public void advanceToNextNonce() { + int nonce = getCurrentNonce(); + table[(2 + index)] = generateEntry(); + index = (nonce & 0x0F); + } + + @Override + public String toString() { + return "NonceState{" + + "currentNonce=" + getCurrentNonce() + + ", index=" + index + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PacketType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PacketType.java new file mode 100644 index 0000000000..aae862d9b8 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PacketType.java @@ -0,0 +1,42 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum PacketType { + INVALID((byte) 0), + POD((byte) 0b111), + PDM((byte) 0b101), + CON((byte) 0b100), + ACK((byte) 0b010); + + private byte value; + + PacketType(byte value) { + this.value = value; + } + + public static PacketType fromByte(byte value) { + for (PacketType type : values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Unknown PacketType: " + value); + } + + public int getMaxBodyLength() { + switch (this) { + case ACK: + return 4; + case CON: + case PDM: + case POD: + return 31; + default: + return 0; + } + } + + public byte getValue() { + return value; + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodInfoType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodInfoType.java new file mode 100644 index 0000000000..374ca53b6a --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodInfoType.java @@ -0,0 +1,69 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfo; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoActiveAlerts; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoDataLog; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoFaultAndInitializationTime; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoFaultEvent; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoLowFlashLogDump; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoOlderHighFlashLogDump; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoRecentHighFlashLogDump; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoTestValues; + +public enum PodInfoType { + NORMAL((byte) 0x00), + ACTIVE_ALERTS((byte) 0x01), + FAULT_EVENT((byte) 0x02), + DATA_LOG((byte) 0x03), // Similar to types $50 & $51. Returns up to the last 60 dwords of data. + FAULT_AND_INITIALIZATION_TIME((byte) 0x05), + HARDCODED_TEST_VALUES((byte) 0x06), + LOW_FLASH_DUMP_LOG((byte) 0x46), // Starting at $4000 + RECENT_HIGH_FLASH_LOG_DUMP((byte) 0x50), // Starting at $4200 + OLDER_HIGH_FLASH_LOG_DUMP((byte) 0x51); // Starting at $4200 but dumps entries before the last 50 + + private final byte value; + + PodInfoType(byte value) { + this.value = value; + } + + public static PodInfoType fromByte(byte value) { + for (PodInfoType type : values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Unknown PodInfoType: " + value); + } + + public byte getValue() { + return value; + } + + public PodInfo decode(byte[] encodedData, int bodyLength) { + switch (this) { + case NORMAL: + // We've never observed a PodInfoResponse with 0x00 subtype + // Instead, the pod returns a StatusResponse + throw new UnsupportedOperationException("Cannot decode PodInfoType.NORMAL"); + case ACTIVE_ALERTS: + return new PodInfoActiveAlerts(encodedData); + case FAULT_EVENT: + return new PodInfoFaultEvent(encodedData); + case DATA_LOG: + return new PodInfoDataLog(encodedData, bodyLength); + case FAULT_AND_INITIALIZATION_TIME: + return new PodInfoFaultAndInitializationTime(encodedData); + case HARDCODED_TEST_VALUES: + return new PodInfoTestValues(encodedData); + case LOW_FLASH_DUMP_LOG: + return new PodInfoLowFlashLogDump(encodedData); + case RECENT_HIGH_FLASH_LOG_DUMP: + return new PodInfoRecentHighFlashLogDump(encodedData, bodyLength); + case OLDER_HIGH_FLASH_LOG_DUMP: + return new PodInfoOlderHighFlashLogDump(encodedData); + default: + throw new IllegalArgumentException("Cannot decode " + this.name()); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodProgressStatus.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodProgressStatus.java new file mode 100644 index 0000000000..300e0c287c --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/PodProgressStatus.java @@ -0,0 +1,43 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum PodProgressStatus { + INITIAL_VALUE((byte) 0x00), + TANK_POWER_ACTIVATED((byte) 0x01), + TANK_FILL_COMPLETED((byte) 0x02), + PAIRING_SUCCESS((byte) 0x03), + PRIMING((byte) 0x04), + READY_FOR_BASAL_SCHEDULE((byte) 0x05), + READY_FOR_CANNULA_INSERTION((byte) 0x06), + CANNULA_INSERTING((byte) 0x07), + RUNNING_ABOVE_FIFTY_UNITS((byte) 0x08), + RUNNING_BELOW_FIFTY_UNITS((byte) 0x09), + ONE_NOT_USED_BUT_IN_33((byte) 0x0a), + TWO_NOT_USED_BUT_IN_33((byte) 0x0b), + THREE_NOT_USED_BUT_IN_33((byte) 0x0c), + ERROR_EVENT_LOGGED_SHUTTING_DOWN((byte) 0x0d), + DELAYED_PRIME((byte) 0x0e), + INACTIVE((byte) 0x0f); + + private byte value; + + PodProgressStatus(byte value) { + this.value = value; + } + + public static PodProgressStatus fromByte(byte value) { + for (PodProgressStatus type : values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Unknown PodProgressStatus: " + value); + } + + public byte getValue() { + return value; + } + + public boolean isReadyForDelivery() { + return this == RUNNING_ABOVE_FIFTY_UNITS || this == RUNNING_BELOW_FIFTY_UNITS; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/SetupProgress.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/SetupProgress.java new file mode 100644 index 0000000000..72a3b5438a --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/SetupProgress.java @@ -0,0 +1,21 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public enum SetupProgress { + ADDRESS_ASSIGNED, + POD_CONFIGURED, + STARTING_PRIME, + PRIMING, + PRIMING_FINISHED, + INITIAL_BASAL_SCHEDULE_SET, + STARTING_INSERT_CANNULA, + CANNULA_INSERTING, + COMPLETED; + + public boolean isBefore(SetupProgress other) { + return this.ordinal() < other.ordinal(); + } + + public boolean isAfter(SetupProgress other) { + return this.ordinal() > other.ordinal(); + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/TimerAlertTrigger.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/TimerAlertTrigger.java new file mode 100644 index 0000000000..8d4096de59 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/TimerAlertTrigger.java @@ -0,0 +1,9 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +import org.joda.time.Duration; + +public class TimerAlertTrigger extends AlertTrigger { + public TimerAlertTrigger(Duration value) { + super(value); + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/UnitsRemainingAlertTrigger.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/UnitsRemainingAlertTrigger.java new file mode 100644 index 0000000000..6668be13c8 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/UnitsRemainingAlertTrigger.java @@ -0,0 +1,7 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs; + +public class UnitsRemainingAlertTrigger extends AlertTrigger { + public UnitsRemainingAlertTrigger(Double value) { + super(value); + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalDeliverySchedule.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalDeliverySchedule.java new file mode 100644 index 0000000000..e6076fa189 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalDeliverySchedule.java @@ -0,0 +1,51 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.IRawRepresentable; + +public class BasalDeliverySchedule extends DeliverySchedule implements IRawRepresentable { + + private final byte currentSegment; + private final int secondsRemaining; + private final int pulsesRemaining; + private final BasalDeliveryTable basalTable; + + public BasalDeliverySchedule(byte currentSegment, int secondsRemaining, int pulsesRemaining, + BasalDeliveryTable basalTable) { + this.currentSegment = currentSegment; + this.secondsRemaining = secondsRemaining; + this.pulsesRemaining = pulsesRemaining; + this.basalTable = basalTable; + } + + @Override + public byte[] getRawData() { + byte[] rawData = new byte[0]; + rawData = ByteUtil.concat(rawData, currentSegment); + rawData = ByteUtil.concat(rawData, ByteUtil.getBytesFromInt16(secondsRemaining << 3)); + rawData = ByteUtil.concat(rawData, ByteUtil.getBytesFromInt16(pulsesRemaining)); + for (BasalTableEntry entry : basalTable.getEntries()) { + rawData = ByteUtil.concat(rawData, entry.getRawData()); + } + return rawData; + } + + @Override + public InsulinScheduleType getType() { + return InsulinScheduleType.BASAL_SCHEDULE; + } + + @Override + public int getChecksum() { + int checksum = 0; + byte[] rawData = getRawData(); + for (int i = 0; i < rawData.length && i < 5; i++) { + checksum += ByteUtil.convertUnsignedByteToInt(rawData[i]); + } + for (BasalTableEntry entry : basalTable.getEntries()) { + checksum += entry.getChecksum(); + } + + return checksum; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalDeliveryTable.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalDeliveryTable.java new file mode 100644 index 0000000000..83b997ef60 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalDeliveryTable.java @@ -0,0 +1,105 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule; + +import org.joda.time.Duration; + +import java.util.ArrayList; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class BasalDeliveryTable { + + public static final int SEGMENT_DURATION = 30 * 60; + public static final int MAX_PULSES_PER_RATE_ENTRY = 6400; + + private static final int NUM_SEGMENTS = 48; + private static final int MAX_SEGMENTS_PER_ENTRY = 16; + + private List entries = new ArrayList<>(); + + public BasalDeliveryTable(BasalSchedule schedule) { + TempSegment[] expandedSegments = new TempSegment[48]; + + boolean halfPulseRemainder = false; + for (int i = 0; i < NUM_SEGMENTS; i++) { + double rate = schedule.rateAt(Duration.standardMinutes(i * 30)); + int pulsesPerHour = (int) Math.round(rate / OmnipodConst.POD_PULSE_SIZE); + int pulsesPerSegment = pulsesPerHour >>> 1; + boolean halfPulse = (pulsesPerHour & 0b1) != 0; + + expandedSegments[i] = new TempSegment(pulsesPerSegment + (halfPulseRemainder && halfPulse ? 1 : 0)); + halfPulseRemainder = halfPulseRemainder != halfPulse; + } + + List segmentsToMerge = new ArrayList<>(); + + boolean altSegmentPulse = false; + for (TempSegment segment : expandedSegments) { + if (segmentsToMerge.isEmpty()) { + segmentsToMerge.add(segment); + continue; + } + + TempSegment firstSegment = segmentsToMerge.get(0); + + int delta = segment.getPulses() - firstSegment.getPulses(); + if (segmentsToMerge.size() == 1) { + altSegmentPulse = delta == 1; + } + + int expectedDelta = altSegmentPulse ? segmentsToMerge.size() % 2 : 0; + + if (expectedDelta != delta || segmentsToMerge.size() == MAX_SEGMENTS_PER_ENTRY) { + addBasalTableEntry(segmentsToMerge, altSegmentPulse); + segmentsToMerge.clear(); + } + + segmentsToMerge.add(segment); + } + + addBasalTableEntry(segmentsToMerge, altSegmentPulse); + } + + public BasalDeliveryTable(double tempBasalRate, Duration duration) { + int pulsesPerHour = (int) Math.round(tempBasalRate / OmnipodConst.POD_PULSE_SIZE); + int pulsesPerSegment = pulsesPerHour >> 1; + boolean alternateSegmentPulse = (pulsesPerHour & 0b1) != 0; + + int remaining = (int) Math.round(duration.getStandardSeconds() / (double) BasalDeliveryTable.SEGMENT_DURATION); + + while (remaining > 0) { + int segments = Math.min(MAX_SEGMENTS_PER_ENTRY, remaining); + entries.add(new BasalTableEntry(segments, pulsesPerSegment, segments > 1 && alternateSegmentPulse)); + remaining -= segments; + } + } + + private void addBasalTableEntry(List segments, boolean alternateSegmentPulse) { + entries.add(new BasalTableEntry(segments.size(), segments.get(0).getPulses(), alternateSegmentPulse)); + } + + public BasalTableEntry[] getEntries() { + return entries.toArray(new BasalTableEntry[0]); + } + + byte numSegments() { + byte numSegments = 0; + for (BasalTableEntry entry : entries) { + numSegments += entry.getSegments(); + } + return numSegments; + } + + private class TempSegment { + private int pulses; + + public TempSegment(int pulses) { + this.pulses = pulses; + } + + public int getPulses() { + return pulses; + } + } +} + diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalSchedule.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalSchedule.java new file mode 100644 index 0000000000..416eeeb420 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalSchedule.java @@ -0,0 +1,143 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule; + +import org.joda.time.Duration; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class BasalSchedule { + private final List entries; + + public BasalSchedule(List entries) { + if (entries == null) { + throw new IllegalArgumentException("Entries cannot be null"); + } + this.entries = entries; + } + + public double rateAt(Duration offset) { + return lookup(offset).getBasalScheduleEntry().getRate(); + } + + public List getEntries() { + return new ArrayList<>(entries); + } + + public BasalScheduleLookupResult lookup(Duration offset) { + if (offset.isLongerThan(Duration.standardHours(24)) || offset.isShorterThan(Duration.ZERO)) { + throw new IllegalArgumentException("Invalid duration"); + } + + List reversedBasalScheduleEntries = reversedBasalScheduleEntries(); + + Duration last = Duration.standardHours(24); + int index = 0; + for (BasalScheduleEntry entry : reversedBasalScheduleEntries) { + if (entry.getStartTime().isShorterThan(offset) || entry.getStartTime().equals(offset)) { + return new BasalScheduleLookupResult( // + reversedBasalScheduleEntries.size() - (index + 1), // + entry, // + entry.getStartTime(), // + last.minus(entry.getStartTime())); + } + last = entry.getStartTime(); + index++; + } + + throw new IllegalArgumentException("Basal schedule incomplete"); + } + + private List reversedBasalScheduleEntries() { + List reversedEntries = new ArrayList<>(entries); + Collections.reverse(reversedEntries); + return reversedEntries; + } + + public List adjacentEqualRatesMergedEntries() { + List mergedEntries = new ArrayList<>(); + Double lastRate = null; + for (BasalScheduleEntry entry : entries) { + if (lastRate == null || entry.getRate() != lastRate) { + mergedEntries.add(entry); + } + lastRate = entry.getRate(); + } + return mergedEntries; + } + + public List getDurations() { + List durations = new ArrayList<>(); + Duration last = Duration.standardHours(24); + List basalScheduleEntries = reversedBasalScheduleEntries(); + for (BasalScheduleEntry entry : basalScheduleEntries) { + durations.add(new BasalScheduleDurationEntry( // + entry.getRate(), // + entry.getStartTime(), // + last.minus(entry.getStartTime()))); + last = entry.getStartTime(); + } + + Collections.reverse(durations); + return durations; + } + + @Override + public String toString() { + return "BasalSchedule (" + entries.size() + " entries)"; + } + + public static class BasalScheduleDurationEntry { + private final double rate; + private final Duration duration; + private final Duration startTime; + + public BasalScheduleDurationEntry(double rate, Duration startTime, Duration duration) { + this.rate = rate; + this.duration = duration; + this.startTime = startTime; + } + + public double getRate() { + return rate; + } + + public Duration getDuration() { + return duration; + } + + public Duration getStartTime() { + return startTime; + } + } + + public static class BasalScheduleLookupResult { + private final int index; + private final BasalScheduleEntry basalScheduleEntry; + private final Duration startTime; + private final Duration duration; + + public BasalScheduleLookupResult(int index, BasalScheduleEntry basalScheduleEntry, Duration startTime, Duration duration) { + this.index = index; + this.basalScheduleEntry = basalScheduleEntry; + this.startTime = startTime; + this.duration = duration; + } + + public int getIndex() { + return index; + } + + public BasalScheduleEntry getBasalScheduleEntry() { + return basalScheduleEntry; + } + + public Duration getStartTime() { + return startTime; + } + + public Duration getDuration() { + return duration; + } + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalScheduleEntry.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalScheduleEntry.java new file mode 100644 index 0000000000..0e7a06f730 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalScheduleEntry.java @@ -0,0 +1,36 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule; + +import org.joda.time.Duration; + +import info.nightscout.androidaps.Constants; + +public class BasalScheduleEntry { + private final double rate; + private final Duration startTime; + + public BasalScheduleEntry(double rate, Duration startTime) { + if (rate < 0D) { + throw new IllegalArgumentException("Rate should be >= 0"); + } else if (rate > Constants.MAX_BASAL_RATE) { + throw new IllegalArgumentException("Rate exceeds max basal rate"); + } + this.rate = rate; + this.startTime = startTime; + } + + public double getRate() { + return rate; + } + + public Duration getStartTime() { + return startTime; + } + + @Override + public String toString() { + return "BasalScheduleEntry{" + + "rate=" + rate + + ", startTime=" + startTime + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalScheduleMapper.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalScheduleMapper.java new file mode 100644 index 0000000000..cce2dceb1a --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalScheduleMapper.java @@ -0,0 +1,20 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule; + +import org.joda.time.Duration; + +import java.util.ArrayList; +import java.util.List; + +import info.nightscout.androidaps.data.Profile; + +public class BasalScheduleMapper { + public static BasalSchedule mapProfileToBasalSchedule(Profile profile) { + Profile.ProfileValue[] basalValues = profile.getBasalValues(); + List entries = new ArrayList<>(); + for(Profile.ProfileValue basalValue : basalValues) { + entries.add(new BasalScheduleEntry(basalValue.value, Duration.standardSeconds(basalValue.timeAsSeconds))); + } + + return new BasalSchedule(entries); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalTableEntry.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalTableEntry.java new file mode 100644 index 0000000000..e652ebec44 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BasalTableEntry.java @@ -0,0 +1,44 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.IRawRepresentable; + +public class BasalTableEntry implements IRawRepresentable { + + private final int segments; + private final int pulses; + private final boolean alternateSegmentPulse; + + public BasalTableEntry(int segments, int pulses, boolean alternateSegmentPulse) { + this.segments = segments; + this.pulses = pulses; + this.alternateSegmentPulse = alternateSegmentPulse; + } + + @Override + public byte[] getRawData() { + byte[] rawData = new byte[2]; + byte pulsesHighByte = (byte) ((pulses >>> 8) & 0b11); + byte pulsesLowByte = (byte) pulses; + rawData[0] = (byte) ((byte) ((segments - 1) << 4) + (byte) ((alternateSegmentPulse ? 1 : 0) << 3) + pulsesHighByte); + rawData[1] = pulsesLowByte; + return rawData; + } + + public int getChecksum() { + int checksumPerSegment = ByteUtil.convertUnsignedByteToInt((byte) pulses) + (pulses >>> 8); + return (checksumPerSegment * segments + (alternateSegmentPulse ? segments / 2 : 0)); + } + + public int getSegments() { + return this.segments; + } + + public int getPulses() { + return pulses; + } + + public boolean isAlternateSegmentPulse() { + return alternateSegmentPulse; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BolusDeliverySchedule.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BolusDeliverySchedule.java new file mode 100644 index 0000000000..54d92ab70a --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/BolusDeliverySchedule.java @@ -0,0 +1,52 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule; + +import org.joda.time.Duration; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.IRawRepresentable; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class BolusDeliverySchedule extends DeliverySchedule implements IRawRepresentable { + + private final double units; + private final Duration timeBetweenPulses; + + public BolusDeliverySchedule(double units, Duration timeBetweenPulses) { + if (units <= 0D) { + throw new IllegalArgumentException("Units should be > 0"); + } else if (units > OmnipodConst.MAX_BOLUS) { + throw new IllegalArgumentException("Units exceeds max bolus"); + } + this.units = units; + this.timeBetweenPulses = timeBetweenPulses; + } + + @Override + public byte[] getRawData() { + byte[] rawData = new byte[]{1}; // Number of half hour segments + + int pulseCount = (int) Math.round(units / OmnipodConst.POD_PULSE_SIZE); + int multiplier = (int) timeBetweenPulses.getStandardSeconds() * 8; + int fieldA = pulseCount * multiplier; + + rawData = ByteUtil.concat(rawData, ByteUtil.getBytesFromInt16(fieldA)); + rawData = ByteUtil.concat(rawData, ByteUtil.getBytesFromInt16(pulseCount)); + rawData = ByteUtil.concat(rawData, ByteUtil.getBytesFromInt16(pulseCount)); + return rawData; + } + + @Override + public InsulinScheduleType getType() { + return InsulinScheduleType.BOLUS; + } + + @Override + public int getChecksum() { + int checksum = 0; + byte[] rawData = getRawData(); + for (int i = 0; i < rawData.length && i < 7; i++) { + checksum += ByteUtil.convertUnsignedByteToInt(rawData[i]); + } + return checksum; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/DeliverySchedule.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/DeliverySchedule.java new file mode 100644 index 0000000000..325b7f19e4 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/DeliverySchedule.java @@ -0,0 +1,10 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.IRawRepresentable; + +public abstract class DeliverySchedule implements IRawRepresentable { + + public abstract InsulinScheduleType getType(); + + public abstract int getChecksum(); +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/InsulinScheduleType.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/InsulinScheduleType.java new file mode 100644 index 0000000000..6c7a364e2d --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/InsulinScheduleType.java @@ -0,0 +1,26 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule; + +public enum InsulinScheduleType { + BASAL_SCHEDULE(0), + TEMP_BASAL_SCHEDULE(1), + BOLUS(2); + + private byte value; + + InsulinScheduleType(int value) { + this.value = (byte) value; + } + + public static InsulinScheduleType fromByte(byte input) { + for (InsulinScheduleType type : values()) { + if (type.value == input) { + return type; + } + } + return null; + } + + public byte getValue() { + return value; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/RateEntry.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/RateEntry.java new file mode 100644 index 0000000000..3021652479 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/RateEntry.java @@ -0,0 +1,69 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule; + +import org.joda.time.Duration; + +import java.util.ArrayList; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.IRawRepresentable; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; + +public class RateEntry implements IRawRepresentable { + + private final double totalPulses; + // We use a double for the delay between pulses because the Joda time API lacks precision for our calculations + private final double delayBetweenPulsesInSeconds; + + public RateEntry(double totalPulses, double delayBetweenPulsesInSeconds) { + this.totalPulses = totalPulses; + this.delayBetweenPulsesInSeconds = delayBetweenPulsesInSeconds; + } + + public static List createEntries(double rate, Duration duration) { + List entries = new ArrayList<>(); + int remainingSegments = (int) Math.round(duration.getStandardSeconds() / 1800.0); + double pulsesPerSegment = (int) Math.round(rate / OmnipodConst.POD_PULSE_SIZE) / 2.0; + int maxSegmentsPerEntry = pulsesPerSegment > 0 ? (int) (BasalDeliveryTable.MAX_PULSES_PER_RATE_ENTRY / pulsesPerSegment) : 1; + + double durationInHours = duration.getStandardSeconds() / 3600.0; + + double remainingPulses = rate * durationInHours / OmnipodConst.POD_PULSE_SIZE; + double delayBetweenPulses = 3600 / rate * OmnipodConst.POD_PULSE_SIZE; + + while (remainingSegments > 0) { + if (rate == 0.0) { + entries.add(new RateEntry(0, 30D * 60)); + remainingSegments -= 1; + } else { + int numSegments = Math.min(maxSegmentsPerEntry, (int) Math.round(remainingPulses / pulsesPerSegment)); + double totalPulses = pulsesPerSegment * numSegments; + entries.add(new RateEntry(totalPulses, delayBetweenPulses)); + remainingSegments -= numSegments; + remainingPulses -= totalPulses; + } + } + + return entries; + } + + public double getTotalPulses() { + return totalPulses; + } + + public double getDelayBetweenPulsesInSeconds() { + return delayBetweenPulsesInSeconds; + } + + @Override + public byte[] getRawData() { + byte[] rawData = new byte[0]; + rawData = ByteUtil.concat(rawData, ByteUtil.getBytesFromInt16((int) Math.round(totalPulses * 10))); + if (totalPulses == 0) { + rawData = ByteUtil.concat(rawData, ByteUtil.getBytesFromInt((int) (delayBetweenPulsesInSeconds * 1000 * 1000))); + } else { + rawData = ByteUtil.concat(rawData, ByteUtil.getBytesFromInt((int) (delayBetweenPulsesInSeconds * 1000 * 100))); + } + return rawData; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/TempBasalDeliverySchedule.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/TempBasalDeliverySchedule.java new file mode 100644 index 0000000000..6f078651a1 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/schedule/TempBasalDeliverySchedule.java @@ -0,0 +1,60 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.IRawRepresentable; + +public class TempBasalDeliverySchedule extends DeliverySchedule implements IRawRepresentable { + + private final int secondsRemaining; + private final int firstSegmentPulses; + private final BasalDeliveryTable basalTable; + + public TempBasalDeliverySchedule(int secondsRemaining, int firstSegmentPulses, BasalDeliveryTable basalTable) { + this.secondsRemaining = secondsRemaining; + this.firstSegmentPulses = firstSegmentPulses; + this.basalTable = basalTable; + } + + @Override + public byte[] getRawData() { + byte[] rawData = new byte[0]; + rawData = ByteUtil.concat(rawData, basalTable.numSegments()); + rawData = ByteUtil.concat(rawData, ByteUtil.getBytesFromInt16(secondsRemaining << 3)); + rawData = ByteUtil.concat(rawData, ByteUtil.getBytesFromInt16(firstSegmentPulses)); + for (BasalTableEntry entry : basalTable.getEntries()) { + rawData = ByteUtil.concat(rawData, entry.getRawData()); + } + return rawData; + } + + @Override + public InsulinScheduleType getType() { + return InsulinScheduleType.TEMP_BASAL_SCHEDULE; + } + + @Override + public int getChecksum() { + int checksum = 0; + byte[] rawData = getRawData(); + for (int i = 0; i < rawData.length && i < 5; i++) { + checksum += ByteUtil.convertUnsignedByteToInt(rawData[i]); + } + for (BasalTableEntry entry : basalTable.getEntries()) { + checksum += entry.getChecksum(); + } + + return checksum; + } + + public int getSecondsRemaining() { + return secondsRemaining; + } + + public int getFirstSegmentPulses() { + return firstSegmentPulses; + } + + public BasalDeliveryTable getBasalTable() { + return basalTable; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodSessionState.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodSessionState.java new file mode 100644 index 0000000000..97e646b717 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodSessionState.java @@ -0,0 +1,229 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.state; + +import com.google.gson.Gson; + +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.joda.time.Duration; + +import java.util.HashMap; +import java.util.Map; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertSet; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertSlot; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.DeliveryStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.FirmwareVersion; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.NonceState; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.SetupProgress; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BasalSchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmniCRC; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodConst; +import info.nightscout.androidaps.plugins.pump.omnipod.util.OmnipodUtil; +import info.nightscout.androidaps.utils.SP; + +public class PodSessionState extends PodState { + private final Map configuredAlerts; + private final DateTime activatedAt; + private final FirmwareVersion piVersion; + private final FirmwareVersion pmVersion; + private final int lot; + private final int tid; + private boolean suspended; + + private DateTimeZone timeZone; + private NonceState nonceState; + private SetupProgress setupProgress; + private AlertSet activeAlerts; + private BasalSchedule basalSchedule; + private DeliveryStatus lastDeliveryStatus; + + public PodSessionState(DateTimeZone timeZone, int address, DateTime activatedAt, FirmwareVersion piVersion, + FirmwareVersion pmVersion, int lot, int tid, int packetNumber, int messageNumber) { + super(address, messageNumber, packetNumber); + if (timeZone == null) { + throw new IllegalArgumentException("Time zone can not be null"); + } + + suspended = false; + configuredAlerts = new HashMap<>(); + configuredAlerts.put(AlertSlot.SLOT7, AlertType.FINISH_SETUP_REMINDER); + + this.timeZone = timeZone; + this.setupProgress = SetupProgress.ADDRESS_ASSIGNED; + this.activatedAt = activatedAt; + this.piVersion = piVersion; + this.pmVersion = pmVersion; + this.lot = lot; + this.tid = tid; + this.nonceState = new NonceState(lot, tid); + store(); + } + + public AlertType getConfiguredAlertType(AlertSlot alertSlot) { + return configuredAlerts.get(alertSlot); + } + + public void putConfiguredAlert(AlertSlot alertSlot, AlertType alertType) { + configuredAlerts.put(alertSlot, alertType); + store(); + } + + public void removeConfiguredAlert(AlertSlot alertSlot) { + configuredAlerts.remove(alertSlot); + store(); + } + + public DateTime getActivatedAt() { + return activatedAt; + } + + public FirmwareVersion getPiVersion() { + return piVersion; + } + + public FirmwareVersion getPmVersion() { + return pmVersion; + } + + public int getLot() { + return lot; + } + + public int getTid() { + return tid; + } + + public synchronized void resyncNonce(int syncWord, int sentNonce, int sequenceNumber) { + int sum = (sentNonce & 0xFFFF) + + OmniCRC.crc16lookup[sequenceNumber] + + (this.lot & 0xFFFF) + + (this.tid & 0xFFFF); + int seed = ((sum & 0xFFFF) ^ syncWord); + + this.nonceState = new NonceState(lot, tid, (byte) (seed & 0xFF)); + store(); + } + + public int getCurrentNonce() { + return nonceState.getCurrentNonce(); + } + + public synchronized void advanceToNextNonce() { + nonceState.advanceToNextNonce(); + store(); + } + + public SetupProgress getSetupProgress() { + return setupProgress; + } + + public synchronized void setSetupProgress(SetupProgress setupProgress) { + if (setupProgress == null) { + throw new IllegalArgumentException("Setup state cannot be null"); + } + this.setupProgress = setupProgress; + store(); + } + + public boolean isSuspended() { + return suspended; + } + + public boolean hasActiveAlerts() { + return activeAlerts != null; + } + + public AlertSet getActiveAlerts() { + return activeAlerts; + } + + public DateTimeZone getTimeZone() { + return timeZone; + } + + public void setTimeZone(DateTimeZone timeZone) { + if (timeZone == null) { + throw new IllegalArgumentException("Time zone can not be null"); + } + this.timeZone = timeZone; + store(); + } + + public DateTime getTime() { + return DateTime.now().withZone(timeZone); + } + + public Duration getScheduleOffset() { + DateTime now = getTime(); + DateTime startOfDay = new DateTime(now.getYear(), now.getMonthOfYear(), now.getDayOfMonth(), + 0, 0, 0, timeZone); + return new Duration(startOfDay, now); + } + + public boolean hasNonceState() { + return true; + } + + @Override + public void setPacketNumber(int packetNumber) { + super.setPacketNumber(packetNumber); + store(); + } + + @Override + public void setMessageNumber(int messageNumber) { + super.setMessageNumber(messageNumber); + store(); + } + + public BasalSchedule getBasalSchedule() { + return basalSchedule; + } + + public void setBasalSchedule(BasalSchedule basalSchedule) { + this.basalSchedule = basalSchedule; + store(); + } + + public DeliveryStatus getLastDeliveryStatus() { + return lastDeliveryStatus; + } + + @Override + public void updateFromStatusResponse(StatusResponse statusResponse) { + suspended = (statusResponse.getDeliveryStatus() == DeliveryStatus.SUSPENDED); + activeAlerts = statusResponse.getAlerts(); + lastDeliveryStatus = statusResponse.getDeliveryStatus(); + store(); + } + + private void store() { + Gson gson = OmnipodUtil.getGsonInstance(); + SP.putString(OmnipodConst.Prefs.PodState, gson.toJson(this)); + } + + @Override + public String toString() { + return "PodSessionState{" + + "activatedAt=" + activatedAt + + ", piVersion=" + piVersion + + ", pmVersion=" + pmVersion + + ", lot=" + lot + + ", tid=" + tid + + ", suspended=" + suspended + + ", timeZone=" + timeZone + + ", nonceState=" + nonceState + + ", setupProgress=" + setupProgress + + ", configuredAlerts=" + configuredAlerts + + ", activeAlerts=" + activeAlerts + + ", basalSchedule=" + basalSchedule + + ", lastDeliveryStatus=" + lastDeliveryStatus + + ", address=" + address + + ", packetNumber=" + packetNumber + + ", messageNumber=" + messageNumber + + ", faultEvent=" + faultEvent + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodSetupState.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodSetupState.java new file mode 100644 index 0000000000..26b5802258 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodSetupState.java @@ -0,0 +1,43 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.state; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; + +public class PodSetupState extends PodState { + public PodSetupState(int address, int packetNumber, int messageNumber) { + super(address, packetNumber, messageNumber); + } + + @Override + public boolean hasNonceState() { + return false; + } + + @Override + public int getCurrentNonce() { + throw new UnsupportedOperationException("PodSetupState does not have a nonce state"); + } + + @Override + public void advanceToNextNonce() { + throw new UnsupportedOperationException("PodSetupState does not have a nonce state"); + } + + @Override + public void resyncNonce(int syncWord, int sentNonce, int sequenceNumber) { + throw new UnsupportedOperationException("PodSetupState does not have a nonce state"); + } + + @Override + public void updateFromStatusResponse(StatusResponse statusResponse) { + } + + @Override + public String toString() { + return "PodSetupState{" + + "address=" + address + + ", packetNumber=" + packetNumber + + ", messageNumber=" + messageNumber + + ", faultEvent=" + faultEvent + + '}'; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodState.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodState.java new file mode 100644 index 0000000000..2e19de1286 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/defs/state/PodState.java @@ -0,0 +1,68 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.defs.state; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.StatusResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoFaultEvent; + +public abstract class PodState { + protected final int address; + protected int packetNumber; + protected int messageNumber; + + protected PodInfoFaultEvent faultEvent; + + public PodState(int address, int packetNumber, int messageNumber) { + this.address = address; + this.packetNumber = packetNumber; + this.messageNumber = messageNumber; + } + + public abstract boolean hasNonceState(); + + public abstract int getCurrentNonce(); + + public abstract void advanceToNextNonce(); + + public abstract void resyncNonce(int syncWord, int sentNonce, int sequenceNumber); + + public abstract void updateFromStatusResponse(StatusResponse statusResponse); + + public int getAddress() { + return address; + } + + public int getMessageNumber() { + return messageNumber; + } + + public void setMessageNumber(int messageNumber) { + this.messageNumber = messageNumber; + } + + public int getPacketNumber() { + return packetNumber; + } + + public void setPacketNumber(int packetNumber) { + this.packetNumber = packetNumber; + } + + public void increaseMessageNumber(int increment) { + setMessageNumber((messageNumber + increment) & 0b1111); + } + + public void increasePacketNumber(int increment) { + setPacketNumber((packetNumber + increment) & 0b11111); + } + + public boolean hasFaultEvent() { + return faultEvent != null; + } + + public PodInfoFaultEvent getFaultEvent() { + return faultEvent; + } + + public void setFaultEvent(PodInfoFaultEvent faultEvent) { + this.faultEvent = faultEvent; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/exception/CrcMismatchException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/exception/CrcMismatchException.java new file mode 100644 index 0000000000..be2b5c69cb --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/exception/CrcMismatchException.java @@ -0,0 +1,11 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.exception; + +public class CrcMismatchException extends OmnipodException { + public CrcMismatchException(String message) { + super(message); + } + + public CrcMismatchException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/exception/NonceOutOfSyncException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/exception/NonceOutOfSyncException.java new file mode 100644 index 0000000000..dff7b1c6cb --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/exception/NonceOutOfSyncException.java @@ -0,0 +1,13 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.exception; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.ErrorResponse; + +public class NonceOutOfSyncException extends PodReturnedErrorResponseException { + public NonceOutOfSyncException(ErrorResponse errorResponse) { + super(errorResponse); + } + + public NonceOutOfSyncException(ErrorResponse errorResponse, Throwable cause) { + super(errorResponse, cause); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/exception/NotEnoughDataException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/exception/NotEnoughDataException.java new file mode 100644 index 0000000000..3ed455213f --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/exception/NotEnoughDataException.java @@ -0,0 +1,11 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.exception; + +public class NotEnoughDataException extends OmnipodException { + public NotEnoughDataException(String message) { + super(message); + } + + public NotEnoughDataException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/exception/OmnipodException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/exception/OmnipodException.java new file mode 100644 index 0000000000..d76d08d8b6 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/exception/OmnipodException.java @@ -0,0 +1,11 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.exception; + +public class OmnipodException extends RuntimeException { + public OmnipodException(String message) { + super(message); + } + + public OmnipodException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/exception/PodFaultException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/exception/PodFaultException.java new file mode 100644 index 0000000000..b6bf577245 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/exception/PodFaultException.java @@ -0,0 +1,34 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.exception; + +import java.util.Locale; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo.PodInfoFaultEvent; + +public class PodFaultException extends OmnipodException { + private final PodInfoFaultEvent faultEvent; + + public PodFaultException(PodInfoFaultEvent faultEvent) { + super(describePodFault(faultEvent)); + this.faultEvent = faultEvent; + } + + public PodFaultException(PodInfoFaultEvent faultEvent, Throwable cause) { + super(describePodFault(faultEvent), cause); + this.faultEvent = faultEvent; + } + + public static String describePodFault(PodInfoFaultEvent faultEvent) { + return String.format(Locale.getDefault(), "Pod fault (%d): %s", faultEvent.getFaultEventCode().getValue(), + faultEvent.getFaultEventCode().toString()); + } + + public PodInfoFaultEvent getFaultEvent() { + return faultEvent; + } + + @Override + public void printStackTrace() { + System.out.println(faultEvent.toString()); + super.printStackTrace(); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/exception/PodReturnedErrorResponseException.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/exception/PodReturnedErrorResponseException.java new file mode 100644 index 0000000000..8cb743b888 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/exception/PodReturnedErrorResponseException.java @@ -0,0 +1,27 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.exception; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.ErrorResponse; + +public class PodReturnedErrorResponseException extends OmnipodException { + private final ErrorResponse errorResponse; + + public PodReturnedErrorResponseException(ErrorResponse errorResponse) { + super("Pod returned error response"); + this.errorResponse = errorResponse; + } + + public PodReturnedErrorResponseException(ErrorResponse errorResponse, Throwable cause) { + super("Pod returned error response", cause); + this.errorResponse = errorResponse; + } + + public ErrorResponse getErrorResponse() { + return errorResponse; + } + + @Override + public void printStackTrace() { + System.out.println(errorResponse.toString()); + super.printStackTrace(); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/util/OmniCRC.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/util/OmniCRC.java new file mode 100644 index 0000000000..ba5c91c0b5 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/util/OmniCRC.java @@ -0,0 +1,74 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.util; + +public class OmniCRC { + public static final int[] crc16lookup = new int[] { + 0x0000, 0x8005, 0x800f, 0x000a, 0x801b, 0x001e, 0x0014, 0x8011, + 0x8033, 0x0036, 0x003c, 0x8039, 0x0028, 0x802d, 0x8027, 0x0022, + 0x8063, 0x0066, 0x006c, 0x8069, 0x0078, 0x807d, 0x8077, 0x0072, + 0x0050, 0x8055, 0x805f, 0x005a, 0x804b, 0x004e, 0x0044, 0x8041, + 0x80c3, 0x00c6, 0x00cc, 0x80c9, 0x00d8, 0x80dd, 0x80d7, 0x00d2, + 0x00f0, 0x80f5, 0x80ff, 0x00fa, 0x80eb, 0x00ee, 0x00e4, 0x80e1, + 0x00a0, 0x80a5, 0x80af, 0x00aa, 0x80bb, 0x00be, 0x00b4, 0x80b1, + 0x8093, 0x0096, 0x009c, 0x8099, 0x0088, 0x808d, 0x8087, 0x0082, + 0x8183, 0x0186, 0x018c, 0x8189, 0x0198, 0x819d, 0x8197, 0x0192, + 0x01b0, 0x81b5, 0x81bf, 0x01ba, 0x81ab, 0x01ae, 0x01a4, 0x81a1, + 0x01e0, 0x81e5, 0x81ef, 0x01ea, 0x81fb, 0x01fe, 0x01f4, 0x81f1, + 0x81d3, 0x01d6, 0x01dc, 0x81d9, 0x01c8, 0x81cd, 0x81c7, 0x01c2, + 0x0140, 0x8145, 0x814f, 0x014a, 0x815b, 0x015e, 0x0154, 0x8151, + 0x8173, 0x0176, 0x017c, 0x8179, 0x0168, 0x816d, 0x8167, 0x0162, + 0x8123, 0x0126, 0x012c, 0x8129, 0x0138, 0x813d, 0x8137, 0x0132, + 0x0110, 0x8115, 0x811f, 0x011a, 0x810b, 0x010e, 0x0104, 0x8101, + 0x8303, 0x0306, 0x030c, 0x8309, 0x0318, 0x831d, 0x8317, 0x0312, + 0x0330, 0x8335, 0x833f, 0x033a, 0x832b, 0x032e, 0x0324, 0x8321, + 0x0360, 0x8365, 0x836f, 0x036a, 0x837b, 0x037e, 0x0374, 0x8371, + 0x8353, 0x0356, 0x035c, 0x8359, 0x0348, 0x834d, 0x8347, 0x0342, + 0x03c0, 0x83c5, 0x83cf, 0x03ca, 0x83db, 0x03de, 0x03d4, 0x83d1, + 0x83f3, 0x03f6, 0x03fc, 0x83f9, 0x03e8, 0x83ed, 0x83e7, 0x03e2, + 0x83a3, 0x03a6, 0x03ac, 0x83a9, 0x03b8, 0x83bd, 0x83b7, 0x03b2, + 0x0390, 0x8395, 0x839f, 0x039a, 0x838b, 0x038e, 0x0384, 0x8381, + 0x0280, 0x8285, 0x828f, 0x028a, 0x829b, 0x029e, 0x0294, 0x8291, + 0x82b3, 0x02b6, 0x02bc, 0x82b9, 0x02a8, 0x82ad, 0x82a7, 0x02a2, + 0x82e3, 0x02e6, 0x02ec, 0x82e9, 0x02f8, 0x82fd, 0x82f7, 0x02f2, + 0x02d0, 0x82d5, 0x82df, 0x02da, 0x82cb, 0x02ce, 0x02c4, 0x82c1, + 0x8243, 0x0246, 0x024c, 0x8249, 0x0258, 0x825d, 0x8257, 0x0252, + 0x0270, 0x8275, 0x827f, 0x027a, 0x826b, 0x026e, 0x0264, 0x8261, + 0x0220, 0x8225, 0x822f, 0x022a, 0x823b, 0x023e, 0x0234, 0x8231, + 0x8213, 0x0216, 0x021c, 0x8219, 0x0208, 0x820d, 0x8207, 0x0202 + }; + public static final int[] crc8lookup = new int[]{ + 0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15, 0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, 0x2D, + 0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65, 0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, 0x5A, 0x5D, + 0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5, 0xD8, 0xDF, 0xD6, 0xD1, 0xC4, 0xC3, 0xCA, 0xCD, + 0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85, 0xA8, 0xAF, 0xA6, 0xA1, 0xB4, 0xB3, 0xBA, 0xBD, + 0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2, 0xFF, 0xF8, 0xF1, 0xF6, 0xE3, 0xE4, 0xED, 0xEA, + 0xB7, 0xB0, 0xB9, 0xBE, 0xAB, 0xAC, 0xA5, 0xA2, 0x8F, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A, + 0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32, 0x1F, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0D, 0x0A, + 0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42, 0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A, + 0x89, 0x8E, 0x87, 0x80, 0x95, 0x92, 0x9B, 0x9C, 0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4, + 0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2, 0xEB, 0xEC, 0xC1, 0xC6, 0xCF, 0xC8, 0xDD, 0xDA, 0xD3, 0xD4, + 0x69, 0x6E, 0x67, 0x60, 0x75, 0x72, 0x7B, 0x7C, 0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44, + 0x19, 0x1E, 0x17, 0x10, 0x05, 0x02, 0x0B, 0x0C, 0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34, + 0x4E, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5C, 0x5B, 0x76, 0x71, 0x78, 0x7F, 0x6A, 0x6D, 0x64, 0x63, + 0x3E, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B, 0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13, + 0xAE, 0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB, 0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83, + 0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB, 0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, 0xF3 + }; + + + public static int crc16(byte[] bytes) { + int crc = 0x0000; + for (byte b : bytes) { + crc = (crc >> 8) ^ crc16lookup[(crc ^ b) & 0xff]; + } + return crc; + } + + public static byte crc8(byte[] bytes) { + byte crc = 0x00; + for (byte b : bytes) { + crc = (byte) crc8lookup[(crc ^ b) & 0xff]; + } + return crc; + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/util/OmnipodConst.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/util/OmnipodConst.java index 0364541af4..3436836795 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/util/OmnipodConst.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/util/OmnipodConst.java @@ -1,5 +1,7 @@ package info.nightscout.androidaps.plugins.pump.omnipod.util; +import org.joda.time.Duration; + /** * Created by andy on 4.8.2019 */ @@ -9,12 +11,10 @@ public class OmnipodConst { static final String Prefix = "AAPS.Omnipod."; public class Prefs { - - //public static final int BatteryType = R.string.pref_key_medtronic_battery_type; + public static final String PodState = Prefix + "pod_state"; } public class Statistics { - public static final String StatsPrefix = "omnipod_"; public static final String FirstPumpStart = Prefix + "first_pump_use"; public static final String LastGoodPumpCommunicationTime = Prefix + "lastGoodPumpCommunicationTime"; @@ -25,4 +25,20 @@ public class OmnipodConst { public static final String LastPumpHistoryEntry = StatsPrefix + "pump_history_entry"; } + public static final double POD_PULSE_SIZE = 0.05; + public static final double MAX_RESERVOIR_READING = 50.0; + public static final double MAX_BOLUS = 30.0; + public static final double MAX_BASAL_RATE = 30.0; + public static final Duration MAX_TEMP_BASAL_DURATION = Duration.standardHours(12); + public static final int DEFAULT_ADDRESS = 0xffffffff; + + public static final Duration SERVICE_DURATION = Duration.standardHours(80); + public static final Duration EXPIRATION_ALERT_WINDOW = Duration.standardHours(2); + public static final Duration EXPIRATION_ADVISORY_WINDOW = Duration.standardHours(2); + public static final Duration END_OF_SERVICE_IMMINENT_WINDOW = Duration.standardHours(1); + + public static final double POD_PRIME_BOLUS_UNITS = 2.6; + public static final double POD_CANNULA_INSERTION_BOLUS_UNITS = 0.5; + public static final Duration POD_PRIME_DURATION = Duration.standardSeconds(55); + public static final Duration POD_CANNULA_INSERTION_DURATION = Duration.standardSeconds(10); } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/util/OmnipodUtil.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/util/OmnipodUtil.java index 1e3f6e9214..70de575737 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/util/OmnipodUtil.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/util/OmnipodUtil.java @@ -4,7 +4,13 @@ import android.content.Context; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializer; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.joda.time.format.ISODateTimeFormat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,20 +43,12 @@ public class OmnipodUtil extends RileyLinkUtil { private static RileyLinkOmnipodService omnipodService; private static OmnipodPumpStatus omnipodPumpStatus; private static OmnipodCommandType currentCommand; - public static Gson gsonInstance = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); - public static Gson gsonInstancePretty = new GsonBuilder().excludeFieldsWithoutExposeAnnotation() - .setPrettyPrinting().create(); - + private static Gson gsonInstance = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); public static Gson getGsonInstance() { return gsonInstance; } - public static Gson getGsonInstancePretty() { - return gsonInstancePretty; - } - - public static int makeUnsignedShort(int b2, int b1) { int k = (b2 & 0xff) << 8 | b1 & 0xff; return k; @@ -167,4 +165,18 @@ public class OmnipodUtil extends RileyLinkUtil { public static void setPumpStatus(OmnipodPumpStatus omnipodPumpStatus) { OmnipodUtil.omnipodPumpStatus = omnipodPumpStatus; } + + private static Gson createGson() { + GsonBuilder gsonBuilder = new GsonBuilder() + .registerTypeAdapter(DateTime.class, (JsonSerializer) (dateTime, typeOfSrc, context) -> + new JsonPrimitive(ISODateTimeFormat.dateTime().print(dateTime))) + .registerTypeAdapter(DateTime.class, (JsonDeserializer) (json, typeOfT, context) -> + ISODateTimeFormat.dateTime().parseDateTime(json.getAsString())) + .registerTypeAdapter(DateTimeZone.class, (JsonSerializer) (timeZone, typeOfSrc, context) -> + new JsonPrimitive(timeZone.getID())) + .registerTypeAdapter(DateTimeZone.class, (JsonDeserializer) (json, typeOfT, context) -> + DateTimeZone.forID(json.getAsString())); + + return gsonBuilder.create(); + } } diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/PairActionTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/PairActionTest.java new file mode 100644 index 0000000000..e518f6e674 --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/PairActionTest.java @@ -0,0 +1,93 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action; + +import org.joda.time.DateTime; +import org.joda.time.Seconds; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationService; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.action.service.PairService; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.VersionResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.FirmwareVersion; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSessionState; + +import static junit.framework.TestCase.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class PairActionTest { + @Mock + private PairService pairService; + + @Mock + private OmnipodCommunicationService communicationService; + + @Mock + private VersionResponse assignAddressResponse; + + @Mock + private VersionResponse confirmPairingResponse; + + // FIXME test fails because PodState requires android context to be able to store itself + // Proposal: storing PodState should happen elsewhere, not in the class itself + @Ignore + @Test + public void testServiceInvocationFromCapture() { + // Setup + int address = 0x1f173217; + + FirmwareVersion pmVersion = new FirmwareVersion(1, 2, 3); + FirmwareVersion piVersion = new FirmwareVersion(4, 5, 6); + + when(assignAddressResponse.getLot()).thenReturn(13); + when(assignAddressResponse.getTid()).thenReturn(8); + + when(confirmPairingResponse.getLot()).thenReturn(13); + when(confirmPairingResponse.getTid()).thenReturn(8); + when(confirmPairingResponse.getPmVersion()).thenReturn(pmVersion); + when(confirmPairingResponse.getPiVersion()).thenReturn(piVersion); + + when(pairService.executeAssignAddressCommand(eq(communicationService), argThat(setupState -> setupState.getAddress() == address))) // + .thenReturn(assignAddressResponse); + + when(pairService.executeConfigurePodCommand(eq(communicationService), argThat(setupState -> setupState.getAddress() == address), eq(13), eq(8), any(DateTime.class))) // + .thenReturn(confirmPairingResponse); + + // SUT + PodSessionState podState = new PairAction(pairService, address).execute(communicationService); + + // Verify + verify(pairService).executeAssignAddressCommand(any(), any()); + verify(pairService).executeConfigurePodCommand(any(), any(), anyInt(), anyInt(), any(DateTime.class)); + + verifyNoMoreInteractions(pairService); + + // The InitializePodAction should not directly invoke the OmnipodCommunicationService + // This should be done by the InitializePodService + verifyZeroInteractions(communicationService); + + Seconds seconds = Seconds.secondsBetween(podState.getActivatedAt(), DateTime.now()); + + assertTrue("Expected the pod activation time to be less then 3 seconds ago", seconds.isLessThan(Seconds.seconds(3))); + assertEquals(13, podState.getLot()); + assertEquals(8, podState.getTid()); + assertEquals(piVersion, podState.getPiVersion()); + assertEquals(pmVersion, podState.getPmVersion()); + //assertEquals(0xTODO, podState.getCurrentNonce()); + //assertEquals(0xTODO, podState.getPacketNumber()); + //assertEquals(0xTODO, podState.getMessageNumber()); + } + + // TODO add scenarios (?) +} diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/service/PairServiceTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/service/PairServiceTest.java new file mode 100644 index 0000000000..fe7773ff7b --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/action/service/PairServiceTest.java @@ -0,0 +1,67 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.action.service; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import info.nightscout.androidaps.Constants; +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.OmnipodCommunicationService; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.OmnipodMessage; +import info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.VersionResponse; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.state.PodSetupState; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.powermock.api.mockito.PowerMockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class PairServiceTest { + @Mock + private OmnipodCommunicationService communicationService; + + @After + public void tearDown() { + verifyNoMoreInteractions(communicationService); + } + + @Test + public void testExecuteAssignAddressCommand() { + // Setup + PodSetupState setupState = new PodSetupState(0x1f173217, 0x00, 0x00); + VersionResponse response = mock(VersionResponse.class); + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(OmnipodMessage.class); + + when(communicationService.exchangeMessages(any(), any(), any(), any(), any())).thenReturn(response); + + // SUT + VersionResponse versionResponse = new PairService().executeAssignAddressCommand(communicationService, setupState); + + // verify + verify(communicationService).exchangeMessages(eq(VersionResponse.class), eq(setupState), messageCaptor.capture(), eq(Constants.DEFAULT_ADDRESS), eq(0x1f173217)); + verifyNoMoreInteractions(communicationService); + verifyZeroInteractions(response); + + assertEquals(versionResponse, response); + + OmnipodMessage message = messageCaptor.getValue(); + byte[] expectedMessage = ByteUtil.fromHexString("ffffffff000607041f17321700fa"); // from https://github.com/openaps/openomni/wiki/Priming-and-Deploying-New-Pod-%28jweismann%29 + assertArrayEquals(expectedMessage, message.getEncoded()); + } + + @Test + public void testExecuteConfigurePodCommand() { + // TODO + } + + // TODO add scenarios +} diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/AcknowledgeAlertsCommandTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/AcknowledgeAlertsCommandTest.java new file mode 100644 index 0000000000..950085413b --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/AcknowledgeAlertsCommandTest.java @@ -0,0 +1,42 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import org.junit.Test; + +import java.util.Arrays; + +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertSet; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertSlot; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; + +import static org.junit.Assert.assertArrayEquals; + +public class AcknowledgeAlertsCommandTest { + + @Test + public void testEncodingMultipleAlerts() { + + AlertSet alerts = new AlertSet(Arrays.asList(AlertSlot.SLOT0, AlertSlot.SLOT5)); + AcknowledgeAlertsCommand acknowledgeAlertsCommand = new AcknowledgeAlertsCommand(0x10203040, alerts); + byte[] rawData = acknowledgeAlertsCommand.getRawData(); + assertArrayEquals(new byte[]{ + MessageBlockType.ACKNOWLEDGE_ALERT.getValue(), + 5, // length + (byte) 0x10, (byte) 0x20, (byte) 0x30, (byte) 0x40, // nonce + (byte) 0x21 // alerts (bits 5 and 0) + }, rawData); + } + + @Test + public void testEncodingSingleAlert() { + AcknowledgeAlertsCommand acknowledgeAlertsCommand = new AcknowledgeAlertsCommand(0x10203040, AlertSlot.SLOT5); + byte[] rawData = acknowledgeAlertsCommand.getRawData(); + assertArrayEquals(new byte[]{ + MessageBlockType.ACKNOWLEDGE_ALERT.getValue(), + 5, // length + (byte) 0x10, (byte) 0x20, (byte) 0x30, (byte) 0x40, // nonce + (byte) 0x20 // alerts (bit 5) + }, rawData); + } + + // TODO add tests +} diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/AssignAddressCommandTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/AssignAddressCommandTest.java new file mode 100644 index 0000000000..9880c0b25a --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/AssignAddressCommandTest.java @@ -0,0 +1,21 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class AssignAddressCommandTest { + + @Test + public void testEncoding() { + AssignAddressCommand assignAddressCommand = new AssignAddressCommand(0x11223344); + byte[] rawData = assignAddressCommand.getRawData(); + assertEquals(0x11, rawData[2]); + assertEquals(0x22, rawData[3]); + assertEquals(0x33, rawData[4]); + assertEquals(0x44, rawData[5]); + } + + // TODO add tests + +} diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BasalScheduleExtraCommandTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BasalScheduleExtraCommandTest.java new file mode 100644 index 0000000000..08c19bb250 --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BasalScheduleExtraCommandTest.java @@ -0,0 +1,173 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import org.joda.time.Duration; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BasalSchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BasalScheduleEntry; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.RateEntry; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class BasalScheduleExtraCommandTest { + @Test + public void testEncodingFromRateEntries() { + List rateEntries = RateEntry.createEntries(3.0, Duration.standardHours(24)); + BasalScheduleExtraCommand basalScheduleExtraCommand = new BasalScheduleExtraCommand( // + false, // + true, // + Duration.ZERO, // + (byte) 0, // + 689, // + 20D, // + rateEntries); + + assertArrayEquals( // + ByteUtil.createByteArrayFromHexString("130e40001aea01312d003840005b8d80"), // From https://github.com/openaps/openomni/wiki/Bolus + basalScheduleExtraCommand.getRawData()); + } + + @Test + public void testParametersCorrectFromBasalSchedule() { + BasalSchedule basalSchedule = new BasalSchedule(Collections.singletonList(new BasalScheduleEntry(0.05, Duration.ZERO))); + BasalScheduleExtraCommand basalScheduleExtraCommand = new BasalScheduleExtraCommand( // + basalSchedule, // + Duration.standardHours(8).plus(Duration.standardMinutes(15)), // + false, // + true, // + Duration.standardMinutes(1)); + + assertFalse(basalScheduleExtraCommand.isAcknowledgementBeep()); + assertTrue(basalScheduleExtraCommand.isCompletionBeep()); + assertEquals(0, basalScheduleExtraCommand.getCurrentEntryIndex()); + assertEquals(180D, basalScheduleExtraCommand.getDelayUntilNextTenthOfPulseInSeconds(), 0.00001); + assertEquals(60, basalScheduleExtraCommand.getProgramReminderInterval().getStandardSeconds()); + assertEquals(15.8, basalScheduleExtraCommand.getRemainingPulses(), 0.01); + + List rateEntries = basalScheduleExtraCommand.getRateEntries(); + + assertEquals(1, rateEntries.size()); + + RateEntry rateEntry = rateEntries.get(0); + + assertEquals(3600.0, rateEntry.getDelayBetweenPulsesInSeconds(), 0.00000001); + assertEquals(24, rateEntry.getTotalPulses(), 0.001); + } + + @Test + public void testEncodingFromBasalScheduleWithThreeEntries() { + BasalSchedule schedule = new BasalSchedule(Arrays.asList( // + new BasalScheduleEntry(1.05, Duration.ZERO), // + new BasalScheduleEntry(0.9, Duration.standardHours(10).plus(Duration.standardMinutes(30))), // + new BasalScheduleEntry(1.0, Duration.standardHours(18).plus(Duration.standardMinutes(30))))); + + BasalScheduleExtraCommand basalScheduleExtraCommand = new BasalScheduleExtraCommand(schedule, Duration.standardMinutes((0x2e + 1) * 30).minus(Duration.standardSeconds(0x1be8 / 8)), + false, true, Duration.ZERO); + + assertArrayEquals(ByteUtil.fromHexString("131a4002009600a7d8c0089d0105944905a001312d00044c0112a880"), + basalScheduleExtraCommand.getRawData()); + } + + @Test + public void testEncodingFromBasalScheduleWithSingleEntry() { + BasalSchedule basalSchedule = new BasalSchedule(Arrays.asList(new BasalScheduleEntry(1.05, Duration.ZERO))); + BasalScheduleExtraCommand basalScheduleExtraCommand = new BasalScheduleExtraCommand(basalSchedule, + Duration.standardMinutes((0x20 + 1) * 30).minus(Duration.standardSeconds(0x33c0 / 8)), + false, true, Duration.ZERO); + assertBasalScheduleExtraCommandWithLessPrecision("130e40000688009cf29113b001059449", + basalScheduleExtraCommand.getRawData()); + } + + @Test + public void testSegmentMerging() { + List entries = Arrays.asList( + new BasalScheduleEntry(0.8, Duration.ZERO), + new BasalScheduleEntry(0.9, Duration.standardMinutes(180)), // + new BasalScheduleEntry(0.85, Duration.standardMinutes(300)), // + new BasalScheduleEntry(0.85, Duration.standardMinutes(450)), // + new BasalScheduleEntry(0.85, Duration.standardMinutes(750)), // + new BasalScheduleEntry(0.7, Duration.standardMinutes(900)), // + new BasalScheduleEntry(0.9, Duration.standardMinutes(1080)), // + new BasalScheduleEntry(1.10, Duration.standardMinutes(1200)) // + ); + + BasalSchedule basalSchedule = new BasalSchedule(entries); + + BasalScheduleExtraCommand basalScheduleExtraCommand = new BasalScheduleExtraCommand(basalSchedule, + Duration.standardMinutes((0x2a + 1) * 30).minus(Duration.standardSeconds(0x1e50 / 8)), + false, + true, + Duration.ZERO); + + assertBasalScheduleExtraCommandWithLessPrecision( // + "132c4005026200455b9c01e0015752a0016801312d0006a40143209601a401885e6d016801312d00037000f9b074", // + basalScheduleExtraCommand.getRawData()); + } + + @Test + public void testEncodingFromBasalScheduleWithThirteenEntries() { + List entries = Arrays.asList( + new BasalScheduleEntry(1.30, Duration.ZERO), // + new BasalScheduleEntry(0.05, Duration.standardMinutes(30)), // + new BasalScheduleEntry(1.70, Duration.standardMinutes(120)), // + new BasalScheduleEntry(0.85, Duration.standardMinutes(150)), // + new BasalScheduleEntry(1.00, Duration.standardMinutes(180)), // + new BasalScheduleEntry(0.65, Duration.standardMinutes(450)), // + new BasalScheduleEntry(0.50, Duration.standardMinutes(510)), // + new BasalScheduleEntry(0.65, Duration.standardMinutes(570)), // + new BasalScheduleEntry(0.60, Duration.standardMinutes(630)), // + new BasalScheduleEntry(0.65, Duration.standardMinutes(690)), // + new BasalScheduleEntry(1.65, Duration.standardMinutes(840)), // + new BasalScheduleEntry(0.15, Duration.standardMinutes(930)), // + new BasalScheduleEntry(0.85, Duration.standardMinutes(990)) // + ); + + BasalSchedule basalSchedule = new BasalSchedule(entries); + BasalScheduleExtraCommand basalScheduleExtraCommand = new BasalScheduleExtraCommand(basalSchedule, + Duration.standardMinutes((0x27 + 1) * 30).minus(Duration.standardSeconds(0x1518 / 8)), + false, true, Duration.ZERO); + + assertBasalScheduleExtraCommandWithLessPrecision("1356400c02c8011abc64008200d34689000f15752a0000aa00a1904b00550143209603840112a880008201a68d13006402255100008201a68d13007801c9c380014501a68d1301ef00a675a2001e07270e0004fb01432096", + basalScheduleExtraCommand.getRawData()); + } + + @Test + public void testBasalScheduleExtraCommandRoundsToNearestSecond() { + BasalSchedule basalSchedule = new BasalSchedule(Arrays.asList(new BasalScheduleEntry(1.00, Duration.ZERO))); + + BasalScheduleExtraCommand basalScheduleExtraCommand = new BasalScheduleExtraCommand(basalSchedule, + Duration.standardMinutes((0x2b + 1) * 30).minus(Duration.standardSeconds(0x1b38 / 8).plus(Duration.millis(456))), + false, true, Duration.ZERO); + + assertBasalScheduleExtraCommandWithLessPrecision("130e400001c1006acfc012c00112a880", basalScheduleExtraCommand.getRawData()); + } + + + private void assertBasalScheduleExtraCommandWithLessPrecision(String expectedHexString, byte[] actual) { + // The XXXXXXXX field is in thousands of a millisecond. Since we use floating points for + // recreating the offset, we can have small errors in reproducing the the encoded output, which we really + // don't care about. + + byte[] expected = ByteUtil.fromHexString(expectedHexString); + + assertEquals(extractDelayUntilNextPulseInSeconds(expected), extractDelayUntilNextPulseInSeconds(actual), 0.0001); + + // Discard the last byte of the integer so that we can compare the other bytes of the message + expected[9] = 0; + actual[9] = 0; + + assertArrayEquals(expected, actual); + } + + private double extractDelayUntilNextPulseInSeconds(byte[] message) { + return ByteUtil.toInt((int) message[6], (int) message[7], (int) message[8], (int) message[9], ByteUtil.BitConversion.BIG_ENDIAN) / 1_000_000.0; + } +} diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BeepConfigCommandTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BeepConfigCommandTest.java new file mode 100644 index 0000000000..fc04d3f6c8 --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BeepConfigCommandTest.java @@ -0,0 +1,27 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import org.joda.time.Duration; +import org.junit.Test; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.BeepType; + +import static org.junit.Assert.assertArrayEquals; + +public class BeepConfigCommandTest { + @Test + public void testConfidenceReminders() { + BeepConfigCommand beepConfigCommand = new BeepConfigCommand(BeepType.BIP_BEEP_BIP_BEEP_BIP_BEEP_BIP_BEEP, true, + Duration.ZERO, true, Duration.ZERO, + true, Duration.ZERO); + assertArrayEquals(ByteUtil.fromHexString("1e0402404040"), beepConfigCommand.getRawData()); + } + + @Test + public void testProgramReminders() { + BeepConfigCommand beepConfigCommand = new BeepConfigCommand(BeepType.BEEP_CONFIG_NO_BEEP, true, + Duration.ZERO, false, Duration.standardMinutes(60), + false, Duration.standardMinutes(60)); + assertArrayEquals(ByteUtil.fromHexString("1e040f403c3c"), beepConfigCommand.getRawData()); + } +} diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BolusExtraCommandTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BolusExtraCommandTest.java new file mode 100644 index 0000000000..14355e399c --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/BolusExtraCommandTest.java @@ -0,0 +1,62 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import org.joda.time.Duration; +import org.junit.Test; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; + +import static org.junit.Assert.assertArrayEquals; + +public class BolusExtraCommandTest { + @Test + public void testBolusExtraCommand() { + BolusExtraCommand bolusExtraCommand = new BolusExtraCommand(1.25, 0.0, + Duration.ZERO, false, true, Duration.standardHours(1), + Duration.standardSeconds(2)); + + assertArrayEquals( // + ByteUtil.createByteArrayFromHexString("170d7c00fa00030d40000000000000"), // From https://github.com/openaps/openomni/wiki/Bolus + bolusExtraCommand.getRawData()); + } + + @Test + public void testTypicalPrime() { + BolusExtraCommand bolusExtraCommand = new BolusExtraCommand(2.6, Duration.standardSeconds(1), false, false); + assertArrayEquals(ByteUtil.fromHexString("170d000208000186a0000000000000"), // + bolusExtraCommand.getRawData()); + } + + @Test + public void testBolusExtraCommandWithExtraOddPulseCount() { + BolusExtraCommand bolusExtraCommand = new BolusExtraCommand(1.25, 0D, Duration.ZERO, // + false, true, Duration.standardHours(1), Duration.standardSeconds(2)); + assertArrayEquals(ByteUtil.fromHexString("170d7c00fa00030d40000000000000"), // + bolusExtraCommand.getRawData()); + } + + @Test + public void testBolusExtraCommandWithExtraOddPulseCount2() { + BolusExtraCommand bolusExtraCommand = new BolusExtraCommand(2.05, 0D, Duration.ZERO, // + false, false, Duration.standardHours(1), Duration.standardSeconds(2)); + assertArrayEquals(ByteUtil.fromHexString("170d3c019a00030d40000000000000"), // + bolusExtraCommand.getRawData()); + } + + @Test + public void testLargeBolus() { + BolusExtraCommand bolusExtraCommand = new BolusExtraCommand(30D, 0, Duration.ZERO, // + false, true, Duration.standardHours(1), Duration.standardSeconds(2)); + assertArrayEquals(ByteUtil.fromHexString("170d7c177000030d40000000000000"), // + bolusExtraCommand.getRawData()); + } + + @Test + public void testLargeBolus2() { + BolusExtraCommand bolusExtraCommand = new BolusExtraCommand(29.95, 0, Duration.ZERO, // + false, true, Duration.standardHours(1), Duration.standardSeconds(2)); + assertArrayEquals(ByteUtil.fromHexString("170d7c176600030d40000000000000"), // + bolusExtraCommand.getRawData()); + } + + // TODO add square wave bolus tests +} diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/CancelDeliveryCommandTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/CancelDeliveryCommandTest.java new file mode 100644 index 0000000000..7af9465b2c --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/CancelDeliveryCommandTest.java @@ -0,0 +1,45 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import org.junit.Test; + +import java.util.EnumSet; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.BeepType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.DeliveryType; + +import static org.junit.Assert.assertArrayEquals; + +public class CancelDeliveryCommandTest { + + @Test + public void testCancelBolusAndBasalWithBeep() { + CancelDeliveryCommand command = new CancelDeliveryCommand(0x10203040, BeepType.BIP_BIP, EnumSet.of(DeliveryType.BASAL, DeliveryType.BOLUS)); + + byte[] expected = ByteUtil.fromHexString("1F051020304035"); + assertArrayEquals(expected, command.getRawData()); + } + + @Test + public void testCancelBolusWithBeep() { + CancelDeliveryCommand command = new CancelDeliveryCommand(0x4d91f8ff, BeepType.BEEEEEEP, DeliveryType.BOLUS); + + byte[] expected = ByteUtil.fromHexString("1f054d91f8ff64"); + assertArrayEquals(expected, command.getRawData()); + } + + @Test + public void testSuspendBasalCommandWithoutBeep() { + CancelDeliveryCommand command = new CancelDeliveryCommand(0x6fede14a, BeepType.NO_BEEP, DeliveryType.BASAL); + + byte[] expected = ByteUtil.fromHexString("1f056fede14a01"); + assertArrayEquals(expected, command.getRawData()); + } + + + @Test + public void testCancelTempBasalWithoutBeep() { + CancelDeliveryCommand cancelDeliveryCommand = new CancelDeliveryCommand(0xf76d34c4, BeepType.NO_BEEP, DeliveryType.TEMP_BASAL); + assertArrayEquals(ByteUtil.fromHexString("1f05f76d34c402"), cancelDeliveryCommand.getRawData()); + } +} diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/ConfigureAlertsCommandTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/ConfigureAlertsCommandTest.java new file mode 100644 index 0000000000..bfe78a1765 --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/ConfigureAlertsCommandTest.java @@ -0,0 +1,77 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import org.joda.time.Duration; +import org.junit.Test; + +import java.util.Arrays; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertConfiguration; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertSlot; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.AlertType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.BeepRepeat; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.BeepType; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.TimerAlertTrigger; + +import static org.junit.Assert.assertArrayEquals; + +public class ConfigureAlertsCommandTest { + @Test + public void testEncoding() { + // from https://github.com/ps2/rileylink_ios/blob/master/OmniKitTests/MessageTests.swift + + Duration softExpirationTime = Duration.standardHours(72).minus(Duration.standardMinutes(1)); + AlertConfiguration alertConfiguration1 = new AlertConfiguration( // + AlertType.EXPIRATION_ADVISORY_ALERT, + AlertSlot.SLOT7, // + true, // + false, // + Duration.standardHours(7), // + new TimerAlertTrigger(softExpirationTime), // + BeepType.BEEP_BEEP_BEEP, // + BeepRepeat.EVERY_MINUTE_FOR_15_MINUTES); + + assertArrayEquals( // + ByteUtil.fromHexString("79a410df0502"), // + alertConfiguration1.getRawData()); + + Duration hardExpirationTime = Duration.standardHours(79).minus(Duration.standardMinutes(1)); + AlertConfiguration alertConfiguration2 = new AlertConfiguration( // + AlertType.SHUTDOWN_IMMINENT_ALARM, + AlertSlot.SLOT2, // + true, // + false, // + Duration.ZERO, // + new TimerAlertTrigger(hardExpirationTime), // + BeepType.BEEEEEEP, // + BeepRepeat.EVERY_MINUTE_FOR_15_MINUTES); + + assertArrayEquals( // + ByteUtil.fromHexString("280012830602"), // + alertConfiguration2.getRawData()); + + AlertConfiguration alertConfiguration3 = new AlertConfiguration( // + AlertType.AUTO_OFF_ALARM, + AlertSlot.SLOT0, // + false, // + true, // + Duration.standardMinutes(15), // + new TimerAlertTrigger(Duration.ZERO), // + BeepType.BIP_BEEP_BIP_BEEP_BIP_BEEP_BIP_BEEP, // + BeepRepeat.EVERY_MINUTE_FOR_15_MINUTES); + + assertArrayEquals( // + ByteUtil.fromHexString("020f00000202"), // + alertConfiguration3.getRawData()); + + ConfigureAlertsCommand configureAlertsCommand = new ConfigureAlertsCommand( // + 0xfeb6268b, // + Arrays.asList(alertConfiguration1, alertConfiguration2, alertConfiguration3)); + + assertArrayEquals( // + ByteUtil.fromHexString("1916feb6268b79a410df0502280012830602020f00000202"), // + configureAlertsCommand.getRawData()); + } + + // TODO add tests +} diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/ConfigurePodCommandTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/ConfigurePodCommandTest.java new file mode 100644 index 0000000000..f553bfbc0a --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/ConfigurePodCommandTest.java @@ -0,0 +1,25 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import org.joda.time.DateTime; +import org.junit.Test; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; + +import static org.junit.Assert.assertArrayEquals; + +public class ConfigurePodCommandTest { + @Test + public void testEncoding() { + ConfigurePodCommand configurePodCommand = new ConfigurePodCommand( // + 0x1f00ee87, // + new DateTime(2013, 4, 5, 22, 52, 0), // + 41847, // + 240439); + + assertArrayEquals( // + ByteUtil.fromHexString("03131f00ee87140404050d16340000a3770003ab37"), // From https://github.com/openaps/openomni/wiki/Command-03-Setup-Pod + configurePodCommand.getRawData()); + } + + // TODO add tests +} diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/DeactivatePodCommandTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/DeactivatePodCommandTest.java new file mode 100644 index 0000000000..b9a7854766 --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/DeactivatePodCommandTest.java @@ -0,0 +1,23 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import org.junit.Test; + +import info.nightscout.androidaps.plugins.pump.omnipod.defs.MessageBlockType; + +import static org.junit.Assert.assertArrayEquals; + +public class DeactivatePodCommandTest { + + @Test + public void testEncoding() { + DeactivatePodCommand deactivatePodCommand = new DeactivatePodCommand(0x10203040); + byte[] rawData = deactivatePodCommand.getRawData(); + assertArrayEquals(new byte[]{ + MessageBlockType.DEACTIVATE_POD.getValue(), + 4, // length + (byte) 0x10, (byte) 0x20, (byte) 0x30, (byte) 0x40 // nonce + }, rawData); + } + + // TODO add tests +} diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/FaultConfigCommandTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/FaultConfigCommandTest.java new file mode 100644 index 0000000000..5667b65e6e --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/FaultConfigCommandTest.java @@ -0,0 +1,5 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +public class FaultConfigCommandTest { + // TODO add tests (obtain captures first) +} diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/GetStatusCommandTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/GetStatusCommandTest.java new file mode 100644 index 0000000000..34c552f29d --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/GetStatusCommandTest.java @@ -0,0 +1,38 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import org.junit.Test; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; + +import static org.junit.Assert.assertArrayEquals; + +public class GetStatusCommandTest { + @Test + public void testPodInfoTypeNormal() { + GetStatusCommand getStatusCommand = new GetStatusCommand(PodInfoType.NORMAL); + + assertArrayEquals(ByteUtil.fromHexString("0e0100"), getStatusCommand.getRawData()); + } + + @Test + public void testPodInfoTypeConfiguredAlerts() { + GetStatusCommand getStatusCommand = new GetStatusCommand(PodInfoType.ACTIVE_ALERTS); + + assertArrayEquals(ByteUtil.fromHexString("0e0101"), getStatusCommand.getRawData()); + } + + @Test + public void testPodInfoTypeFaultEvents() { + GetStatusCommand getStatusCommand = new GetStatusCommand(PodInfoType.FAULT_EVENT); + + assertArrayEquals(ByteUtil.fromHexString("0e0102"), getStatusCommand.getRawData()); + } + + @Test + public void testPodInfoTypeResetStatus() { + GetStatusCommand getStatusCommand = new GetStatusCommand(PodInfoType.LOW_FLASH_DUMP_LOG); + + assertArrayEquals(ByteUtil.fromHexString("0e0146"), getStatusCommand.getRawData()); + } +} diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/SetInsulinScheduleCommandTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/SetInsulinScheduleCommandTest.java new file mode 100644 index 0000000000..b1a15cac63 --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/SetInsulinScheduleCommandTest.java @@ -0,0 +1,248 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import org.joda.time.Duration; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BasalSchedule; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BasalScheduleEntry; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BolusDeliverySchedule; + +import static org.junit.Assert.assertArrayEquals; + +public class SetInsulinScheduleCommandTest { + @Test + public void testTemporaryBasalScheduleAlternatingSegmentFlag() { + SetInsulinScheduleCommand setInsulinScheduleCommand = new SetInsulinScheduleCommand(0x9746c65b, // + 0.05, Duration.standardMinutes(30)); + assertArrayEquals(ByteUtil.fromHexString("1a0e9746c65b01007901384000000000"), // + setInsulinScheduleCommand.getRawData()); + + SetInsulinScheduleCommand setInsulinScheduleCommand2 = new SetInsulinScheduleCommand(0x9746c65b, // + 0.05, Duration.standardHours(8).plus(Duration.standardMinutes(30))); + assertArrayEquals(ByteUtil.fromHexString("1a109746c65b0100911138400000f8000000"), // + setInsulinScheduleCommand2.getRawData()); + + // Test passed before introducing a maximum temp basal duration of 12 hours +// SetInsulinScheduleCommand setInsulinScheduleCommand3 = new SetInsulinScheduleCommand(0x9746c65b, // +// 0.05, Duration.standardHours(16).plus(Duration.standardMinutes(30))); +// assertArrayEquals(ByteUtil.fromHexString("1a129746c65b0100a92138400000f800f8000000"), // +// setInsulinScheduleCommand3.getRawData()); + } + + @Test + public void testTemporaryBasalScheduleMessage() { + SetInsulinScheduleCommand setInsulinScheduleCommand = new SetInsulinScheduleCommand(0xea2d0a3b, // + 0.2, Duration.standardMinutes(30)); + + assertArrayEquals( // + ByteUtil.fromHexString("1a0eea2d0a3b01007d01384000020002"), // from https://github.com/ps2/rileylink_ios/blob/omnipod-testing/OmniKitTests/TempBasalTests.swift + setInsulinScheduleCommand.getRawData()); + } + + @Test + public void testTemporaryBasalScheduleMessageWithAlternatingPulse() { + SetInsulinScheduleCommand setInsulinScheduleCommand = new SetInsulinScheduleCommand(0x4e2c2717, // + 0.05, Duration.standardMinutes(150)); + + assertArrayEquals( // + ByteUtil.fromHexString("1a0e4e2c271701007f05384000004800"), // from https://github.com/ps2/rileylink_ios/blob/omnipod-testing/OmniKitTests/TempBasalTests.swift + setInsulinScheduleCommand.getRawData()); + } + + @Test + public void testLargerTemporaryBasalScheduleMessage() { + SetInsulinScheduleCommand setInsulinScheduleCommand = new SetInsulinScheduleCommand(0x87e8d03a, // + 2D, Duration.standardMinutes(90)); + + assertArrayEquals( // + ByteUtil.fromHexString("1a0e87e8d03a0100cb03384000142014"), // from https://github.com/ps2/rileylink_ios/blob/omnipod-testing/OmniKitTests/TempBasalTests.swift + setInsulinScheduleCommand.getRawData()); + } + + @Test + public void testExtremelyLargeTemporaryBasalScheduleMessage() { + SetInsulinScheduleCommand setInsulinScheduleCommand = new SetInsulinScheduleCommand(0xa958c5ad, // + 30D, Duration.standardHours(12)); + + assertArrayEquals( // + ByteUtil.fromHexString("1a10a958c5ad0104f5183840012cf12c712c"), // from https://github.com/ps2/rileylink_ios/blob/omnipod-testing/OmniKitTests/TempBasalTests.swift + setInsulinScheduleCommand.getRawData()); + } + + @Test + public void testBasalScheduleSingleEntry() { + BasalSchedule basalSchedule = new BasalSchedule(Collections.singletonList(new BasalScheduleEntry(0.05, Duration.ZERO))); + + SetInsulinScheduleCommand setInsulinScheduleCommand = new SetInsulinScheduleCommand( // + 0x01020304, // + basalSchedule, // + Duration.standardHours(8).plus(Duration.standardMinutes(15))); + + assertArrayEquals( // + ByteUtil.fromHexString("1a1201020304000064101c200000f800f800f800"), // from https://github.com/ps2/rileylink_ios/blob/omnipod-testing/OmniKitTests/BasalScheduleTests.swift + setInsulinScheduleCommand.getRawData()); + } + + @Test + public void testBasalScheduleWithTwelveEntries() { + List entries = Arrays.asList( + new BasalScheduleEntry(1.30, Duration.ZERO), // + new BasalScheduleEntry(0.05, Duration.standardMinutes(30)), // + new BasalScheduleEntry(1.70, Duration.standardMinutes(120)), // + new BasalScheduleEntry(0.85, Duration.standardMinutes(150)), // + new BasalScheduleEntry(1.00, Duration.standardMinutes(180)), // + new BasalScheduleEntry(0.65, Duration.standardMinutes(450)), // + new BasalScheduleEntry(0.50, Duration.standardMinutes(510)), // + new BasalScheduleEntry(0.65, Duration.standardMinutes(570)), // + new BasalScheduleEntry(0.60, Duration.standardMinutes(630)), // + new BasalScheduleEntry(0.65, Duration.standardMinutes(690)), // + new BasalScheduleEntry(1.65, Duration.standardMinutes(840)), // + new BasalScheduleEntry(0.85, Duration.standardMinutes(960)) // + ); + BasalSchedule basalSchedule = new BasalSchedule(entries); + + SetInsulinScheduleCommand setInsulinScheduleCommand = new SetInsulinScheduleCommand( // + 0xf36a23a3, // + basalSchedule, // + Duration.standardMinutes((0x03 + 1) * 30).minus(Duration.standardSeconds(0x0ae8 / 8))); + + assertArrayEquals( // + ByteUtil.fromHexString("1a2af36a23a3000291030ae80000000d280000111809700a180610052806100600072806001128100009e808"), // from https://github.com/ps2/rileylink_ios/blob/omnipod-testing/OmniKitTests/BasalScheduleTests.swift + setInsulinScheduleCommand.getRawData()); + } + + @Test + public void testBasalScheduleWithThirteenEntries() { + List entries = Arrays.asList( + new BasalScheduleEntry(1.30, Duration.ZERO), // + new BasalScheduleEntry(0.05, Duration.standardMinutes(30)), // + new BasalScheduleEntry(1.70, Duration.standardMinutes(120)), // + new BasalScheduleEntry(0.85, Duration.standardMinutes(150)), // + new BasalScheduleEntry(1.00, Duration.standardMinutes(180)), // + new BasalScheduleEntry(0.65, Duration.standardMinutes(450)), // + new BasalScheduleEntry(0.50, Duration.standardMinutes(510)), // + new BasalScheduleEntry(0.65, Duration.standardMinutes(570)), // + new BasalScheduleEntry(0.60, Duration.standardMinutes(630)), // + new BasalScheduleEntry(0.65, Duration.standardMinutes(690)), // + new BasalScheduleEntry(1.65, Duration.standardMinutes(840)), // + new BasalScheduleEntry(0.15, Duration.standardMinutes(930)), // + new BasalScheduleEntry(0.85, Duration.standardMinutes(990)) // + ); + BasalSchedule basalSchedule = new BasalSchedule(entries); + + SetInsulinScheduleCommand setInsulinScheduleCommand = new SetInsulinScheduleCommand( // + 0x851072aa, // + basalSchedule, // + Duration.standardMinutes((0x27 + 1) * 30).minus(Duration.standardSeconds(0x1518 / 8))); + + assertArrayEquals( // + ByteUtil.fromHexString("1a2a851072aa0001dd2715180003000d280000111809700a180610052806100600072806001118101801e808"), // from https://github.com/ps2/rileylink_ios/blob/omnipod-testing/OmniKitTests/BasalScheduleTests.swift + setInsulinScheduleCommand.getRawData()); + } + + @Test + public void testBasalScheduleRounding() { + List entries = Arrays.asList( + new BasalScheduleEntry(2.75, Duration.ZERO), + new BasalScheduleEntry(20.25, Duration.standardMinutes(60)), // + new BasalScheduleEntry(5D, Duration.standardMinutes(90)), // + new BasalScheduleEntry(10.1, Duration.standardMinutes(120)), // + new BasalScheduleEntry(0.05, Duration.standardMinutes(150)), // + new BasalScheduleEntry(3.5, Duration.standardMinutes(930)) // + ); + + BasalSchedule basalSchedule = new BasalSchedule(entries); + + SetInsulinScheduleCommand setInsulinScheduleCommand = new SetInsulinScheduleCommand( // + 0xc2a32da8, // + basalSchedule, // + Duration.standardMinutes((0x28 + 1) * 30).minus(Duration.standardSeconds(0x1af0 / 8))); + + assertArrayEquals( // + ByteUtil.fromHexString("1a1ec2a32da800053a281af00010181b00ca003200650001f8008800f0230023"), // from https://github.com/ps2/rileylink_ios/blob/omnipod-testing/OmniKitTests/BasalScheduleTests.swift + setInsulinScheduleCommand.getRawData()); + } + + @Test + public void testBasalScheduleRounding2() { + List entries = Arrays.asList( + new BasalScheduleEntry(0.6, Duration.ZERO), + new BasalScheduleEntry(0.65, Duration.standardMinutes(450)), // + new BasalScheduleEntry(0.5, Duration.standardMinutes(510)), // + new BasalScheduleEntry(0.65, Duration.standardMinutes(570)), // + new BasalScheduleEntry(0.15, Duration.standardMinutes(930)), // + new BasalScheduleEntry(0.8, Duration.standardMinutes(978)) // + ); + + BasalSchedule basalSchedule = new BasalSchedule(entries); + + SetInsulinScheduleCommand setInsulinScheduleCommand = new SetInsulinScheduleCommand( // + 0x851072aa, // + basalSchedule, // + Duration.standardMinutes((0x2c + 1) * 30).minus(Duration.standardSeconds(0x2190 / 8))); + + assertArrayEquals( // + ByteUtil.fromHexString("1a18851072aa00021b2c21900004f00600071005b8061801e008"), // from https://github.com/ps2/rileylink_ios/blob/omnipod-testing/OmniKitTests/BasalScheduleTests.swift + setInsulinScheduleCommand.getRawData()); + } + + @Test + public void testBasalScheduleSegmentMerging() { + List entries = Arrays.asList( + new BasalScheduleEntry(0.8, Duration.ZERO), + new BasalScheduleEntry(0.9, Duration.standardMinutes(180)), // + new BasalScheduleEntry(0.85, Duration.standardMinutes(300)), // + new BasalScheduleEntry(0.85, Duration.standardMinutes(450)), // + new BasalScheduleEntry(0.85, Duration.standardMinutes(750)), // + new BasalScheduleEntry(0.7, Duration.standardMinutes(900)), // + new BasalScheduleEntry(0.9, Duration.standardMinutes(1080)), // + new BasalScheduleEntry(1.10, Duration.standardMinutes(1200)) // + ); + + BasalSchedule basalSchedule = new BasalSchedule(entries); + + SetInsulinScheduleCommand setInsulinScheduleCommand = new SetInsulinScheduleCommand( // + 0x851072aa, // + basalSchedule, // + Duration.standardMinutes((0x2a + 1) * 30).minus(Duration.standardSeconds(0x1e50 / 8))); + + assertArrayEquals( // + ByteUtil.fromHexString("1a1a851072aa0002422a1e50000650083009f808380850073009700b"), // from https://github.com/ps2/rileylink_ios/blob/omnipod-testing/OmniKitTests/BasalScheduleTests.swift + setInsulinScheduleCommand.getRawData()); + } + + @Test + public void testBolus() { + BolusDeliverySchedule bolusDeliverySchedule = new BolusDeliverySchedule(2.6, Duration.standardSeconds(1)); + SetInsulinScheduleCommand setInsulinScheduleCommand = new SetInsulinScheduleCommand(0xbed2e16b, bolusDeliverySchedule); + + assertArrayEquals( // + ByteUtil.createByteArrayFromHexString("1a0ebed2e16b02010a0101a000340034"), // from https://github.com/ps2/rileylink_ios/blob/omnipod-testing/OmniKitTests/BolusTests.swift + setInsulinScheduleCommand.getRawData()); + } + + @Test + public void testBolusExtraCommandWithExtraOddPulseCount() { + BolusDeliverySchedule bolusDeliverySchedule = new BolusDeliverySchedule(2.05, Duration.standardSeconds(2)); + SetInsulinScheduleCommand setInsulinScheduleCommand = new SetInsulinScheduleCommand(0xcf9e81ac, bolusDeliverySchedule); + + assertArrayEquals( // + ByteUtil.createByteArrayFromHexString("1a0ecf9e81ac0200e501029000290029"), // from https://github.com/ps2/rileylink_ios/blob/omnipod-testing/OmniKitTests/BolusTests.swift + setInsulinScheduleCommand.getRawData()); + } + + @Test + public void testLargeBolus() { + BolusDeliverySchedule bolusDeliverySchedule = new BolusDeliverySchedule(29.95, Duration.standardSeconds(2)); + SetInsulinScheduleCommand setInsulinScheduleCommand = new SetInsulinScheduleCommand(0x31204ba7, bolusDeliverySchedule); + + assertArrayEquals( // + ByteUtil.createByteArrayFromHexString("1a0e31204ba702014801257002570257"), // from https://github.com/ps2/rileylink_ios/blob/omnipod-testing/OmniKitTests/BolusTests.swift + setInsulinScheduleCommand.getRawData()); + } +} diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/TempBasalExtraCommandTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/TempBasalExtraCommandTest.java new file mode 100644 index 0000000000..0e009d578c --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/command/TempBasalExtraCommandTest.java @@ -0,0 +1,75 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.command; + +import org.joda.time.Duration; +import org.junit.Test; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; + +import static org.junit.Assert.assertArrayEquals; + +public class TempBasalExtraCommandTest { + @Test + public void testTempBasalExtraCommand() { + TempBasalExtraCommand tempBasalExtraCommand = new TempBasalExtraCommand(30D, Duration.standardMinutes(30), + false, true, Duration.standardHours(1)); + assertArrayEquals(ByteUtil.fromHexString("160e7c000bb8000927c00bb8000927c0"), + tempBasalExtraCommand.getRawData()); + } + + @Test + public void testBasalExtraCommandForOddPulseCountRate() { + TempBasalExtraCommand tempBasalExtraCommand = new TempBasalExtraCommand(0.05, Duration.standardMinutes(30), + false, true, Duration.standardHours(1)); + assertArrayEquals(ByteUtil.fromHexString("160e7c00000515752a00000515752a00"), + tempBasalExtraCommand.getRawData()); + + TempBasalExtraCommand tempBasalExtraCommand2 = new TempBasalExtraCommand(2.05, Duration.standardMinutes(30), + false, false, Duration.standardHours(1)); + assertArrayEquals(ByteUtil.fromHexString("160e3c0000cd0085fac700cd0085fac7"), + tempBasalExtraCommand2.getRawData()); + + TempBasalExtraCommand tempBasalExtraCommand3 = new TempBasalExtraCommand(2.10, Duration.standardMinutes(30), + false, false, Duration.standardHours(1)); + assertArrayEquals(ByteUtil.fromHexString("160e3c0000d20082ca2400d20082ca24"), + tempBasalExtraCommand3.getRawData()); + + TempBasalExtraCommand tempBasalExtraCommand4 = new TempBasalExtraCommand(2.15, Duration.standardMinutes(30), + false, false, Duration.standardHours(1)); + assertArrayEquals(ByteUtil.fromHexString("160e3c0000d7007fbf7d00d7007fbf7d"), + tempBasalExtraCommand4.getRawData()); + } + + @Test + public void testBasalExtraCommandPulseCount() { + TempBasalExtraCommand tempBasalExtraCommand = new TempBasalExtraCommand(27.35, Duration.standardHours(12), + false, false, Duration.ZERO); + assertArrayEquals(ByteUtil.fromHexString("16140000f5b9000a0ad7f5b9000a0ad70aaf000a0ad7"), + tempBasalExtraCommand.getRawData()); + } + + @Test + public void testTempBasalExtraCommandExtremeValues() { + TempBasalExtraCommand tempBasalExtraCommand2 = new TempBasalExtraCommand(29.95, Duration.standardHours(12), + false, false, Duration.standardHours(1)); + assertArrayEquals(ByteUtil.fromHexString("16143c00f5af00092ba9f5af00092ba9231900092ba9"), + tempBasalExtraCommand2.getRawData()); + + TempBasalExtraCommand tempBasalExtraCommand = new TempBasalExtraCommand(30D, Duration.standardHours(12), + false, false, Duration.standardHours(1)); + assertArrayEquals(ByteUtil.fromHexString("16143c00f618000927c0f618000927c02328000927c0"), + tempBasalExtraCommand.getRawData()); + } + + @Test + public void testTempBasalExtraCommandZeroBasal() { + TempBasalExtraCommand tempBasalExtraCommand = new TempBasalExtraCommand(0D, Duration.standardMinutes(30), + false, true, Duration.standardHours(1)); + assertArrayEquals(ByteUtil.fromHexString("160e7c0000006b49d20000006b49d200"), + tempBasalExtraCommand.getRawData()); + + TempBasalExtraCommand tempBasalExtraCommand2 = new TempBasalExtraCommand(0D, Duration.standardHours(3), + false, true, Duration.standardHours(1)); + assertArrayEquals(ByteUtil.fromHexString("162c7c0000006b49d20000006b49d20000006b49d20000006b49d20000006b49d20000006b49d20000006b49d200"), + tempBasalExtraCommand2.getRawData()); + } +} diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/defs/schedule/BasalTableEntryTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/defs/schedule/BasalTableEntryTest.java new file mode 100644 index 0000000000..eac6943a5e --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/defs/schedule/BasalTableEntryTest.java @@ -0,0 +1,21 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.defs.schedule; + +import org.junit.Test; + +import info.nightscout.androidaps.plugins.pump.omnipod.defs.schedule.BasalTableEntry; + +import static junit.framework.Assert.assertEquals; + +public class BasalTableEntryTest { + @Test + public void testChecksum() { + BasalTableEntry basalTableEntry = new BasalTableEntry(2, 300, false); + assertEquals(0x5a, basalTableEntry.getChecksum()); + } + + @Test + public void testChecksumWithAlternatePulses() { + BasalTableEntry basalTableEntry = new BasalTableEntry(2, 260, true); + assertEquals(0x0b, basalTableEntry.getChecksum()); + } +} diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/ErrorResponseTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/ErrorResponseTest.java new file mode 100644 index 0000000000..c5556be9f0 --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/ErrorResponseTest.java @@ -0,0 +1,46 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response; + +import org.junit.Test; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.ErrorResponseType; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class ErrorResponseTest { + @Test + public void testGetRawData() { + byte[] encodedData = ByteUtil.fromHexString("060314fa92"); + ErrorResponse errorResponse = new ErrorResponse(encodedData); + + assertArrayEquals(encodedData, errorResponse.getRawData()); + } + + @Test + public void testGetRawDataWithLongerMessage() { + byte[] encodedData = ByteUtil.fromHexString("060314fa9201"); + byte[] expected = ByteUtil.fromHexString("060314fa92"); + + ErrorResponse errorResponse = new ErrorResponse(encodedData); + + assertArrayEquals(expected, errorResponse.getRawData()); + } + + @Test + public void testBadNonce() { + byte[] encodedData = ByteUtil.fromHexString("060314fa92"); + + ErrorResponse errorResponse = new ErrorResponse(encodedData); + assertEquals(ErrorResponseType.BAD_NONCE, errorResponse.getErrorResponseType()); + // TODO add assertion one nonce search key (obtain captures first) + } + + @Test + public void testUnknownError() { + ErrorResponse errorResponse = new ErrorResponse(ByteUtil.fromHexString("060307fa92")); + + assertNull(errorResponse.getErrorResponseType()); + } +} diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/StatusResponseTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/StatusResponseTest.java new file mode 100644 index 0000000000..6ac202622a --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/StatusResponseTest.java @@ -0,0 +1,81 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response; + +import org.joda.time.Duration; +import org.junit.Test; + +import info.nightscout.androidaps.Constants; +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.DeliveryStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodProgressStatus; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class StatusResponseTest { + // TODO add /extend tests + + @Test + public void testRawData() { + byte[] encodedData = ByteUtil.fromHexString("1d080000000038800000"); + + StatusResponse statusResponse = new StatusResponse(encodedData); + + assertArrayEquals(encodedData, statusResponse.getRawData()); + } + + @Test + public void testRawDataWithLongerMessage() { + byte[] encodedData = ByteUtil.fromHexString("1d08000000003880000001"); + byte[] expected = ByteUtil.fromHexString("1d080000000038800000"); + + StatusResponse statusResponse = new StatusResponse(encodedData); + + assertArrayEquals(expected, statusResponse.getRawData()); + } + + @Test + public void testWithSampleCapture() { + byte[] bytes = ByteUtil.fromHexString("1d180258f80000146fff"); // From https://github.com/openaps/openomni/wiki/Command-1D-Status-response + StatusResponse statusResponse = new StatusResponse(bytes); + + assertEquals(DeliveryStatus.NORMAL, statusResponse.getDeliveryStatus()); + assertEquals(PodProgressStatus.RUNNING_ABOVE_FIFTY_UNITS, statusResponse.getPodProgressStatus()); + assertNull("Reservoir level should be null", statusResponse.getReservoirLevel()); + assertEquals(Duration.standardMinutes(1307).getMillis(), statusResponse.getTimeActive().getMillis()); + assertEquals(60.05, statusResponse.getInsulin(), 0.000001); + assertEquals(15, statusResponse.getPodMessageCounter()); + assertEquals(0, statusResponse.getInsulinNotDelivered(), 0.000001); + assertEquals(0, statusResponse.getAlerts().getAlertSlots().size()); + + assertArrayEquals(ByteUtil.fromHexString("1d180258f80000146fff"), statusResponse.getRawData()); + } + + @Test + public void testLargeValues() { + byte[] bytes = ByteUtil.fromHexString("0011ffffffffffffffffff"); + StatusResponse statusResponse = new StatusResponse(bytes); + + assertEquals(Duration.standardMinutes(8191).getMillis(), statusResponse.getTimeActive().getMillis()); + assertEquals(Constants.POD_PULSE_SIZE * 1023, statusResponse.getInsulinNotDelivered(), 0.000001); + assertNull("Reservoir level should be null", statusResponse.getReservoirLevel()); + assertEquals(Constants.POD_PULSE_SIZE * 8191, statusResponse.getInsulin(), 0.0000001); + assertEquals(15, statusResponse.getPodMessageCounter()); + assertEquals(8, statusResponse.getAlerts().getAlertSlots().size()); + } + + @Test + public void testWithReservoirLevel() { + byte[] bytes = ByteUtil.fromHexString("1d19050ec82c08376f9801dc"); + StatusResponse statusResponse = new StatusResponse(bytes); + + assertTrue(Duration.standardMinutes(3547).isEqual(statusResponse.getTimeActive())); + assertEquals(DeliveryStatus.NORMAL, statusResponse.getDeliveryStatus()); + assertEquals(PodProgressStatus.RUNNING_BELOW_FIFTY_UNITS, statusResponse.getPodProgressStatus()); + assertEquals(129.45, statusResponse.getInsulin(), 0.00001); + assertEquals(46.00, statusResponse.getReservoirLevel(), 0.00001); + assertEquals(2.2, statusResponse.getInsulinNotDelivered(), 0.0001); + assertEquals(9, statusResponse.getPodMessageCounter()); + } +} \ No newline at end of file diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/VersionResponseTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/VersionResponseTest.java new file mode 100644 index 0000000000..62afe4f090 --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/VersionResponseTest.java @@ -0,0 +1,69 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response; + +import org.junit.Test; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodProgressStatus; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +public class VersionResponseTest { + @Test + public void testRawDataShortResponse() { + byte[] encodedData = ByteUtil.fromHexString("011502070002070002020000a64000097c279c1f08ced2"); + + VersionResponse versionResponse = new VersionResponse(encodedData); + assertArrayEquals(encodedData, versionResponse.getRawData()); + } + + @Test + public void testRawDataShortResponseWithLongerMessage() { + byte[] encodedData = ByteUtil.fromHexString("011502070002070002020000a64000097c279c1f08ced201"); + byte[] expected = ByteUtil.fromHexString("011502070002070002020000a64000097c279c1f08ced2"); + + VersionResponse versionResponse = new VersionResponse(encodedData); + assertArrayEquals(expected, versionResponse.getRawData()); + } + + @Test + public void testRawDataLongResponse() { + byte[] encodedData = ByteUtil.fromHexString("011b13881008340a5002070002070002030000a3770003ab371f00ee87"); + + VersionResponse versionResponse = new VersionResponse(encodedData); + + assertArrayEquals(encodedData, versionResponse.getRawData()); + } + + @Test + public void testRawDataLongResponseWithLongerMessage() { + byte[] encodedData = ByteUtil.fromHexString("011b13881008340a5002070002070002030000a3770003ab371f00ee8701"); + byte[] expected = ByteUtil.fromHexString("011b13881008340a5002070002070002030000a3770003ab371f00ee87"); + + VersionResponse versionResponse = new VersionResponse(encodedData); + assertArrayEquals(expected, versionResponse.getRawData()); + } + + @Test + public void testVersionResponse() { + VersionResponse versionResponse = new VersionResponse(ByteUtil.fromHexString("011502070002070002020000a64000097c279c1f08ced2")); + + assertEquals(0x1f08ced2, versionResponse.getAddress()); + assertEquals(42560, versionResponse.getLot()); + assertEquals(621607, versionResponse.getTid()); + assertEquals("2.7.0", versionResponse.getPiVersion().toString()); + assertEquals("2.7.0", versionResponse.getPmVersion().toString()); + } + + @Test + public void testLongVersionResponse() { + VersionResponse versionResponse = new VersionResponse(ByteUtil.fromHexString("011b13881008340a5002070002070002030000a3770003ab371f00ee87")); + + assertEquals(0x1f00ee87, versionResponse.getAddress()); + assertEquals(41847, versionResponse.getLot()); + assertEquals(240439, versionResponse.getTid()); + assertEquals(PodProgressStatus.PAIRING_SUCCESS, versionResponse.getPodProgressStatus()); + assertEquals("2.7.0", versionResponse.getPiVersion().toString()); + assertEquals("2.7.0", versionResponse.getPmVersion().toString()); + } +} diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoActiveAlertsTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoActiveAlertsTest.java new file mode 100644 index 0000000000..6b81ef2618 --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoActiveAlertsTest.java @@ -0,0 +1,35 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo; + +import org.joda.time.Duration; +import org.junit.Test; + +import java.util.List; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class PodInfoActiveAlertsTest { + @Test + public void testNoActiveAlerts() { + byte[] encodedMessage = ByteUtil.fromHexString("01000000000000000000000000000000000000"); // from https://github.com/ps2/rileylink_ios/blob/omnipod-testing/OmniKitTests/PodInfoTests.swift + PodInfoActiveAlerts podInfoActiveAlerts = new PodInfoActiveAlerts(encodedMessage); + + List alertActivations = podInfoActiveAlerts.getAlertActivations(); + assertEquals(0, alertActivations.size()); + } + + @Test + public void testReplacePodAfter3Days() { + byte[] encodedMessage = ByteUtil.fromHexString("010000000000000000000000000000000010e1"); // from https://github.com/ps2/rileylink_ios/blob/omnipod-testing/OmniKitTests/PodInfoTests.swift + PodInfoActiveAlerts podInfoActiveAlerts = new PodInfoActiveAlerts(encodedMessage); + + List alertActivations = podInfoActiveAlerts.getAlertActivations(); + assertEquals(1, alertActivations.size()); + + PodInfoActiveAlerts.AlertActivation alertActivation = alertActivations.get(0); + Duration expectedDuration = Duration.standardHours(72).plus(Duration.standardMinutes(1)); + assertTrue(expectedDuration.isEqual(alertActivation.getValueAsDuration())); + } +} diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoDataLogTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoDataLogTest.java new file mode 100644 index 0000000000..5db87003bc --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoDataLogTest.java @@ -0,0 +1,23 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo; + +import org.joda.time.Duration; +import org.junit.Test; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.FaultEventCode; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class PodInfoDataLogTest { + @Test + public void testDecoding() { + PodInfoDataLog podInfoDataLog = new PodInfoDataLog(ByteUtil.fromHexString("030100010001043c"), 8); // From https://github.com/ps2/rileylink_ios/blob/omnipod-testing/OmniKitTests/PodInfoTests.swift + + assertEquals(FaultEventCode.FAILED_FLASH_ERASE, podInfoDataLog.getFaultEventCode()); + assertTrue(Duration.standardMinutes(1).isEqual(podInfoDataLog.getTimeFaultEvent())); + assertTrue(Duration.standardMinutes(1).isEqual(podInfoDataLog.getTimeSinceActivation())); + assertEquals(4, podInfoDataLog.getDataChunkSize()); + assertEquals(60, podInfoDataLog.getMaximumNumberOfDwords()); + } +} diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoFaultAndInitializationTimeTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoFaultAndInitializationTimeTest.java new file mode 100644 index 0000000000..a7baff4f06 --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoFaultAndInitializationTimeTest.java @@ -0,0 +1,28 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo; + +import org.joda.time.DateTime; +import org.joda.time.Duration; +import org.junit.Test; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.FaultEventCode; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class PodInfoFaultAndInitializationTimeTest { + @Test + public void testDecoding() { + PodInfoFaultAndInitializationTime podInfoFaultAndInitializationTime = new PodInfoFaultAndInitializationTime(ByteUtil.fromHexString("059200010000000000000000091912170e")); // From https://github.com/ps2/rileylink_ios/blob/omnipod-testing/OmniKitTests/PodInfoTests.swift + + assertEquals(FaultEventCode.BAD_PUMP_REQ_2_STATE, podInfoFaultAndInitializationTime.getFaultEventCode()); + assertTrue(Duration.standardMinutes(1).isEqual(podInfoFaultAndInitializationTime.getTimeFaultEvent())); + + DateTime dateTime = podInfoFaultAndInitializationTime.getInitializationTime(); + assertEquals(2018, dateTime.getYear()); + assertEquals(9, dateTime.getMonthOfYear()); + assertEquals(25, dateTime.getDayOfMonth()); + assertEquals(23, dateTime.getHourOfDay()); + assertEquals(14, dateTime.getMinuteOfHour()); + } +} diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoFaultEventTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoFaultEventTest.java new file mode 100644 index 0000000000..a754937ef7 --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoFaultEventTest.java @@ -0,0 +1,98 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo; + +import org.joda.time.Duration; +import org.junit.Test; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.DeliveryStatus; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.FaultEventCode; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.LogEventErrorCode; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodProgressStatus; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +// From https://github.com/ps2/rileylink_ios/blob/omnipod-testing/OmniKitTests/PodInfoTests.swift +public class PodInfoFaultEventTest { + @Test + public void testPodInfoFaultEventNoFaultAlerts() { + PodInfoFaultEvent podInfoFaultEvent = new PodInfoFaultEvent(ByteUtil.fromHexString("02080100000a003800000003ff008700000095ff0000")); + + assertEquals(PodProgressStatus.RUNNING_ABOVE_FIFTY_UNITS, podInfoFaultEvent.getPodProgressStatus()); + assertEquals(DeliveryStatus.NORMAL, podInfoFaultEvent.getDeliveryStatus()); + assertEquals(0, podInfoFaultEvent.getInsulinNotDelivered(), 0.000001); + assertEquals(0x0a, podInfoFaultEvent.getPodMessageCounter()); + assertEquals(FaultEventCode.NO_FAULTS, podInfoFaultEvent.getFaultEventCode()); + assertTrue(Duration.ZERO.isEqual(podInfoFaultEvent.getFaultEventTime())); + assertNull(podInfoFaultEvent.getReservoirLevel()); + assertTrue(Duration.standardSeconds(8100).isEqual(podInfoFaultEvent.getTimeSinceActivation())); + assertEquals(0, podInfoFaultEvent.getUnacknowledgedAlerts().getRawValue()); + assertFalse(podInfoFaultEvent.isFaultAccessingTables()); + assertEquals(LogEventErrorCode.NONE, podInfoFaultEvent.getLogEventErrorType()); + assertEquals(PodProgressStatus.INACTIVE, podInfoFaultEvent.getPodProgressStatusAtTimeOfFirstLoggedFaultEvent()); + assertEquals(2, podInfoFaultEvent.getReceiverLowGain()); + assertEquals(21, podInfoFaultEvent.getRadioRSSI()); + } + + @Test + public void testPodInfoFaultEventDeliveryErrorDuringPriming() { + PodInfoFaultEvent podInfoFaultEvent = new PodInfoFaultEvent(ByteUtil.fromHexString("020f0000000900345c000103ff0001000005ae056029")); + + assertEquals(PodProgressStatus.INACTIVE, podInfoFaultEvent.getPodProgressStatus()); + assertEquals(DeliveryStatus.SUSPENDED, podInfoFaultEvent.getDeliveryStatus()); + assertEquals(0, podInfoFaultEvent.getInsulinNotDelivered(), 0.000001); + assertEquals(0x09, podInfoFaultEvent.getPodMessageCounter()); + assertEquals(FaultEventCode.PRIME_OPEN_COUNT_TOO_LOW, podInfoFaultEvent.getFaultEventCode()); + assertTrue(Duration.standardSeconds(60).isEqual(podInfoFaultEvent.getFaultEventTime())); + assertNull(podInfoFaultEvent.getReservoirLevel()); + assertTrue(Duration.standardSeconds(60).isEqual(podInfoFaultEvent.getTimeSinceActivation())); + assertEquals(0, podInfoFaultEvent.getUnacknowledgedAlerts().getRawValue()); + assertFalse(podInfoFaultEvent.isFaultAccessingTables()); + assertEquals(LogEventErrorCode.NONE, podInfoFaultEvent.getLogEventErrorType()); + assertEquals(PodProgressStatus.READY_FOR_BASAL_SCHEDULE, podInfoFaultEvent.getPodProgressStatusAtTimeOfFirstLoggedFaultEvent()); + assertEquals(2, podInfoFaultEvent.getReceiverLowGain()); + assertEquals(46, podInfoFaultEvent.getRadioRSSI()); + } + + @Test + public void testPodInfoFaultEventErrorShuttingDown() { + PodInfoFaultEvent podInfoFaultEvent = new PodInfoFaultEvent(ByteUtil.fromHexString("020d0000000407f28609ff03ff0a0200000823080000")); + + assertEquals(PodProgressStatus.ERROR_EVENT_LOGGED_SHUTTING_DOWN, podInfoFaultEvent.getPodProgressStatus()); + assertEquals(DeliveryStatus.SUSPENDED, podInfoFaultEvent.getDeliveryStatus()); + assertEquals(101.7, podInfoFaultEvent.getTotalInsulinDelivered(), 0.000001); + assertEquals(0, podInfoFaultEvent.getInsulinNotDelivered(), 0.000001); + assertEquals(0x04, podInfoFaultEvent.getPodMessageCounter()); + assertEquals(FaultEventCode.BASAL_OVER_INFUSION_PULSE, podInfoFaultEvent.getFaultEventCode()); + assertTrue(Duration.standardMinutes(2559).isEqual(podInfoFaultEvent.getFaultEventTime())); + assertNull(podInfoFaultEvent.getReservoirLevel()); + assertEquals(0, podInfoFaultEvent.getUnacknowledgedAlerts().getRawValue()); + assertFalse(podInfoFaultEvent.isFaultAccessingTables()); + assertEquals(LogEventErrorCode.NONE, podInfoFaultEvent.getLogEventErrorType()); + assertEquals(PodProgressStatus.RUNNING_ABOVE_FIFTY_UNITS, podInfoFaultEvent.getPodProgressStatusAtTimeOfFirstLoggedFaultEvent()); + assertEquals(0, podInfoFaultEvent.getReceiverLowGain()); + assertEquals(35, podInfoFaultEvent.getRadioRSSI()); + } + + @Test + public void testPodInfoFaultEventIsulinNotDelivered() { + PodInfoFaultEvent podInfoFaultEvent = new PodInfoFaultEvent(ByteUtil.fromHexString("020f0000010200ec6a026803ff026b000028a7082023")); + + assertEquals(PodProgressStatus.INACTIVE, podInfoFaultEvent.getPodProgressStatus()); + assertEquals(DeliveryStatus.SUSPENDED, podInfoFaultEvent.getDeliveryStatus()); + assertEquals(11.8, podInfoFaultEvent.getTotalInsulinDelivered(), 0.000001); + assertEquals(0.05, podInfoFaultEvent.getInsulinNotDelivered(), 0.000001); + assertEquals(0x02, podInfoFaultEvent.getPodMessageCounter()); + assertEquals(FaultEventCode.OCCLUSION_CHECK_ABOVE_THRESHOLD, podInfoFaultEvent.getFaultEventCode()); + assertTrue(Duration.standardMinutes(616).isEqual(podInfoFaultEvent.getFaultEventTime())); + assertNull(podInfoFaultEvent.getReservoirLevel()); + assertEquals(0, podInfoFaultEvent.getUnacknowledgedAlerts().getRawValue()); + assertFalse(podInfoFaultEvent.isFaultAccessingTables()); + assertEquals(LogEventErrorCode.INTERNAL_2_BIT_VARIABLE_SET_AND_MANIPULATED_IN_MAIN_LOOP_ROUTINES_2, podInfoFaultEvent.getLogEventErrorType()); + assertEquals(PodProgressStatus.RUNNING_ABOVE_FIFTY_UNITS, podInfoFaultEvent.getPodProgressStatusAtTimeOfFirstLoggedFaultEvent()); + assertEquals(2, podInfoFaultEvent.getReceiverLowGain()); + assertEquals(39, podInfoFaultEvent.getRadioRSSI()); + } +} diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoLowFlashLogDumpTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoLowFlashLogDumpTest.java new file mode 100644 index 0000000000..12c51657bc --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoLowFlashLogDumpTest.java @@ -0,0 +1,17 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo; + +import org.junit.Test; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; + +import static org.junit.Assert.assertEquals; + +public class PodInfoLowFlashLogDumpTest { + @Test + public void testDecoding() { + PodInfoLowFlashLogDump podInfoLowFlashLogDump = new PodInfoLowFlashLogDump(ByteUtil.fromHexString("4600791f00ee841f00ee84ff00ff00ffffffffffff0000ffffffffffffffffffffffff04060d10070000a62b0004e3db0000ffffffffffffff32cd50af0ff014eb01fe01fe06f9ff00ff0002fd649b14eb14eb07f83cc332cd05fa02fd58a700ffffffffffffffffffffffffffffffffffffffffffffffffffffffff")); // from https://github.com/ps2/rileylink_ios/blob/omnipod-testing/OmniKitTests/PodInfoTests.swift + + assertEquals(121, podInfoLowFlashLogDump.getNumberOfBytes()); + assertEquals(0x1f00ee84, podInfoLowFlashLogDump.getPodAddress()); + } +} diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoOlderHighFlashLogDumpTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoOlderHighFlashLogDumpTest.java new file mode 100644 index 0000000000..088cb4c977 --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoOlderHighFlashLogDumpTest.java @@ -0,0 +1,5 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo; + +public class PodInfoOlderHighFlashLogDumpTest { + // TODO +} diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoRecentHighFlashLogDumpTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoRecentHighFlashLogDumpTest.java new file mode 100644 index 0000000000..18f8dd3a2b --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoRecentHighFlashLogDumpTest.java @@ -0,0 +1,16 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo; + +import org.junit.Test; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; + +import static org.junit.Assert.assertEquals; + +public class PodInfoRecentHighFlashLogDumpTest { + @Test + public void testDecoding() { + PodInfoRecentHighFlashLogDump podInfoRecentHighFlashLogDump = new PodInfoRecentHighFlashLogDump(ByteUtil.fromHexString("3d313b004030350045303a00483033004d313a005031310054313f00583038805d302d806030368001313b800c3033801130388014313480193138801c313280213039802431360029313d002c31390031303f0034313900393140003c31390041313e00443137004905723a80087335800d733a801073358015733a80187235801d7338802073338025733a00287235002d723b003072360035703b00383134"), 160); + + assertEquals(39, podInfoRecentHighFlashLogDump.getDwords().size()); + } +} diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoResponseTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoResponseTest.java new file mode 100644 index 0000000000..583456e2d2 --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoResponseTest.java @@ -0,0 +1,58 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.LogEventErrorCode; +import info.nightscout.androidaps.plugins.pump.omnipod.defs.PodInfoType; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +public class PodInfoResponseTest { + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void testRawData() { + byte[] encodedData = ByteUtil.fromHexString("0216020d0000000000ab6a038403ff03860000285708030d"); + + PodInfoResponse podInfoResponse = new PodInfoResponse(encodedData); + + assertArrayEquals(encodedData, podInfoResponse.getRawData()); + } + + @Test + public void testRawDataWithLongerMessage() { + byte[] encodedData = ByteUtil.fromHexString("0216020d0000000000ab6a038403ff03860000285708030d01"); + byte[] expected = ByteUtil.fromHexString("0216020d0000000000ab6a038403ff03860000285708030d"); + + PodInfoResponse podInfoResponse = new PodInfoResponse(encodedData); + + assertArrayEquals(expected, podInfoResponse.getRawData()); + } + + @Test + public void testMessageDecoding() { + PodInfoResponse podInfoResponse = new PodInfoResponse(ByteUtil.fromHexString("0216020d0000000000ab6a038403ff03860000285708030d")); + + assertEquals(PodInfoType.FAULT_EVENT, podInfoResponse.getSubType()); + + PodInfoFaultEvent podInfo = podInfoResponse.getPodInfo(); + assertFalse(podInfo.isFaultAccessingTables()); + assertEquals(LogEventErrorCode.INTERNAL_2_BIT_VARIABLE_SET_AND_MANIPULATED_IN_MAIN_LOOP_ROUTINES_2, podInfo.getLogEventErrorType()); + } + + @Test + public void testInvalidPodInfoTypeMessageDecoding() { + PodInfoResponse podInfoResponse = new PodInfoResponse(ByteUtil.fromHexString("0216020d0000000000ab6a038403ff03860000285708030d")); + + assertEquals(PodInfoType.FAULT_EVENT, podInfoResponse.getSubType()); + + thrown.expect(ClassCastException.class); + PodInfoActiveAlerts podInfo = podInfoResponse.getPodInfo(); + } +} diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoTestValuesTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoTestValuesTest.java new file mode 100644 index 0000000000..1c05a58a87 --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/comm/message/response/podinfo/PodInfoTestValuesTest.java @@ -0,0 +1,19 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.comm.message.response.podinfo; + +import org.junit.Test; + +import info.nightscout.androidaps.plugins.pump.common.utils.ByteUtil; + +import static org.junit.Assert.assertEquals; + +public class PodInfoTestValuesTest { + @Test + public void testDecoding() { + PodInfoTestValues podInfoTestValues = new PodInfoTestValues(ByteUtil.fromHexString("0601003FA8")); + + assertEquals((byte) 0x01, podInfoTestValues.getByte1()); + assertEquals((byte) 0x00, podInfoTestValues.getByte2()); + assertEquals((byte) 0x3f, podInfoTestValues.getByte3()); + assertEquals((byte) 0xa8, podInfoTestValues.getByte4()); + } +}