All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.bidib.wizard.dcca.client.model.DccAdvDecoderModel Maven / Gradle / Ivy

There is a newer version: 2.0.29
Show newest version
package org.bidib.wizard.dcca.client.model;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.SortedSet;
import java.util.TreeSet;

import javax.swing.SwingUtilities;

import org.apache.commons.collections4.IterableUtils;
import org.apache.commons.collections4.Predicate;
import org.apache.commons.lang3.StringUtils;
import org.bidib.jbidibc.messages.DccAInfoIndexedString;
import org.bidib.jbidibc.messages.DccAInfoShortGui;
import org.bidib.jbidibc.messages.DccAInfoShortInfo;
import org.bidib.jbidibc.messages.DecoderIdAddressData;
import org.bidib.jbidibc.messages.enums.AddressMode;
import org.bidib.jbidibc.messages.enums.DccAOpCodeBm;
import org.bidib.jbidibc.messages.enums.DccAdvSelectInfoOpCode;
import org.bidib.jbidibc.messages.utils.ByteUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.jgoodies.binding.beans.Model;

/**
 * This model holds the state of a DccAdv decoder in the system.
 *
 */
public class DccAdvDecoderModel extends Model {

    private static final long serialVersionUID = 1L;

    private static final Logger LOGGER = LoggerFactory.getLogger(DccAdvDecoderModel.class);

    public static final String PROPERTY_CURRENT_ANSWER_RETRY_COUNT = "currentAnswerRetryCount";

    public static final String PROPERTY_DECODER_UNIQUE_ID = "decoderUniqueId";

    public static final String PROPERTY_DECODER_WISH_ADDRESS = "decoderWishAddress";

    public static final String PROPERTY_DECODER_ADDRESS = "decoderAddress";

    public static final String PROPERTY_SHORTNAME = "shortName";

    public static final String PROPERTY_SHORTINFO = "shortInfo";

    public static final String PROPERTY_SHORTGUI = "shortGui";

    public static final String PROPERTY_FULLNAME = "fullName";

    public static final String PROPERTY_PRODUCTNAME = "productName";

    public static final String PROPERTY_FIRMWAREID = "firmwareId";

    public static final String PROPERTY_DETECTIONSTATUS = "detectionStatus";

    public static final int MAX_ANSWER_RETRY_COUNT = 3;

    private int currentAnswerRetryCount;

    private int maxAnswerRetryCount = MAX_ANSWER_RETRY_COUNT;

    private DecoderIdAddressData decoderUniqueId;

    private Integer decoderWishAddress;

    private Integer decoderAddress;

    private AddressMode decoderAddressMode;

    private DccAInfoShortInfo shortInfo;

    private DccAInfoShortGui shortGui;

    // expecting 7 data packets to complete
    private IndexedStringCollector fullName = new IndexedStringCollector(7);

    // expecting 2 data packets to complete
    private IndexedStringCollector productName = new IndexedStringCollector(2);

    // expecting 2 data packets to complete
    private IndexedStringCollector shortName = new IndexedStringCollector(2);

    // expecting 2 data packets to complete
    private IndexedStringCollector firmwareId = new IndexedStringCollector(2);

    private DccAOpCodeBm lastReceivedOpCode;

    private boolean[] addressPartsSet = new boolean[2];

    private final Object checkDataCompleteLock = new Object();

    public enum DetectionStatus {
        UNKNOWN, NEW_DID, GET_INFO, ASSIGNED, UNRESPONSIVE;
    }

    private static class IndexedStringCollector {
        private SortedSet indexList =
            new TreeSet<>(Comparator.comparing(DccAInfoIndexedString::getIndex));

        private final int expectedItemsToComplete;

        // public IndexedStringCollector() {
        // this(1);
        // }

        public IndexedStringCollector(int expectedItemsToComplete) {
            this.expectedItemsToComplete = expectedItemsToComplete;
        }

        // /**
        // * @return the expectedItemsToComplete
        // */
        // public int getExpectedItemsToComplete() {
        // return expectedItemsToComplete;
        // }

        public boolean isDataComplete() {
            return expectedItemsToComplete == indexList.size();
        }

