001// Copyright (c) FIRST and other WPILib contributors.
002// Open Source Software; you can modify and/or share it under the terms of
003// the WPILib BSD license file in the root directory of this project.
004
005package edu.wpi.first.epilogue.logging;
006
007import static edu.wpi.first.util.ErrorMessages.requireNonNullParam;
008
009import edu.wpi.first.datalog.BooleanArrayLogEntry;
010import edu.wpi.first.datalog.BooleanLogEntry;
011import edu.wpi.first.datalog.DataLog;
012import edu.wpi.first.datalog.DataLogEntry;
013import edu.wpi.first.datalog.DoubleArrayLogEntry;
014import edu.wpi.first.datalog.DoubleLogEntry;
015import edu.wpi.first.datalog.FloatArrayLogEntry;
016import edu.wpi.first.datalog.FloatLogEntry;
017import edu.wpi.first.datalog.IntegerArrayLogEntry;
018import edu.wpi.first.datalog.IntegerLogEntry;
019import edu.wpi.first.datalog.ProtobufLogEntry;
020import edu.wpi.first.datalog.RawLogEntry;
021import edu.wpi.first.datalog.StringArrayLogEntry;
022import edu.wpi.first.datalog.StringLogEntry;
023import edu.wpi.first.datalog.StructArrayLogEntry;
024import edu.wpi.first.datalog.StructLogEntry;
025import edu.wpi.first.util.protobuf.Protobuf;
026import edu.wpi.first.util.struct.Struct;
027import java.util.HashMap;
028import java.util.HashSet;
029import java.util.Map;
030import java.util.Set;
031import java.util.function.BiFunction;
032import us.hebi.quickbuf.ProtoMessage;
033
034/** A backend implementation that saves information to a WPILib {@link DataLog} file on disk. */
035public class FileBackend implements EpilogueBackend {
036  private final DataLog m_dataLog;
037  private final Map<String, DataLogEntry> m_entries = new HashMap<>();
038  private final Map<String, NestedBackend> m_subLoggers = new HashMap<>();
039  private final Set<Struct<?>> m_seenSchemas = new HashSet<>();
040  private final Set<Protobuf<?, ?>> m_seenProtos = new HashSet<>();
041
042  /**
043   * Creates a new file-based backend.
044   *
045   * @param dataLog the data log to save data to
046   */
047  public FileBackend(DataLog dataLog) {
048    this.m_dataLog = requireNonNullParam(dataLog, "dataLog", "FileBackend");
049  }
050
051  @Override
052  public EpilogueBackend getNested(String path) {
053    if (!m_subLoggers.containsKey(path)) {
054      var nested = new NestedBackend(path, this);
055      m_subLoggers.put(path, nested);
056      return nested;
057    }
058
059    return m_subLoggers.get(path);
060  }
061
062  @SuppressWarnings("unchecked")
063  private <E extends DataLogEntry> E getEntry(
064      String identifier, BiFunction<DataLog, String, ? extends E> ctor) {
065    if (m_entries.get(identifier) != null) {
066      return (E) m_entries.get(identifier);
067    }
068
069    var entry = ctor.apply(m_dataLog, identifier);
070    m_entries.put(identifier, entry);
071    return entry;
072  }
073
074  @Override
075  public void log(String identifier, int value) {
076    getEntry(identifier, IntegerLogEntry::new).append(value);
077  }
078
079  @Override
080  public void log(String identifier, long value) {
081    getEntry(identifier, IntegerLogEntry::new).append(value);
082  }
083
084  @Override
085  public void log(String identifier, float value) {
086    getEntry(identifier, FloatLogEntry::new).append(value);
087  }
088
089  @Override
090  public void log(String identifier, double value) {
091    getEntry(identifier, DoubleLogEntry::new).append(value);
092  }
093
094  @Override
095  public void log(String identifier, boolean value) {
096    getEntry(identifier, BooleanLogEntry::new).append(value);
097  }
098
099  @Override
100  public void log(String identifier, byte[] value) {
101    getEntry(identifier, RawLogEntry::new).append(value);
102  }
103
104  @Override
105  @SuppressWarnings("PMD.UnnecessaryCastRule")
106  public void log(String identifier, int[] value) {
107    long[] widened = new long[value.length];
108    for (int i = 0; i < value.length; i++) {
109      widened[i] = (long) value[i];
110    }
111    getEntry(identifier, IntegerArrayLogEntry::new).append(widened);
112  }
113
114  @Override
115  public void log(String identifier, long[] value) {
116    getEntry(identifier, IntegerArrayLogEntry::new).append(value);
117  }
118
119  @Override
120  public void log(String identifier, float[] value) {
121    getEntry(identifier, FloatArrayLogEntry::new).append(value);
122  }
123
124  @Override
125  public void log(String identifier, double[] value) {
126    getEntry(identifier, DoubleArrayLogEntry::new).append(value);
127  }
128
129  @Override
130  public void log(String identifier, boolean[] value) {
131    getEntry(identifier, BooleanArrayLogEntry::new).append(value);
132  }
133
134  @Override
135  public void log(String identifier, String value) {
136    getEntry(identifier, StringLogEntry::new).append(value);
137  }
138
139  @Override
140  public void log(String identifier, String[] value) {
141    getEntry(identifier, StringArrayLogEntry::new).append(value);
142  }
143
144  @Override
145  @SuppressWarnings("unchecked")
146  public <S> void log(String identifier, S value, Struct<S> struct) {
147    // DataLog.addSchema has checks that we're able to skip, avoiding allocations
148    if (m_seenSchemas.add(struct)) {
149      m_dataLog.addSchema(struct);
150    }
151
152    if (!m_entries.containsKey(identifier)) {
153      m_entries.put(identifier, StructLogEntry.create(m_dataLog, identifier, struct));
154    }
155
156    ((StructLogEntry<S>) m_entries.get(identifier)).append(value);
157  }
158
159  @Override
160  @SuppressWarnings("unchecked")
161  public <S> void log(String identifier, S[] value, Struct<S> struct) {
162    // DataLog.addSchema has checks that we're able to skip, avoiding allocations
163    if (m_seenSchemas.add(struct)) {
164      m_dataLog.addSchema(struct);
165    }
166
167    if (!m_entries.containsKey(identifier)) {
168      m_entries.put(identifier, StructArrayLogEntry.create(m_dataLog, identifier, struct));
169    }
170
171    ((StructArrayLogEntry<S>) m_entries.get(identifier)).append(value);
172  }
173
174  @Override
175  @SuppressWarnings("unchecked")
176  public <P, M extends ProtoMessage<M>> void log(String identifier, P value, Protobuf<P, M> proto) {
177    // DataLog.addSchema has checks that we're able to skip, avoiding allocations
178    if (m_seenProtos.add(proto)) {
179      m_dataLog.addSchema(proto);
180    }
181
182    if (!m_entries.containsKey(identifier)) {
183      m_entries.put(identifier, ProtobufLogEntry.create(m_dataLog, identifier, proto));
184    }
185
186    ((ProtobufLogEntry<P>) m_entries.get(identifier)).append(value);
187  }
188}