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}