        public void addIndexedString(final DccAInfoIndexedString string) {

            DccAInfoIndexedString oldValue = IterableUtils.find(indexList, new Predicate() {

                @Override
                public boolean evaluate(DccAInfoIndexedString instance) {
                    return instance.getIndex() == string.getIndex();
                }
            });
            if (oldValue != null) {
                LOGGER.info("Remove old value: {}", oldValue);
                indexList.remove(oldValue);
            }
            indexList.add(string);
        }

        public String getString() {
            // concat the string values
            StringBuilder sb = new StringBuilder();
            for (DccAInfoIndexedString string : indexList) {
                sb.append(string.getData());
            }
            return sb.toString();
        }

        public void setString(String fullName) {
            indexList.clear();
            indexList.add(new DccAInfoIndexedString(0, fullName));
        }
    }

    private DetectionStatus detectionStatus = DetectionStatus.UNKNOWN;

    public DccAdvDecoderModel() {

    }

    /**
     * @return the maxAnswerRetryCount
     */
    public int getMaxAnswerRetryCount() {
        return maxAnswerRetryCount;
    }

    /**
     * @param maxAnswerRetryCount
     *            the maxAnswerRetryCount to set
     */
    public void setMaxAnswerRetryCount(int maxAnswerRetryCount) {
        this.maxAnswerRetryCount = maxAnswerRetryCount;
    }

    /**
     * @return the currentAnswerRetryCount
     */
    public int getCurrentAnswerRetryCount() {
        return currentAnswerRetryCount;
    }

    /**
     * @param currentAnswerRetryCount
     *            the currentAnswerRetryCount to set
     */
    public void setCurrentAnswerRetryCount(int currentAnswerRetryCount) {
        int oldValue = this.currentAnswerRetryCount;
        this.currentAnswerRetryCount = currentAnswerRetryCount;
        edtFirePropertyChange(PROPERTY_CURRENT_ANSWER_RETRY_COUNT, oldValue, this.currentAnswerRetryCount);
    }

    /**
     * Increment the answer retry count.
     */
    public void incCurrentAnswerRetryCount() {
        int oldValue = this.currentAnswerRetryCount;
        this.currentAnswerRetryCount++;

        LOGGER.info("Incremented the currentAnswerRetryCount: {}", currentAnswerRetryCount);

        edtFirePropertyChange(PROPERTY_CURRENT_ANSWER_RETRY_COUNT, oldValue, this.currentAnswerRetryCount);
    }

    /**
     * @return the max answer retry count is exceeded
     */
    public boolean isMaxAnswerRetryExceeded() {
        return currentAnswerRetryCount > maxAnswerRetryCount;
    }

    /**
     * @return the vendorId
     */
    public DecoderIdAddressData getDecoderUniqueId() {
        return decoderUniqueId;
    }

    /**
     * @param decoderUniqueData
     *            the did to set
     */
    public void setDecoderUniqueId(DecoderIdAddressData decoderUniqueData) {
        DecoderIdAddressData oldValue = this.decoderUniqueId;
        this.decoderUniqueId = decoderUniqueData;
        edtFirePropertyChange(PROPERTY_DECODER_UNIQUE_ID, oldValue, this.decoderUniqueId);
    }

    /**
     * @return the decoderUniqueId of the decoder or {@code null}
     */
    public Long getDecoderDid() {
        if (decoderUniqueId != null) {
            return decoderUniqueId.getDid();
        }
        return null;
    }

    /**
     * @return the manufacturer id of the decoder or {@code null}
     */
    public Integer getManufacturerId() {
        if (decoderUniqueId != null) {
            return decoderUniqueId.getManufacturedId();
        }
        return null;
    }

    public String getFormattedDecoderUniqueId() {

        if (decoderUniqueId != null) {
            return String.format("%04X %08X", decoderUniqueId.getManufacturedId(), decoderUniqueId.getDid());
        }
        return null;
    }

    public Integer getDecoderWishAddress() {
        return decoderWishAddress;
    }

    /**
     * @param decoderWishAddress
     *            the decoderAddress to set
     */
    public void setDecoderWishAddress(Integer decoderWishAddress) {
        LOGGER.info("Set the decoder wish address: {}", decoderWishAddress);
        Integer oldValue = this.decoderWishAddress;

        this.decoderWishAddress = decoderWishAddress;

        edtFirePropertyChange(PROPERTY_DECODER_WISH_ADDRESS, oldValue, this.decoderWishAddress);
    }

    /**
     * @return the decoderAddress
     */
    public Integer getDecoderAddress() {
        return decoderAddress;
    }

    /**
     * @param decoderAddress
     *            the decoderAddress to set
     */
    public void setDecoderAddress(Integer decoderAddress) {
        LOGGER.info("Set the decoder address: {}", decoderAddress);
        Integer oldValue = this.decoderAddress;

        this.decoderAddress = decoderAddress;

        if (decoderAddress == null) {
            addressPartsSet[0] = false;
            addressPartsSet[1] = false;
        }
        else {
            addressPartsSet[0] = true;
            addressPartsSet[1] = true;
        }

        edtFirePropertyChange(PROPERTY_DECODER_ADDRESS, oldValue, this.decoderAddress);
    }

    /**
     * @param decoderAddressLowValue
     *            the decoderAddress low value to set
     */
    public void setDecoderAddressLowValue(Integer decoderAddressLowValue) {
        LOGGER.info("Set the decoder address low value: {}", decoderAddressLowValue);

        if (decoderAddress == null) {
            decoderAddress = 0;
        }

        decoderAddress = ByteUtils.toInt((decoderAddress >> 8), decoderAddressLowValue);
        addressPartsSet[0] = true;
    }

    /**
     * @param decoderAddressHighValue
     *            the decoderAddress high value to set
     */
    public void setDecoderAddressHighValue(Integer decoderAddressHighValue) {
        LOGGER.info("Set the decoder address high value: {}", decoderAddressHighValue);

        if (decoderAddress == null) {
            decoderAddress = 0;
        }

        decoderAddress = ByteUtils.toInt(decoderAddressHighValue, decoderAddress);
        addressPartsSet[1] = true;
    }

    public boolean isAddressComplete() {
        boolean addressComplete = false;
        if (AddressMode.SHORT == decoderAddressMode) {
            // short address only needs low byte
            addressComplete = addressPartsSet[0];
        }
        else {
            addressComplete = (addressPartsSet[0] && addressPartsSet[1]);
        }
        return addressComplete;
    }

    /**
     * @param addressMode
     *            the addressMode to set
     */
    public void setDecoderAddressMode(AddressMode addressMode) {
        this.decoderAddressMode = addressMode;
    }

    /**
     * @return the addressMode
     */
    public AddressMode getDecoderAddressMode() {
        return decoderAddressMode;
    }

    /**
     * @return the detectionStatus
     */
    public DetectionStatus getDetectionStatus() {
        if (detectionStatus == null) {
            return DetectionStatus.UNKNOWN;
        }
        return detectionStatus;
    }

    /**
     * @param detectionStatus
     *            the detectionStatus to set
     */
    public void setDetectionStatus(DetectionStatus detectionStatus) {
        DetectionStatus oldValue = this.detectionStatus;
        this.detectionStatus = detectionStatus;

        edtFirePropertyChange(PROPERTY_DETECTIONSTATUS, oldValue, this.detectionStatus);
    }

    private void edtFirePropertyChange(final String propertyName, final Object oldValue, final Object newValue) {
        if (SwingUtilities.isEventDispatchThread()) {
            super.firePropertyChange(propertyName, oldValue, newValue);
        }
        else {
            SwingUtilities.invokeLater(() -> {
                try {
                    super.firePropertyChange(propertyName, oldValue, newValue);
                }
                catch (Exception ex) {
                    LOGGER
                        .warn("Fire property changed failed, propertyName: {}, oldValue: {}, newValue: {}",
                            propertyName, oldValue, newValue, ex);
                }
            });
        }
    }

    /**
     * @return the shortInfo
     */
    public DccAInfoShortInfo getShortInfo() {
        return shortInfo;
    }

    /**
     * @param shortInfo
     *            the shortInfo to set
     */
    public void setShortInfo(DccAInfoShortInfo shortInfo) {
        DccAInfoShortInfo oldValue = this.shortInfo;
        this.shortInfo = shortInfo;
        edtFirePropertyChange(PROPERTY_SHORTINFO, oldValue, this.shortInfo);

        setDecoderWishAddress(shortInfo.getDemandedLocoAddress());

        // update the las received opcode
        setLastReceivedOpCode(DccAOpCodeBm.BIDIB_DCCA_INFO_SHORTINFO);
    }

    /**
     * @return the shortGui
     */
    public DccAInfoShortGui getShortGui() {
        return shortGui;
    }

    /**
     * @param shortGui
     *            the shortGui to set
     */
    public void setShortGui(DccAInfoShortGui shortGui) {
        DccAInfoShortGui oldValue = this.shortGui;
        this.shortGui = shortGui;
        edtFirePropertyChange(PROPERTY_SHORTGUI, oldValue, this.shortGui);

        // update the las received opcode
        setLastReceivedOpCode(DccAOpCodeBm.BIDIB_DCCA_INFO_SHORTGUI);
    }

    /**
     * @return the fullName
     */
    public String getFullName() {
        return fullName.getString();
    }

    /**
     * @param fullName
     *            the fullName to set
     */
    public void setFullName(DccAInfoIndexedString fullName) {
        String oldValue = this.fullName.getString();

        LOGGER.info("Update the fullname: {}", fullName);

        if (StringUtils.isBlank(fullName.getData())) {
            LOGGER.info("No data delivered in fullName.");
            // return;
        }

        this.fullName.addIndexedString(fullName);
        edtFirePropertyChange(PROPERTY_FULLNAME, oldValue, this.fullName.getString());

        // update the las received opcode
        setLastReceivedOpCode(DccAOpCodeBm.BIDIB_DCCA_INFO_FULLNAME);
    }

    /**
     * @param fullName
     *            the fullName to set
     */
    public void setFullName(String fullName) {
        String oldValue = this.fullName.getString();
        this.fullName.setString(fullName);
        edtFirePropertyChange(PROPERTY_FULLNAME, oldValue, this.fullName.getString());
    }

    /**
     * @return the productName
     */
    public String getProductName() {
        return productName.getString();
    }

    /**
     * @param productName
     *            the productName to set
     */
    public void setProductName(DccAInfoIndexedString productName) {
        String oldValue = this.productName.getString();

        if (StringUtils.isBlank(productName.getData())) {
            LOGGER.info("No data delivered in productName.");
            // return;
        }

        this.productName.addIndexedString(productName);
        edtFirePropertyChange(PROPERTY_PRODUCTNAME, oldValue, this.productName.getString());

        // update the las received opcode
        setLastReceivedOpCode(DccAOpCodeBm.BIDIB_DCCA_INFO_PRODUCTNAME);
    }

    /**
     * @param productName
     *            the productName to set
     */
    public void setProductName(String productName) {
        String oldValue = this.productName.getString();
        this.productName.setString(productName);
        edtFirePropertyChange(PROPERTY_PRODUCTNAME, oldValue, this.productName.getString());
    }

    /**
     * @return the shortName
     */
    public String getShortName() {
        return shortName.getString();
    }

    /**
     * @param shortName
     *            the shortName to set
     */
    public void setShortName(DccAInfoIndexedString shortName) {
        String oldValue = this.shortName.getString();

        if (StringUtils.isBlank(shortName.getData())) {
            LOGGER.info("No data delivered in shortName.");
            // return;
        }

        this.shortName.addIndexedString(shortName);
        edtFirePropertyChange(PROPERTY_SHORTNAME, oldValue, this.shortName.getString());

        // update the las received opcode
        setLastReceivedOpCode(DccAOpCodeBm.BIDIB_DCCA_INFO_SHORTNAME);
    }

    /**
     * @param shortName
     *            the shortName to set
     */
    public void setShortName(String shortName) {
        String oldValue = this.shortName.getString();
        this.shortName.setString(shortName);
        edtFirePropertyChange(PROPERTY_SHORTNAME, oldValue, this.shortName.getString());
    }

    /**
     * @return the firmwareId
     */
    public String getFirmwareId() {
        return firmwareId.getString();
    }

    /**
     * @param firmwareId
     *            the firmwareId to set
     */
    public void setFirmwareId(DccAInfoIndexedString firmwareId) {
        String oldValue = this.firmwareId.getString();

        if (StringUtils.isBlank(firmwareId.getData())) {
            LOGGER.info("No data delivered in firmwareId.");
            // return;
        }

        this.firmwareId.addIndexedString(firmwareId);
        edtFirePropertyChange(PROPERTY_FIRMWAREID, oldValue, this.firmwareId.getString());

        // update the las received opcode
        setLastReceivedOpCode(DccAOpCodeBm.BIDIB_DCCA_INFO_FIRMWAREID);
    }

    /**
     * @param firmwareId
     *            the firmwareId to set
     */
    public void setFirmwareId(String firmwareId) {
        String oldValue = this.firmwareId.getString();
        this.firmwareId.setString(firmwareId);
        edtFirePropertyChange(PROPERTY_FIRMWAREID, oldValue, this.firmwareId.getString());
    }

    /**
     * @return the lastReceivedOpCode
     */
    public DccAOpCodeBm getLastReceivedOpCode() {
        return lastReceivedOpCode;
    }

    /**
     * @param lastReceivedOpCode
     *            the lastReceivedOpCode to set
     */
    public void setLastReceivedOpCode(DccAOpCodeBm lastReceivedOpCode) {
        LOGGER.info("Set the last received opCode: {}", lastReceivedOpCode);
        this.lastReceivedOpCode = lastReceivedOpCode;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof DccAdvDecoderModel) {
            DccAdvDecoderModel other = (DccAdvDecoderModel) obj;
            if (decoderUniqueId != null && Objects.equals(decoderUniqueId, other.decoderUniqueId)) {
                return true;
            }
            if (decoderAddress != null && Objects.equals(decoderAddress, other.decoderAddress)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public int hashCode() {
        int hashCode = super.hashCode();
        if (decoderUniqueId != null) {
            hashCode += decoderUniqueId.hashCode();
        }
        // if (decoderAddress != null) {
        // hashCode += decoderAddress.hashCode();
        // }
        // ignore decoder address
        return hashCode;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder("DccAdvDecoderModel[");
        sb.append("decoderUniqueId=").append(decoderUniqueId);
        sb.append(", decoderAddress=").append(decoderAddress).append("]");
        return sb.toString();
    }

    public boolean checkAllDataReceived(DccAOpCodeBm lastReceivedOpCode) {
        boolean allDataReceived = isInfoComplete(lastReceivedOpCode);

        // check the data of the other incompleteOpCodes before trigger
        if (allDataReceived && !hasIncompleteOpCodes()) {
            LOGGER.info("All data of the last expected bmOpCode was received: {}", lastReceivedOpCode);
            return allDataReceived;
        }
        LOGGER
            .info("Check all data received: {}, but return false because incomplete data was detected.",
                allDataReceived);
        return false;
    }

    private boolean isInfoComplete(DccAOpCodeBm infoOpCode) {
        boolean isInfoComplete = false;
        switch (infoOpCode) {
            case BIDIB_DCCA_INFO_FIRMWAREID:
                isInfoComplete = firmwareId.isDataComplete();
                break;
            case BIDIB_DCCA_INFO_PRODUCTNAME:
                isInfoComplete = productName.isDataComplete();
                break;
            case BIDIB_DCCA_INFO_SHORTNAME:
                isInfoComplete = shortName.isDataComplete();
                break;
            case BIDIB_DCCA_INFO_FULLNAME:
                isInfoComplete = fullName.isDataComplete();
                break;
            case BIDIB_DCCA_INFO_SHORTGUI:
                isInfoComplete = shortGui != null;
                break;
            case BIDIB_DCCA_INFO_SHORTINFO:
                isInfoComplete = shortInfo != null;
                break;
            default:
                break;
        }
        return isInfoComplete;
    }

    private static final DccAOpCodeBm[] REQUIRED_OPCODEBM =
        new DccAOpCodeBm[] { DccAOpCodeBm.BIDIB_DCCA_INFO_FIRMWAREID, DccAOpCodeBm.BIDIB_DCCA_INFO_FULLNAME,
            DccAOpCodeBm.BIDIB_DCCA_INFO_PRODUCTNAME, DccAOpCodeBm.BIDIB_DCCA_INFO_SHORTNAME,
            DccAOpCodeBm.BIDIB_DCCA_INFO_SHORTINFO, DccAOpCodeBm.BIDIB_DCCA_INFO_SHORTGUI };

    private List incompleteOpCodes = new ArrayList<>();

    public DccAOpCodeBm[] getIncompleteOpCodes() {
        LOGGER.info("The incomplete opCodes: {}", incompleteOpCodes);
        return incompleteOpCodes.toArray(new DccAOpCodeBm[0]);
    }

    private boolean hasIncompleteOpCodes() {
        for (DccAOpCodeBm opCodeBm : incompleteOpCodes) {
            if (!isInfoComplete(opCodeBm)) {
                LOGGER.info("Incomplete opCodeBm found: {}", opCodeBm);
                return true;
            }
        }
        return false;
    }

    public DccAOpCodeBm[] checkIncompleteOpCodes() {
        incompleteOpCodes.clear();
        for (DccAOpCodeBm opCodeBm : REQUIRED_OPCODEBM) {
            if (!isInfoComplete(opCodeBm)) {
                LOGGER.info("Incomplete opCodeBm found: {}", opCodeBm);
                incompleteOpCodes.add(opCodeBm);
            }
        }
        LOGGER.info("The info data is complete: {}", incompleteOpCodes.isEmpty());
        return incompleteOpCodes.toArray(new DccAOpCodeBm[0]);
    }

    /**
     * @return the checkDataCompleteLock
     */
    public Object getCheckDataCompleteLock() {
        return checkDataCompleteLock;
    }

    private DccAdvSelectInfoOpCode lastRequestedDccAdvSelectInfoOpCode;

    private DccAOpCodeBm lastExpectedReceiveBmOpCode;

    public void setLastRequestedInfoOpCode(DccAdvSelectInfoOpCode dccAdvSelectInfoOpCode) {
        LOGGER.info("Set the last requested info opCode: {}", dccAdvSelectInfoOpCode);
        this.lastRequestedDccAdvSelectInfoOpCode = dccAdvSelectInfoOpCode;

        switch (dccAdvSelectInfoOpCode) {
            case BIDIB_DCCA_SPACE_FULLNAME:
                lastExpectedReceiveBmOpCode = DccAOpCodeBm.BIDIB_DCCA_INFO_FULLNAME;
                break;
            case BIDIB_DCCA_SPACE_FIRMWARE_ID:
                lastExpectedReceiveBmOpCode = DccAOpCodeBm.BIDIB_DCCA_INFO_FIRMWAREID;
                break;
            case BIDIB_DCCA_SPACE_PRODUCTNAME:
                lastExpectedReceiveBmOpCode = DccAOpCodeBm.BIDIB_DCCA_INFO_PRODUCTNAME;
                break;
            case BIDIB_DCCA_SPACE_SHORTGUI:
                lastExpectedReceiveBmOpCode = DccAOpCodeBm.BIDIB_DCCA_INFO_SHORTGUI;
                break;
            case BIDIB_DCCA_SPACE_SHORTINFO:
                lastExpectedReceiveBmOpCode = DccAOpCodeBm.BIDIB_DCCA_INFO_SHORTINFO;
                break;
            case BIDIB_DCCA_SPACE_SHORTNAME:
                lastExpectedReceiveBmOpCode = DccAOpCodeBm.BIDIB_DCCA_INFO_SHORTNAME;
                break;
            default:
                LOGGER.warn("Unhandled infoOpCode in setLastRequestedInfoOpCode: {}", dccAdvSelectInfoOpCode);
                break;
        }

        LOGGER.info("Current lastExpectedReceiveBmOpCode: {}", lastExpectedReceiveBmOpCode);
    }

    public DccAOpCodeBm getLastExpectedReceiveBmOpCode() {
        return lastExpectedReceiveBmOpCode;
    }

    public DccAdvSelectInfoOpCode getLastRequestedInfoOpCode() {
        return lastRequestedDccAdvSelectInfoOpCode;
    }

    private int assignRetryCounter;

    public int getAssignRetryCounter() {
        return assignRetryCounter;
    }

    public void incAssignRetryCounter() {
        assignRetryCounter++;
        LOGGER.info("Incremented the assignRetryCounter: {}", assignRetryCounter);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy