9 Commits

Author SHA1 Message Date
Daniel Carta b41ed2667c Testing 9/18 2025-09-18 17:51:05 -06:00
Daniel Carta 5279c085e1 Match Scouting Indicator Overhaul
Redo of the UI for the scouting indicator and overall enhancement of the UI
2025-06-17 11:15:36 -06:00
Michael Mikovsky 41460fcd7e Use android log. Fixes #5 2025-05-27 09:29:57 -06:00
Michael Mikovsky 782fb73050 Delete TODO.md 2025-05-26 11:18:01 -06:00
Michael Mikovsky 7e9954d78a Merge pull request #6 from Team4388/python-file-transfer
Add HTTP file transfer
2025-05-26 17:07:25 +00:00
Michael Mikovsky 65baecac35 Fix crash 2025-05-26 10:13:28 -06:00
Michael Mikovsky e278bc10a1 Add java side of http syncing 2025-05-25 18:48:02 -06:00
Michael Mikovsky 5d727cf359 Add upload and download to python server 2025-05-25 13:51:45 -06:00
Michael Mikovsky ae147771cb Start working on the server 2025-05-25 13:44:37 -06:00
34 changed files with 1606 additions and 165 deletions
+6
View File
@@ -1,3 +1,9 @@
# Python server
__pycache__/
metadata.json
api_key.txt
server_data/
# Gradle files
.gradle/
build/
-31
View File
@@ -1,31 +0,0 @@
### TODO:
##### Scouting:
- Make an easier way to make game-specific UI elements
##### Data Analysis:
- Add analysis for the rest of the data types besides Tally
- Add a "scout note" system for scouters to contribute to the scouting report?
- Make data that has been marked for rescouting not processed in either by team or by type analysis.
##### Functionality:
- Rewrite FTP transfer to be over HTTP requests
- - Potentially using a python server or some other pre-existing file transfer over HTTP
- Delete file menu
- Make "Sync meta files" option only block uploading fields specifically.
##### UI:
- Update docs and README.md with new features
- Improve file status indicator for scouting
- - The autosave timeout can be significantly reduced, so the save indicator is not a necessity
- - A new system for the rescout indicator should be used.
- Improve UI elements for scouting data by team
- Field button in settings overlaps with other elements in some devices
### In Progress:
##### Scouting:
##### Data Analysis:
##### Functionality:
##### UI:
### Done:
##### Scouting:
##### Data Analysis:
##### Functionality:
##### UI:
@@ -1,5 +1,7 @@
package com.ridgebotics.ridgescout.scoutingData;
import android.util.Log;
import com.ridgebotics.ridgescout.scoutingData.transfer.TransferType;
import com.ridgebotics.ridgescout.types.ScoutingArray;
import com.ridgebotics.ridgescout.types.data.RawDataType;
@@ -31,15 +33,15 @@ public class ScoutingDataWriter {
switch (data[i].getValueType()){
case NUM:
bb.addInt((int) data[i].forceGetValue());
System.out.println("Saved INT: " + data[i].getUUID() + ", ("+ data[i].get() +")");
Log.i(ScoutingDataWriter.class.toString(),"Saved INT: " + data[i].getUUID() + ", ("+ data[i].get() +")");
break;
case STRING:
bb.addString((String) data[i].forceGetValue());
System.out.println("Saved STR: " + data[i].getUUID() + ", ("+ data[i].get() +")");
Log.i(ScoutingDataWriter.class.toString(), "Saved STR: " + data[i].getUUID() + ", ("+ data[i].get() +")");
break;
case NUMARR:
bb.addIntArray((int[]) data[i].forceGetValue());
System.out.println("Saved INT Array: " + data[i].getUUID() + ", ("+ Arrays.toString((int[]) data[i].get()) +")");
Log.i(ScoutingDataWriter.class.toString(), "Saved INT Array: " + data[i].getUUID() + ", ("+ Arrays.toString((int[]) data[i].get()) +")");
}
}
byte[] bytes = bb.build();
@@ -82,17 +84,17 @@ public class ScoutingDataWriter {
case 1: // Int
rawDataTypes[i] = IntType.newNull(values[version][i].UUID);
rawDataTypes[i].forceSetValue(objects.get(i+2).get());
System.out.println("Loaded INT: " + values[version][i].name + " (" + values[version][i].UUID + ") " + ", ("+ rawDataTypes[i].get() +")");
Log.i(ParsedScoutingDataResult.class.toString(),"Loaded INT: " + values[version][i].name + " (" + values[version][i].UUID + ") " + ", ("+ rawDataTypes[i].get() +")");
break;
case 2: // String
rawDataTypes[i] = StringType.newNull(values[version][i].UUID);
rawDataTypes[i].forceSetValue(objects.get(i+2).get());
System.out.println("Loaded STR: " + values[version][i].name + " (" + values[version][i].UUID + ") " + ", ("+ rawDataTypes[i].get() +")");
Log.i(ParsedScoutingDataResult.class.toString(),"Loaded STR: " + values[version][i].name + " (" + values[version][i].UUID + ") " + ", ("+ rawDataTypes[i].get() +")");
break;
case 3: // Int array
rawDataTypes[i] = IntArrType.newNull(values[version][i].UUID);
rawDataTypes[i].forceSetValue(objects.get(i+2).get());
System.out.println("Loaded intARR: " + values[version][i].name + " (" + values[version][i].UUID + ") " + ", ("+ Arrays.toString((int[]) rawDataTypes[i].get()) +")");
Log.i(ParsedScoutingDataResult.class.toString(),"Loaded intARR: " + values[version][i].name + " (" + values[version][i].UUID + ") " + ", ("+ Arrays.toString((int[]) rawDataTypes[i].get()) +")");
break;
}
}
@@ -1,5 +1,7 @@
package com.ridgebotics.ridgescout.types;
import android.util.Log;
import com.ridgebotics.ridgescout.scoutingData.transfer.CreateTransferType;
import com.ridgebotics.ridgescout.scoutingData.transfer.DirectTransferType;
import com.ridgebotics.ridgescout.scoutingData.transfer.TransferType;
@@ -44,7 +46,7 @@ public class ScoutingArray {
}
this.array = new_values;
version++;
System.out.println("Updated to " + version);
Log.i(getClass().toString(),"Updated to " + version);
}
}
@@ -24,6 +24,7 @@ public abstract class FieldType {
public static final int numberType = 251;
public static final int checkboxType = 250;
public static final int fieldposType = 249;
public static final int toggleType = 248;
public enum inputTypes {
SLIDER,
@@ -32,7 +33,8 @@ public abstract class FieldType {
TALLY,
NUMBER,
CHECKBOX,
FIELDPOS
FIELDPOS,
TOGGLE;
}
public String UUID;
public String name;
@@ -89,8 +91,6 @@ public abstract class FieldType {
public abstract void setViewValue(Object value);
public abstract RawDataType getViewValue();
public abstract void add_individual_view(LinearLayout parent, RawDataType data);
public abstract void add_compiled_view(LinearLayout parent, RawDataType[] data);
public abstract void add_history_view(LinearLayout parent, RawDataType[] data);
@@ -65,6 +65,7 @@ public class TallyType extends FieldType {
public TallyCounterView tally = null;
public View createView(Context context, Function<RawDataType, Integer> onUpdate){
tally = new TallyCounterView(context);
tally.setOnCountChangedListener(n -> onUpdate.apply(getViewValue()));
@@ -0,0 +1,368 @@
package com.ridgebotics.ridgescout.types.input;
import static com.ridgebotics.ridgescout.utility.Colors.chart_background;
import static com.ridgebotics.ridgescout.utility.Colors.chart_text;
import static com.ridgebotics.ridgescout.utility.Colors.dropdown_value_text_1;
import static com.ridgebotics.ridgescout.utility.Colors.dropdown_value_text_2;
import static com.ridgebotics.ridgescout.utility.Colors.tally_data;
import android.content.Context;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TableLayout;
import android.widget.TableRow;
import android.widget.TextView;
import com.github.mikephil.charting.charts.LineChart;
import com.github.mikephil.charting.components.Legend;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.data.LineData;
import com.github.mikephil.charting.data.LineDataSet;
import com.ridgebotics.ridgescout.types.data.IntType;
import com.ridgebotics.ridgescout.types.data.RawDataType;
import com.ridgebotics.ridgescout.ui.data.DataProcessing;
import com.ridgebotics.ridgescout.ui.views.CandlestickHeader;
import com.ridgebotics.ridgescout.ui.views.CandlestickView;
import com.ridgebotics.ridgescout.ui.views.TallyCounterView;
import com.ridgebotics.ridgescout.utility.BuiltByteParser;
import com.ridgebotics.ridgescout.utility.ByteBuilder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
public class ToggleType extends FieldType {
public int get_byte_id() {return toggleType;}
public inputTypes getInputType(){return inputTypes.TOGGLE;}
public RawDataType.valueTypes getValueType(){return RawDataType.valueTypes.NUM;}
public Object get_fallback_value(){return 0;}
public ToggleType(){}
public String get_type_name(){return "Toggle";}
public ToggleType(String UUID, String name, String description, int default_value){
super(UUID, name, description);
this.default_value = default_value;
}
public void encodeData(ByteBuilder bb) throws ByteBuilder.buildingException {
bb.addInt((int) default_value);
}
public void decodeData(ArrayList<BuiltByteParser.parsedObject> objects) {
default_value = objects.get(0).get();
}
public TallyCounterView tally = null;
public View createView(Context context, Function<RawDataType, Integer> onUpdate){
tally = new TallyCounterView(context);
tally.setOnCountChangedListener(n -> onUpdate.apply(getViewValue()));
setViewValue(default_value);
return tally;
}
public void setViewValue(Object value) {
if(tally == null) return;
if(IntType.isNull((int)value)){
nullify();
return;
}
isBlank = false;
tally.setVisibility(View.VISIBLE);
tally.setValue((int) value);
}
public void nullify(){
isBlank = true;
tally.setVisibility(View.GONE);
}
public RawDataType getViewValue(){
if(tally == null) return null;
if(tally.getVisibility() == View.GONE) return IntType.newNull(name);
return new IntType(name, tally.getValue());
}
public void add_individual_view(LinearLayout parent, RawDataType data){
if(data.isNull()) return;
TextView tv = new TextView(parent.getContext());
tv.setLayoutParams(new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
));
tv.setGravity(Gravity.CENTER_HORIZONTAL);
tv.setText(String.valueOf((int) data.get()));
tv.setTextSize(24);
parent.addView(tv);
}
private static float calculateMean(int[] data) {
float sum = 0;
for (int value : data) {
sum += (float) value;
}
return sum / data.length;
}
private static float calculateStandardDeviation(int[] data, float mean) {
float sum = 0;
for (int value : data) {
sum += (float) Math.pow((float) value - mean, 2);
}
return (float) Math.sqrt(sum / (data.length - 1));
}
private static List<Entry> generateNormalDistribution(float mean, float stdDev, int count, int scale) {
List<Entry> entries = new ArrayList<>();
for (int i = 0; i < count; i++) {
float y = (float) ((1 / (stdDev * Math.sqrt(2 * Math.PI)))
* Math.exp(-0.5 * Math.pow(((float) i - mean) / stdDev, 2)));
entries.add(new Entry((float) i, y*scale)); // Scale y for visibility
}
return entries;
}
private static int findMin(RawDataType[] data){
int min = (int)data[0].get();
for(int i = 1; i < data.length; i++)
if((int)data[i].get() < min)
min = (int)data[i].get();
return min;
}
private static int findMax(RawDataType[] data){
int max = (int)data[0].get();
for(int i = 1; i < data.length; i++)
if((int)data[i].get() > max)
max = (int)data[i].get();
return max;
}
public void add_compiled_view(LinearLayout parent, RawDataType[] data){
LineChart chart = new LineChart(parent.getContext());
FrameLayout.LayoutParams layout = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
);
layout.height = 350;
chart.setLayoutParams(layout);
chart.setBackgroundColor(0xff252025);
int min = findMin(data);
int max = findMax(data);
int[] values = new int[max-min+1];
for (int i = 0; i < data.length; i++)
if(data[i] != null && data[i].isNull())
values[(int) data[i].get()-min]++;
ArrayList<Integer> mean_temp = new ArrayList<>();
for (int i = 0; i < data.length; i++)
if((int)data[i].get() != 0)
mean_temp.add((int) data[i].get());
int[] mean_vals = mean_temp.stream().mapToInt(Integer::intValue).toArray();
List<Entry> entries = new ArrayList<>();
for (int i = 0; i < values.length; i++)
entries.add(new Entry(i, values[i]));
LineDataSet dataSet = new LineDataSet(entries, name);
dataSet.setColor(tally_data);
dataSet.setValueTextColor(dropdown_value_text_1);
dataSet.setDrawCircles(false);
dataSet.setDrawValues(false);
// Calculate mean and standard deviation
float mean = calculateMean(mean_vals);
float stdDev = calculateStandardDeviation(mean_vals, mean);
// Generate normal distribution curve
List<Entry> normalDistEntries = generateNormalDistribution(mean-min, stdDev, max-min+1, (max-min)/data.length);
LineDataSet normalDistSet = new LineDataSet(normalDistEntries, "Normal Distribution");
normalDistSet.setColor(dropdown_value_text_2);
normalDistSet.setDrawCircles(false);
normalDistSet.setDrawValues(false);
normalDistSet.setLineWidth(2f);
LineData lineData = new LineData(dataSet, normalDistSet);
chart.setData(lineData);
chart.invalidate();
chart.getDescription().setEnabled(false);
chart.setTouchEnabled(false);
chart.setDragEnabled(false);
chart.setScaleEnabled(false);
dataSet.setValueTextColor(dropdown_value_text_2);
chart.getXAxis().setTextColor(chart_text);
chart.getAxisLeft().setTextColor(chart_text);
chart.getAxisRight().setTextColor(chart_text);
Legend legend = chart.getLegend();
legend.setTextColor(chart_text);
parent.addView(chart);
}
public void add_history_view(LinearLayout parent, RawDataType[] data){
LineChart chart = new LineChart(parent.getContext());
FrameLayout.LayoutParams layout = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
);
layout.height = 350;
chart.setLayoutParams(layout);
chart.setBackgroundColor(chart_background);
int min = findMin(data);
int max = findMax(data);
List<Entry> entries = new ArrayList<>();
for (int i = 0; i < data.length; i++){
if(data[i] == null) continue;
if(data[i].isNull()) continue;
entries.add(new Entry(i, (float)(int) data[i].get()));
}
LineDataSet dataSet = new LineDataSet(entries, name);
dataSet.setColor(tally_data);
dataSet.setValueTextColor(dropdown_value_text_1);
dataSet.setDrawCircles(false);
dataSet.setDrawValues(false);
LineData lineData = new LineData(dataSet);
chart.setData(lineData);
chart.invalidate();
chart.getDescription().setEnabled(false);
chart.setTouchEnabled(false);
chart.setDragEnabled(false);
chart.setScaleEnabled(false);
dataSet.setValueTextColor(dropdown_value_text_2);
chart.getXAxis().setTextColor(chart_text);
chart.getAxisLeft().setTextColor(chart_text);
chart.getAxisRight().setTextColor(chart_text);
Legend legend = chart.getLegend();
legend.setTextColor(chart_text);
chart.getAxisLeft().setAxisMinimum(min);
chart.getAxisLeft().setAxisMaximum(max);
chart.getAxisRight().setAxisMinimum(min);
chart.getAxisRight().setAxisMaximum(max);
parent.addView(chart);
}
public void addDataToTable(TableLayout parent, Map<Integer, List<RawDataType>> data){
int[] tmp_abs_bounds = DataProcessing.getNumberBounds(data);
int absmin = tmp_abs_bounds[0];
int absmax = tmp_abs_bounds[1];
//(int[]) teamData.get(i).get())[0];
// AlertManager.alert("Results","Min: " + min + " Max: " + max);
parent.removeAllViews();
List<CandlestickView> views = new ArrayList<>();
for(Integer teamNum : data.keySet()){
CandlestickView candlestickView = new CandlestickView(parent.getContext());
candlestickView.fromTeamData(data.get(teamNum), teamNum, absmin, absmax);
views.add(candlestickView);
}
TableRow row = new TableRow(parent.getContext());
// Make candlestick chart fill full width
parent.setColumnStretchable(1, true);
// Fill in top left cell
row.addView(new View(parent.getContext()));
CandlestickHeader header = new CandlestickHeader(parent.getContext());
header.setScale(absmin, absmax);
row.addView(header);
parent.addView(row);
// parent.addView(new );
try {
Collections.sort(views, (a, b) -> (int) ((b.average - a.average) * 50.f));
}catch (Exception e){}
for(int i = 0; i < views.size(); i++){
row = new TableRow(parent.getContext());
CandlestickView view = views.get(i);
TextView teamNum = new TextView(parent.getContext());
TableRow.LayoutParams params = new TableRow.LayoutParams();
params.gravity = Gravity.CENTER;
teamNum.setLayoutParams(params);
teamNum.setPadding(10,10,10,10);
teamNum.setTextAppearance(com.google.android.material.R.style.TextAppearance_MaterialComponents_Headline6);
teamNum.setText(String.valueOf(view.teamNum));
row.addView(teamNum);
row.addView(view);
parent.addView(row);
}
}
public String toString(RawDataType data){
return String.valueOf((int) data.get());
}
}
@@ -10,6 +10,7 @@ import static com.ridgebotics.ridgescout.utility.DataManager.rescout_list;
import android.app.ProgressDialog;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -63,7 +64,7 @@ public class FieldDataFragment extends Fragment {
for (int i = 0; i < filenames.size(); i++) {
try {
System.out.println("Loading: " + filenames.get(i));
Log.i(getClass().toString(), "Loading: " + filenames.get(i));
ScoutingDataWriter.ParsedScoutingDataResult psda = ScoutingDataWriter.load(filenames.get(i), match_values, match_transferValues);
if (psda.data.array[fieldIndex] != null && psda.data.array[fieldIndex].get() != null && !psda.data.array[fieldIndex].isNull())
teamData.add(psda.data.array[fieldIndex]);
@@ -75,7 +76,7 @@ public class FieldDataFragment extends Fragment {
data.put(teamNum, teamData);
}
System.out.println("Finished!");
Log.i(getClass().toString(), "Finished!");
@@ -8,6 +8,7 @@ import static com.ridgebotics.ridgescout.utility.DataManager.event;
import android.app.AlertDialog;
import android.os.Bundle;
import android.text.InputType;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -276,7 +277,7 @@ public class EventFragment extends Fragment {
builder.setPositiveButton("OK", (dialogInterface, i) -> {
int index = dropdown.getIndex();
System.out.println(index);
Log.i(getClass().toString(), String.valueOf(index));
if(!(index >= 0 && index < teamNums.size())) return;
event.teams.remove(index);
@@ -9,6 +9,7 @@ import static com.ridgebotics.ridgescout.utility.DataManager.event;
import android.annotation.SuppressLint;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -48,8 +49,9 @@ public class MatchScoutingFragment extends Fragment {
alliance_position = SettingsManager.getAllyPos();
username = SettingsManager.getUsername();
binding.username.setText(username);
binding.alliancePosText.setText(alliance_position);
binding.bindicator.setUsername(username);
binding.bindicator.setAlliancePos(alliance_position);
binding.bindicator.bringToFront();
binding.matchTeamCard.setVisibility(View.GONE);
clear_fields();
@@ -65,7 +67,7 @@ public class MatchScoutingFragment extends Fragment {
binding.nextButton.setOnClickListener(v -> {
binding.bindicator.match_indicator_next_button.setOnClickListener(v -> {
if(edited) save();
SettingsManager.setMatchNum(cur_match_num+1);
cur_match_num += 1;
@@ -74,21 +76,21 @@ public class MatchScoutingFragment extends Fragment {
});
if(SettingsManager.getEnableQuickAlliancePosChange())
binding.fileIndicator.setOnClickListener(v -> {
binding.bindicator.setOnClickListener(v -> {
// if(e.getAction() != MotionEvent.ACTION_MOVE) return true;
// System.out.println(e.getAxisValue(0));
if(edited) save();
alliance_position = incrementMatchPos(alliance_position);
SettingsManager.setAllyPos(alliance_position);
binding.alliancePosText.setText(alliance_position);
binding.bindicator.setAlliancePos(alliance_position);
update_match_num();
update_scouting_data();
// return true;
});
binding.backButton.setOnClickListener(v -> {
binding.bindicator.match_indicator_back_button.setOnClickListener(v -> {
if(edited) save();
SettingsManager.setMatchNum(cur_match_num-1);
cur_match_num -= 1;
@@ -150,7 +152,7 @@ public class MatchScoutingFragment extends Fragment {
public void save(){
System.out.println("Saved!");
Log.i(this.getClass().toString(), "Saved!");
edited = false;
enableRescoutButton();
AlertManager.toast("Saved " + filename);
@@ -158,7 +160,7 @@ public class MatchScoutingFragment extends Fragment {
}
public void set_indicator_color(int color){
binding.fileIndicator.setBackgroundColor(color);
binding.bindicator.setColor(color);
}
public void update_asm(){
@@ -238,18 +240,18 @@ public class MatchScoutingFragment extends Fragment {
edited = false;
binding.matchnum.setText(String.valueOf(cur_match_num+1));
binding.bindicator.setMatchNum(String.valueOf(cur_match_num+1));
if(cur_match_num <= 0){
binding.backButton.setVisibility(View.GONE);
binding.bindicator.match_indicator_back_button.setVisibility(View.GONE);
}else{
binding.backButton.setVisibility(View.VISIBLE);
binding.bindicator.match_indicator_back_button.setVisibility(View.VISIBLE);
}
if(cur_match_num >= event.matches.size()-1){
binding.nextButton.setVisibility(View.GONE);
binding.bindicator.match_indicator_next_button.setVisibility(View.GONE);
}else{
binding.nextButton.setVisibility(View.VISIBLE);
binding.bindicator.match_indicator_next_button.setVisibility(View.VISIBLE);
}
}
@@ -271,7 +273,7 @@ public class MatchScoutingFragment extends Fragment {
break;
}
binding.barTeamNum.setText(String.valueOf(team_num));
binding.bindicator.setTeamNum(String.valueOf(team_num));
frcTeam team = null;
for(int i=0; i < event.teams.size(); i++){
@@ -377,14 +379,14 @@ public class MatchScoutingFragment extends Fragment {
}
if(ScoutingDataWriter.save(DataManager.match_values.length-1, ScoutingDataWriter.checkAddName(fileUsernames, username), filename, types))
System.out.println("Saved!");
Log.i(getClass().toString(), "Saved!");
else
System.out.println("Error saving");
Log.i(getClass().toString(), "Error saving");
}
private void enableRescoutButton(){
set_indicator_color(rescout ? rescout_color : saved_color);
binding.fileIndicator.setOnLongClickListener(v -> {
binding.bindicator.setOnLongClickListener(v -> {
rescout = !rescout;
if(rescout){
set_indicator_color(rescout_color);
@@ -401,6 +403,6 @@ public class MatchScoutingFragment extends Fragment {
}
private void disableRescoutButton(){
binding.fileIndicator.setOnLongClickListener(null);
binding.bindicator.setOnLongClickListener(null);
}
}
@@ -10,6 +10,7 @@ import static com.ridgebotics.ridgescout.utility.DataManager.pit_transferValues;
import static com.ridgebotics.ridgescout.utility.DataManager.pit_values;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -91,10 +92,10 @@ public class PitScoutingFragment extends Fragment {
}
if(ScoutingDataWriter.save(pit_values.length-1, ScoutingDataWriter.checkAddName(fileUsernames, username), filename, types)) {
System.out.println("Saved!");
AlertManager.toast("Saved " + filename);
Log.i(getClass().toString(), "Saved!");
Log.i(getClass().toString(), "Saved " + filename);
}else
System.out.println("Error saving");
Log.i(getClass().toString(), "Error saving");
}
public void set_indicator_color(int color){
@@ -22,6 +22,7 @@ import androidx.fragment.app.Fragment;
import com.ridgebotics.ridgescout.R;
import com.ridgebotics.ridgescout.types.frcEvent;
import com.ridgebotics.ridgescout.utility.AlertManager;
import com.ridgebotics.ridgescout.utility.FileEditor;
import com.ridgebotics.ridgescout.utility.SettingsManager;
import com.ridgebotics.ridgescout.databinding.FragmentScoutingBinding;
@@ -126,7 +127,12 @@ public class ScoutingFragment extends Fragment {
binding.textName.setText("Welcome, " + SettingsManager.getUsername() + "!");
int matchNum = SettingsManager.getMatchNum();
int nextMatch = event.getNextTeamMatch(SettingsManager.getTeamNum(), matchNum).matchIndex;
int nextMatch = -1;
try {
nextMatch = event.getNextTeamMatch(SettingsManager.getTeamNum(), matchNum).matchIndex;
} catch (Exception e){
AlertManager.error(e);
}
binding.textNextMatch.setText("Our next match: Match " + nextMatch);
binding.textMatchAlliance.setText("Match: " + (matchNum+1) + ", " + SettingsManager.getAllyPos());
@@ -18,6 +18,7 @@ import com.ridgebotics.ridgescout.types.input.NumberType;
import com.ridgebotics.ridgescout.types.input.SliderType;
import com.ridgebotics.ridgescout.types.input.TallyType;
import com.ridgebotics.ridgescout.types.input.TextType;
import com.ridgebotics.ridgescout.types.input.ToggleType;
import com.ridgebotics.ridgescout.utility.AlertManager;
import java.lang.reflect.Field;
@@ -29,7 +30,8 @@ public class FieldEditorHelper {
paramNumber,
paramString,
paramStringArray,
paramNumberArray
paramNumberArray,
paramBoolean
}
public static class parameterType {
@@ -64,6 +66,15 @@ public class FieldEditorHelper {
}
}
public static class paramBoolean extends parameterType{
public boolean val;
public paramBoolean(String name, boolean val){
this.name = name + " (Boolean)";
this.val = val;
this.id = parameterTypeEnum.paramBoolean;
}
}
// public static class paramNumberArray extends parameterType{
// public int[] val;
// public paramNumberArray(String name, int[] val){
@@ -113,6 +124,12 @@ public class FieldEditorHelper {
new paramNumber("Default Y", 0)
};
public static final parameterType[] defaultToggleParam = new parameterType[]{
new paramString("Name", "New Toggle"),
new paramString("Description", ""),
new paramBoolean("Default true or false",true)
};
private static parameterType[] getSliderParams(SliderType s){
return new parameterType[]{
@@ -173,6 +190,13 @@ public class FieldEditorHelper {
new paramNumber("Default Y", ((int[]) s.default_value)[1])
};
}
private static parameterType[] getToggleParams(ToggleType s){
return new parameterType[]{
new paramString("Name", s.name),
new paramString("Description", s.description),
new paramBoolean("Default true or false", (boolean) s.default_value)
};
}
@@ -224,6 +248,12 @@ public class FieldEditorHelper {
};
}
public static void setToggleParam(ToggleType s, parameterType[] types){
s.name = ((paramString) types[0]).val;
s.description = ((paramString) types[1]).val;
s.default_value = ((paramBoolean) types[2]).val;
}
private static void setInputParameter(FieldType t, parameterType[] types){
switch (t.getInputType()){
@@ -248,6 +278,9 @@ public class FieldEditorHelper {
case FIELDPOS:
setFieldPosParam((FieldposType) t, types);
break;
case TOGGLE:
setToggleParam((ToggleType) t, types);
break;
}
}
@@ -269,6 +302,8 @@ public class FieldEditorHelper {
return getCheckboxParam((CheckboxType) t);
case FIELDPOS:
return getFieldPosParam((FieldposType) t);
case TOGGLE:
return getToggleParams((ToggleType) t);
}
return new parameterType[]{};
}
@@ -305,6 +340,16 @@ public class FieldEditorHelper {
));
return text;
}
private static View createBooleanEdit(Context c, boolean value){
EditText text = new EditText(c);
text.setText(value);
text.setLayoutParams(new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
));
return text;
}
private static View createEdit(Context c, parameterType t){
switch (t.id){
@@ -384,6 +429,11 @@ public class FieldEditorHelper {
fieldposType.UUID = UUID.randomUUID().toString();
setFieldPosParam(fieldposType, defaultFieldPosParam);
return fieldposType;
case 7:
ToggleType toggleType = new ToggleType();
toggleType.UUID = UUID.randomUUID().toString();
setToggleParam(toggleType, defaultToggleParam);
return toggleType;
}
return null;
}
@@ -7,6 +7,7 @@ import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -270,7 +271,7 @@ public class FieldsFragment extends Fragment {
System.arraycopy(currentValues, 0, newValues, 0, currentValues.length);
System.out.println("Length: " + values.size());
Log.i(getClass().toString(), "Length: " + values.size());
newValues[currentValues.length] = new FieldType[values.size()];
for(int i = 0; i < values.size(); i++) {
@@ -46,6 +46,7 @@ import com.ridgebotics.ridgescout.ui.views.CustomSpinnerView;
import com.ridgebotics.ridgescout.ui.views.TallyCounterView;
import com.ridgebotics.ridgescout.utility.DataManager;
import com.ridgebotics.ridgescout.utility.FileEditor;
import com.ridgebotics.ridgescout.utility.SettingsManager;
import java.util.ArrayList;
import java.util.Arrays;
@@ -92,11 +93,13 @@ public class SettingsFragment extends Fragment {
manager.addItem(new CheckboxSettingsItem(CustomEventsKey, "Custom Events"));
StringSettingsItem FTPServer = new StringSettingsItem(com.ridgebotics.ridgescout.utility.SettingsManager.FTPServer, "FTP Server (Sync)");
StringSettingsItem FTPKey = new StringSettingsItem(com.ridgebotics.ridgescout.utility.SettingsManager.FTPKey, "Sync Key");
manager.addItem(FTPKey);
StringSettingsItem FTPServer = new StringSettingsItem(com.ridgebotics.ridgescout.utility.SettingsManager.FTPServer, "Sync Server (Sync)");
manager.addItem(FTPServer);
CheckboxSettingsItem FTPSendMetaFiles = new CheckboxSettingsItem(com.ridgebotics.ridgescout.utility.SettingsManager.FTPSendMetaFiles, "Sync meta files");
CheckboxSettingsItem FTPSendMetaFiles = new CheckboxSettingsItem(com.ridgebotics.ridgescout.utility.SettingsManager.FTPSendMetaFiles, "⚠ Send meta files");
manager.addItem(FTPSendMetaFiles);
CheckboxSettingsItem FTPEnabled = new CheckboxSettingsItem(com.ridgebotics.ridgescout.utility.SettingsManager.FTPEnabled, "FTP Enabled", FTPServer, FTPSendMetaFiles);
CheckboxSettingsItem FTPEnabled = new CheckboxSettingsItem(com.ridgebotics.ridgescout.utility.SettingsManager.FTPEnabled, "FTP Enabled", FTPServer, FTPKey, FTPSendMetaFiles);
manager.addItem(FTPEnabled);
manager.addItem(new CheckboxSettingsItem(WifiModeKey, "Wifi Mode", FTPEnabled));
@@ -3,6 +3,8 @@ package com.ridgebotics.ridgescout.ui.transfer;
import static com.ridgebotics.ridgescout.utility.DataManager.evcode;
import static com.ridgebotics.ridgescout.utility.FileEditor.baseDir;
import android.util.Log;
import com.ridgebotics.ridgescout.utility.AlertManager;
import com.ridgebotics.ridgescout.utility.BuiltByteParser;
import com.ridgebotics.ridgescout.utility.ByteBuilder;
@@ -26,6 +28,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
// This is now deprecated
// Class to synchronise data over FTP.
public class FTPSync extends Thread {
public static final String remoteBasePath = "/RidgeScout/";
@@ -156,13 +159,13 @@ public class FTPSync extends Thread {
if (remoteTimestamp == null || after(localTimeStamp, remoteTimestamp)) {
uploadFile(localFile);
System.out.println("Uploaded" + localFile.getName());
Log.i(getClass().toString(), "Uploaded" + localFile.getName());
setLocalFileTimestamp(localFile, curSyncTime);
remoteTimestamps.put(localFile.getName(), curSyncTime);
upCount++;
}else{
System.out.println("Did not upload");
Log.i(getClass().toString(), "Did not upload");
}
}
}
@@ -191,17 +194,17 @@ public class FTPSync extends Thread {
if (!localFile.exists() || (after(remoteTimestamp, localTimeStamp) && !localTimeStamp.equals(remoteTimestamp))) {
downloadFile(remoteFile, localFile);
System.out.println("Downloaded " + localFile.getName());
Log.i(getClass().toString(), "Downloaded " + localFile.getName());
if(!localFile.exists()) System.out.println("Not exist");
else if(after(remoteTimestamp, localTimeStamp)) System.out.println("Before: " + (localTimeStamp.getTime()-remoteTimestamp.getTime()));
if(!localFile.exists()) Log.i(getClass().toString(), "Not exist");
else if(after(remoteTimestamp, localTimeStamp)) Log.i(getClass().toString(), "Before: " + (localTimeStamp.getTime()-remoteTimestamp.getTime()));
// Date d = getUtcTimestamp(remoteFile);
setLocalFileTimestamp(localFile, remoteTimestamps.get(localFile.getName()));
// remoteTimestamps.put(remoteFile, curSyncTime);
downCount++;
}else{
System.out.println("Did not download");
Log.i(getClass().toString(), "Did not download");
}
}
@@ -0,0 +1,369 @@
package com.ridgebotics.ridgescout.ui.transfer;
import static com.ridgebotics.ridgescout.utility.FileEditor.baseDir;
import android.util.Log;
import com.ridgebotics.ridgescout.utility.AlertManager;
import com.ridgebotics.ridgescout.utility.BuiltByteParser;
import com.ridgebotics.ridgescout.utility.ByteBuilder;
import com.ridgebotics.ridgescout.utility.FileEditor;
import com.ridgebotics.ridgescout.utility.HttpGetFile;
import com.ridgebotics.ridgescout.utility.HttpPutFile;
import com.ridgebotics.ridgescout.utility.RequestTask;
import com.ridgebotics.ridgescout.utility.SettingsManager;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
// This is now deprsicated
// Class to syncronise data over FTP.
public class HttpSync extends Thread {
public static final String timestampsFilename = "timestamps";
private static final long millisTolerance = 1000;
private boolean after(Date a, Date b){
return a.getTime() - b.getTime() > millisTolerance;
}
public interface onResult {
void onResult(boolean error, int upCount, int downCount);
}
public interface UpdateIndicator {
void onText(String text);
}
private static UpdateIndicator updateIndicator = text -> {};
public static String text = "";
private static void setUpdateIndicator(String m_text){
text = m_text;
updateIndicator.onText(m_text);
}
public static void setOnUpdateIndicator(UpdateIndicator m_updateIndicator){
updateIndicator = m_updateIndicator;
}
private static onResult onResult = (error, upCount, downCount) -> {};
public static void setOnResult(onResult result){
onResult = result;
}
private static boolean isRunning = false;
public static boolean getIsRunning(){return isRunning;}
public static void sync(){
// DataManager.reload_event();
HttpSync sync = new HttpSync();
sync.start();
}
private int upCount = 0;
private int downCount = 0;
private class TransferFile {
public String filename;
public Date updated;
public String checksum;
}
private List<TransferFile> localFiles = new ArrayList<>();
private List<TransferFile> remoteFiles = new ArrayList<>();
private void await() {
while(!runningRequest.get()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {}
}
}
AtomicBoolean runningRequest = new AtomicBoolean(false);
public void run() {
isRunning = true;
boolean sendMetaFiles = SettingsManager.getFTPSendMetaFiles();
String serverIP = SettingsManager.getFTPServer();
String serverKey = SettingsManager.getFTPKey();
setUpdateIndicator("Getting Metadata...");
// Load metadata from server
getRemoteFileMetadata(serverIP, serverKey);
if(!isRunning){
setUpdateIndicator("Error Connecting");
onResult.onResult(true, upCount, downCount);
return;
}
getLocalFileMetadata();
// Wait for metadata request to finish
setUpdateIndicator("Uploading 0%");
for(int i = 0; i < localFiles.size(); i++){
TransferFile localFile = localFiles.get(i);
TransferFile remoteFile = findInFileArray(remoteFiles, localFile.filename);
if(
(
sendMetaFiles || !(
localFile.filename.endsWith(".fields")
)
)
&& (remoteFile == null ||
(
!Objects.equals(localFile.checksum, remoteFile.checksum) &&
after(localFile.updated, remoteFile.updated)
)
)) {
uploadFile(localFile, serverIP, serverKey);
// await();
Log.d(getClass().toString(), "LocalFile: " + localFile.filename + ", " + localFile.checksum + ", " + localFile.updated + ": Uploaded");
upCount++;
}else {
Log.d(getClass().toString(), "LocalFile: " + localFile.filename + ", " + localFile.checksum + ", " + localFile.updated + ": Not uploaded");
}
setUpdateIndicator("Uploading " + (Math.floor((double) (i * 1000) / localFiles.size()) / 10) + "%");
}
setUpdateIndicator("Downloading 0%");
for(int i = 0; i < remoteFiles.size(); i++){
TransferFile remoteFile = remoteFiles.get(i);
TransferFile localFile = findInFileArray(localFiles, remoteFile.filename);
if(localFile == null ||
(
!Objects.equals(localFile.checksum, remoteFile.checksum) &&
after(remoteFile.updated, localFile.updated) &&
!localFile.updated.equals(remoteFile.updated)
)
) {
downloadFile(remoteFile, serverIP);
// await();
Log.d(getClass().toString(), "RemoteFile: " + remoteFile.filename + ", " + remoteFile.checksum + ", " + remoteFile.updated + ": Downloaded");
downCount++;
} else {
Log.d(getClass().toString(), "RemoteFile: " + remoteFile.filename + ", " + remoteFile.checksum + ", " + remoteFile.updated + ": Not downloaded");
}
setUpdateIndicator("Downloading " + (Math.floor((double) (i * 1000) / remoteFiles.size()) / 10) + "%");
}
setUpdateIndicator("Finished, " + upCount + " Up, " + downCount + " Down");
onResult.onResult(false, upCount, downCount);
isRunning = false;
}
private TransferFile findInFileArray(List<TransferFile> files, String filename){
for(TransferFile file : files) {
if(file.filename.equals(filename))
return file;
}
return null;
}
private Date getLocalFileUtcTimestamp(File file) {
return new Date(file.lastModified());
}
public static String getSHA256Hash(String filePath) throws IOException, NoSuchAlgorithmException {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
FileInputStream fis = new FileInputStream(filePath);
byte[] byteArray = new byte[1024];
int bytesCount = 0;
while ((bytesCount = fis.read(byteArray)) != -1) {
digest.update(byteArray, 0, bytesCount);
}
fis.close();
byte[] bytes = digest.digest();
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
private void getLocalFileMetadata() {
File localDir = new File(baseDir);
File[] localFileNames = localDir.listFiles();
assert localFileNames != null;
for (int i = 0; i < localFileNames.length; i++) {
File file = localFileNames[i];
if(file.isDirectory()) continue;
// Remove timestamts file
if(file.getName().equals(timestampsFilename)) continue;
TransferFile tf = new TransferFile();
tf.filename = file.getName();
tf.updated = getLocalFileUtcTimestamp(file);
try {
tf.checksum = getSHA256Hash(file.getPath());
} catch (Exception e) {
}
localFiles.add(tf);
}
}
// Send request to server and retrieve metadata
private void getRemoteFileMetadata(String serverURL, String serverKey) {
final RequestTask rq = new RequestTask();
runningRequest.set(false);
rq.onResult(metadata -> {
try {
JSONObject j = new JSONObject(metadata);
for (Iterator<String> it = j.keys(); it.hasNext(); ) {
String key = it.next();
JSONObject obj = j.getJSONObject(key);
TransferFile tf = new TransferFile();
tf.filename = key;
tf.updated = new Date(Long.parseLong(obj.getString("modified")));
tf.checksum = obj.getString("sha256");
remoteFiles.add(tf);
}
}catch(JSONException | NullPointerException e ) {
AlertManager.error(e);
isRunning = false;
}
runningRequest.set(true);
return null;
});
rq.execute((serverURL + "/api/metadata"), "api_key: " + serverKey);
await();
}
// private boolean setTimestamps(Map<String, Date> timestamps){
// try {
// ByteBuilder bb = new ByteBuilder();
// String[] filenames = timestamps.keySet().toArray(new String[0]);
//
// for(int i = 0; i < filenames.length; i++){
// bb.addString(filenames[i]);
// bb.addLong(timestamps.get(filenames[i]).getTime());
// }
//
// FileEditor.writeFile(timestampsFilename, bb.build());
//
// uploadFile(new File(baseDir + timestampsFilename));
// return true;
// } catch (ByteBuilder.buildingException | IOException e) {
// AlertManager.error("Failed Syncing!", e);
// return false;
// }
// }
//
// private Map<String, Date> getTimestamps() {
// try {
// downloadFile(timestampsFilename, new File(baseDir + timestampsFilename));
//
// byte[] data = FileEditor.readFile(timestampsFilename);
//
// if(data == null || data.length == 0)
// return new HashMap<>();
//
// BuiltByteParser bbp = new BuiltByteParser(data);
// List<BuiltByteParser.parsedObject> pa = bbp.parse();
//
// Map<String, Date> output = new HashMap<>();
// for(int i = 0; i < pa.size(); i+=2){
//// System.out.println((long) pa.get(i).get());
// output.put(
// (String) pa.get(i).get(),
// new Date((long) pa.get(i+1).get())
// );
// }
// return output;
//
// }catch (IOException | BuiltByteParser.byteParsingExeption e){
// AlertManager.error("Failed Syncing!", e);
// return new HashMap<>();
// }
// }
void uploadFile(TransferFile tf, String serverURL, String apiKey) {
runningRequest.set(false);
HttpPutFile uploadTask = new HttpPutFile(serverURL + "/api/" + tf.filename, new File(baseDir + tf.filename), new HttpPutFile.UploadCallback() {
@Override
public void onResult(String error) {
if(error != null)
AlertManager.error(error);
runningRequest.set(true);
}
}, new String[]{
"api_key: " + apiKey,
("modified: " + tf.updated.getTime())
}); // Pass auth token if needed
uploadTask.execute();
await();
}
private void setLocalFileTimestamp(File file, Date date) {
file.setLastModified(date.getTime());
}
void downloadFile(TransferFile tf, String serverURL) {
runningRequest.set(false);
File f = new File(baseDir + tf.filename);
HttpGetFile uploadTask = new HttpGetFile(serverURL + "/api/" + tf.filename, f, new HttpGetFile.DownloadCallback() {
@Override
public void onResult(String error) {
if(error != null)
AlertManager.error(error);
else
setLocalFileTimestamp(f, tf.updated);
runningRequest.set(true);
}
}); // Pass auth token if needed
uploadTask.execute();
await();
}
}
@@ -65,13 +65,13 @@ public class TransferFragment extends Fragment {
binding.SyncButton.setOnClickListener(v -> {
binding.SyncButton.setEnabled(false);
FTPSync.sync();
HttpSync.sync();
});
if(FTPSync.getIsRunning())
if(HttpSync.getIsRunning())
binding.SyncButton.setEnabled(false);
FTPSync.setOnResult((error, upcount, downcount) -> {
HttpSync.setOnResult((error, upcount, downcount) -> {
if (getActivity() != null)
getActivity().runOnUiThread(() -> {
binding.SyncButton.setEnabled(true);
@@ -79,8 +79,8 @@ public class TransferFragment extends Fragment {
});
});
binding.syncIndicator.setText(FTPSync.text);
FTPSync.setOnUpdateIndicator(text -> {if(getActivity() != null) getActivity().runOnUiThread(() -> binding.syncIndicator.setText(text));});
binding.syncIndicator.setText(HttpSync.text);
HttpSync.setOnUpdateIndicator(text -> {if(getActivity() != null) getActivity().runOnUiThread(() -> binding.syncIndicator.setText(text));});
if(evcode.equals("unset")){
binding.noEventError.setVisibility(View.VISIBLE);
@@ -8,6 +8,7 @@ import android.bluetooth.BluetoothSocket;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import android.util.Log;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
@@ -139,7 +140,7 @@ public class BluetoothReceiver {
} catch (IOException e) {
if (e.getMessage() != null && e.getMessage().contains("bt socket closed, read return: -1")) {
receiveddata.onConnectionStop();
System.out.println("Bluetooth socket closed, treating as end of stream");
Log.i(getClass().toString(), "Bluetooth socket closed, treating as end of stream");
} else {
throw e;
}
@@ -1,6 +1,7 @@
package com.ridgebotics.ridgescout.ui.transfer.bluetooth;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -117,7 +118,7 @@ public class BluetoothReceiverFragment extends Fragment {
private void receiveData(byte[] data, int bytes) {
byte[] newBytes = FileEditor.getByteBlock(data, 0, bytes);
System.out.println("Recieved " + bytes + " Bytes over bluetooth!");
Log.i(getClass().toString(), "Recieved " + bytes + " Bytes over bluetooth!");
recievedBytes.add(newBytes);
}
@@ -138,7 +139,7 @@ public class BluetoothReceiverFragment extends Fragment {
ScoutingFile f = ScoutingFile.decode((byte[]) result.get(i).get());
if (f != null) {
System.out.println(f.filename);
Log.i(getClass().toString(), f.filename);
if (f.write())
result_filenames += f.filename + "\n";
}
@@ -7,6 +7,7 @@ import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import com.ridgebotics.ridgescout.types.data.RawDataType;
@@ -208,7 +209,7 @@ public class CandlestickView extends View {
upperQuartile = DataProcessing.calculatePercentile(teamDataArray, 75);
}
System.out.println(locmin + ", " + lowerQuartile + ", " + avg + ", " + upperQuartile + ", " + locmax);
Log.i(getClass().toString(), locmin + ", " + lowerQuartile + ", " + avg + ", " + upperQuartile + ", " + locmax);
setData(locmin, lowerQuartile, avg, upperQuartile, locmax, absmin, absmax);
}
}
@@ -0,0 +1,91 @@
package com.ridgebotics.ridgescout.ui.views;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.ridgebotics.ridgescout.R;
import com.ridgebotics.ridgescout.types.frcTeam;
import org.w3c.dom.Text;
// A view for displaying information about a team.
public class MatchScoutingIndicator extends RelativeLayout {
public MatchScoutingIndicator(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
public MatchScoutingIndicator(Context context) {
super(context);
init(context);
}
public TextView match_indicator_alliance_pos_text;
public TextView match_indicator_bar_team_num;
public TextView match_indicator_matchnum;
public TextView match_indicator_username;
public ImageButton match_indicator_back_button;
public ImageButton match_indicator_next_button;
private ConstraintLayout box;
private View coloredBackground;
public void init(Context context) {
LayoutInflater.from(context).inflate(R.layout.view_match_scouting_indicator, this, true);
match_indicator_back_button = findViewById(R.id.match_indicator_back_button);
match_indicator_next_button = findViewById(R.id.match_indicator_next_button);
match_indicator_alliance_pos_text = findViewById(R.id.match_indicator_alliance_pos_text);
match_indicator_username = findViewById(R.id.match_indicator_username);
match_indicator_matchnum = findViewById(R.id.match_indicator_matchnum);
match_indicator_bar_team_num = findViewById(R.id.match_indicator_bar_team_num);
box = findViewById(R.id.file_indicator_box);
coloredBackground = findViewById(R.id.match_indicator_background);
}
public void setUsername(String username){
match_indicator_username.setText(username);
}
public void setAlliancePos(String alliancePos){
match_indicator_alliance_pos_text.setText(alliancePos);
}
public void setMatchNum(String matchNum){
match_indicator_matchnum.setText(matchNum);
}
public void setTeamNum(String teamNum){
match_indicator_bar_team_num.setText(teamNum);
}
public void setColor(int color){
Drawable drawable = box.getBackground();
drawable.mutate();
drawable.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
float[] hsv = new float[3];
Color.colorToHSV(color,hsv);
coloredBackground.setBackgroundColor(
Color.HSVToColor(220, new float[]{
hsv[0],
Math.min(hsv[1], 0.75f),
Math.min(hsv[2], 0.5f)
})
);
}
}
@@ -0,0 +1,168 @@
package com.ridgebotics.ridgescout.utility;
import android.os.AsyncTask;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
public class HttpGetFile extends AsyncTask<Void, Integer, File> {
public interface DownloadCallback {
void onResult(String error);
}
private String downloadUrl;
private File destinationFile;
private DownloadCallback callback;
private String errorMessage;
public HttpGetFile(String downloadUrl, File destinationFile, DownloadCallback callback) {
this.downloadUrl = downloadUrl;
this.destinationFile = destinationFile;
this.callback = callback;
}
@Override
protected File doInBackground(Void... voids) {
HttpURLConnection connection = null;
InputStream inputStream = null;
FileOutputStream outputStream = null;
try {
URL url = new URL(downloadUrl);
connection = (HttpURLConnection) url.openConnection();
// Configure connection for GET request
connection.setRequestMethod("GET");
connection.setDoInput(true);
connection.setConnectTimeout(30000); // 30 seconds
connection.setReadTimeout(60000); // 60 seconds
connection.connect();
// Check response code
int responseCode = connection.getResponseCode();
if (responseCode < 200 || responseCode >= 300) {
String errorResponse = readErrorResponse(connection);
errorMessage = "Download failed. Response code: " + responseCode +
(errorResponse != null ? ". Error: " + errorResponse : "");
return null;
}
// Get file size for progress tracking
long fileSize = connection.getContentLengthLong();
if (fileSize == -1) {
fileSize = connection.getContentLength(); // fallback for older API
}
inputStream = connection.getInputStream();
// Create destination file and directories if needed
if (destinationFile.getParentFile() != null && !destinationFile.getParentFile().exists()) {
if (!destinationFile.getParentFile().mkdirs()) {
errorMessage = "Failed to create destination directory: " + destinationFile.getParentFile().getAbsolutePath();
return null;
}
}
outputStream = new FileOutputStream(destinationFile);
byte[] buffer = new byte[8192];
long downloadedBytes = 0;
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
if (isCancelled()) {
deletePartialFile();
return null;
}
outputStream.write(buffer, 0, bytesRead);
downloadedBytes += bytesRead;
// Update progress if file size is known
if (fileSize > 0) {
int progress = (int) ((downloadedBytes * 100) / fileSize);
publishProgress(progress);
}
}
outputStream.flush();
// Log.d(TAG, "Download successful. File saved to: " + destinationFile.getAbsolutePath());
return destinationFile;
} catch (Exception e) {
AlertManager.error(e);
errorMessage = "Download error: " + e.getMessage();
// Log.e(TAG, errorMessage, e);
deletePartialFile();
return null;
} finally {
closeResources(inputStream, outputStream, connection);
}
}
@Override
protected void onPostExecute(File result) {
if (callback != null) {
callback.onResult(errorMessage);
}
}
@Override
protected void onCancelled() {
deletePartialFile();
if (callback != null) {
callback.onResult("Download cancelled");
}
}
private String readErrorResponse(HttpURLConnection connection) {
try {
InputStream errorStream = connection.getErrorStream();
if (errorStream != null) {
BufferedReader reader = new BufferedReader(new InputStreamReader(errorStream));
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
reader.close();
return response.toString();
}
} catch (IOException e) {
AlertManager.error(e);
// Log.e(TAG, "Error reading error response", e);
}
return null;
}
private void deletePartialFile() {
if (destinationFile != null && destinationFile.exists()) {
if (destinationFile.delete()) {
// Log.d(TAG, "Partial download file deleted");
} else {
// Log.w(TAG, "Failed to delete partial download file");
}
}
}
private void closeResources(InputStream inputStream, OutputStream outputStream, HttpURLConnection connection) {
try {
if (inputStream != null) inputStream.close();
} catch (IOException e) {
AlertManager.error(e);
// Log.e(TAG, "Error closing input stream", e);
}
try {
if (outputStream != null) outputStream.close();
} catch (IOException e) {
AlertManager.error(e);
// Log.e(TAG, "Error closing output stream", e);
}
if (connection != null) {
connection.disconnect();
}
}
}
@@ -0,0 +1,161 @@
package com.ridgebotics.ridgescout.utility;
import android.os.AsyncTask;
//import android.util.Log;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
public class HttpPutFile extends AsyncTask<Void, Integer, Boolean> {
// private static final String TAG = "FileUploadTask";
public interface UploadCallback {
void onResult(String error);
}
private String uploadUrl;
private File fileToUpload;
private UploadCallback callback;
private String errorMessage;
private String[] headers;
public HttpPutFile(String uploadUrl, File fileToUpload, UploadCallback callback, String[] headers) {
this.uploadUrl = uploadUrl;
this.fileToUpload = fileToUpload;
this.callback = callback;
this.headers = headers;
}
@Override
protected Boolean doInBackground(Void... voids) {
HttpURLConnection connection = null;
InputStream fileInputStream = null;
OutputStream outputStream = null;
try {
if (!fileToUpload.exists()) {
errorMessage = "File does not exist: " + fileToUpload.getAbsolutePath();
return false;
}
URL url = new URL(uploadUrl);
connection = (HttpURLConnection) url.openConnection();
// Configure connection for PUT request
connection.setRequestMethod("PUT");
connection.setDoOutput(true);
connection.setDoInput(true);
connection.setUseCaches(false);
connection.setRequestProperty("Content-Type", "application/octet-stream");
connection.setRequestProperty("Content-Length", String.valueOf(fileToUpload.length()));
connection.setConnectTimeout(30000); // 30 seconds
connection.setReadTimeout(60000); // 60 seconds
for(int i = 0; i < headers.length; i++){
String[] split = headers[i].split(": ");
connection.setRequestProperty(split[0], split[1]);
}
connection.connect();
outputStream = connection.getOutputStream();
fileInputStream = new FileInputStream(fileToUpload);
byte[] buffer = new byte[8192];
long totalBytes = fileToUpload.length();
long uploadedBytes = 0;
int bytesRead;
while ((bytesRead = fileInputStream.read(buffer)) != -1) {
if (isCancelled()) {
return false;
}
outputStream.write(buffer, 0, bytesRead);
uploadedBytes += bytesRead;
// Update progress
int progress = (int) ((uploadedBytes * 100) / totalBytes);
publishProgress(progress);
}
outputStream.flush();
// Check response code
int responseCode = connection.getResponseCode();
if (responseCode >= 200 && responseCode < 300) {
// Log.d(TAG, "Upload successful. Response code: " + responseCode);
return true;
} else {
// Read error response if available
String errorResponse = readErrorResponse(connection);
errorMessage = "Upload failed. Response code: " + responseCode +
(errorResponse != null ? ". Error: " + errorResponse : "");
return false;
}
} catch (Exception e) {
AlertManager.error(e);
errorMessage = "Upload error: " + e.getMessage();
// Log.e(TAG, errorMessage, e);
return false;
} finally {
closeResources(fileInputStream, outputStream, connection);
}
}
@Override
protected void onPostExecute(Boolean success) {
if (callback != null) {
callback.onResult(errorMessage);
}
}
@Override
protected void onCancelled() {
if (callback != null) {
callback.onResult("Upload cancelled");
}
}
private String readErrorResponse(HttpURLConnection connection) {
try {
InputStream errorStream = connection.getErrorStream();
if (errorStream != null) {
BufferedReader reader = new BufferedReader(new InputStreamReader(errorStream));
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
reader.close();
return response.toString();
}
} catch (IOException e) {
AlertManager.error(e);
// Log.e(TAG, "Error reading error response", e);
}
return null;
}
private void closeResources(InputStream inputStream, OutputStream outputStream, HttpURLConnection connection) {
try {
if (inputStream != null) inputStream.close();
} catch (IOException e) {
AlertManager.error(e);
// Log.e(TAG, "Error closing input stream", e);
}
try {
if (outputStream != null) outputStream.close();
} catch (IOException e) {
AlertManager.error(e);
// Log.e(TAG, "Error closing output stream", e);
}
if (connection != null) {
connection.disconnect();
}
}
}
@@ -28,6 +28,7 @@ public class SettingsManager {
public static final String BtUUIDKey = "bt_uuid";
public static final String FTPEnabled = "ftp_enabled";
public static final String FTPServer = "ftp_server";
public static final String FTPKey = "ftp_key";
public static final String FTPSendMetaFiles = "ftp_send_meta_files";
public static final String EnableQuickAllianceChangeKey = "enable_quick_alliance_change";
@@ -53,7 +54,8 @@ public class SettingsManager {
hm.put(TeamsDataModeKey, 0);
hm.put(BtUUIDKey, UUID.randomUUID().toString());
hm.put(FTPEnabled, false);
hm.put(FTPServer, "0.0.0.0");
hm.put(FTPServer, "http://127.0.0.1:8080");
hm.put(FTPKey, "5uper_5ecure_k3y");
hm.put(FTPSendMetaFiles, false);
hm.put(EnableQuickAllianceChangeKey, false);
hm.put(CustomEventsKey, false);
@@ -131,6 +133,9 @@ public class SettingsManager {
public static String getFTPServer(){return prefs.getString( FTPServer, (String) defaults.get(FTPServer));}
public static void setFTPServer(String str){ getEditor().putString( FTPServer,str).apply();}
public static String getFTPKey(){return prefs.getString( FTPKey, (String) defaults.get(FTPKey));}
public static void setFTPKey(String str){ getEditor().putString( FTPKey,str).apply();}
public static boolean getFTPSendMetaFiles(){return prefs.getBoolean(FTPSendMetaFiles, (boolean) defaults.get(FTPSendMetaFiles));}
public static void setFTPSendMetaFiles(boolean bool){getEditor().putBoolean(FTPSendMetaFiles,bool).apply();}
BIN
View File
Binary file not shown.
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="800dp"
android:height="800dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M14,5v10l-9,-5 9,-5z"
android:fillColor="#000000"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="800dp"
android:height="800dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M15,10l-9,5V5l9,5z"
android:fillColor="#000000"/>
</vector>
@@ -5,16 +5,28 @@
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.ridgebotics.ridgescout.ui.views.MatchScoutingIndicator
android:id="@+id/bindicator"
android:layout_width="match_parent"
android:layout_height="60dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ScrollView
android:id="@+id/scrollView3"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent">
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0">
<LinearLayout
android:id="@+id/MatchScoutArea"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="48dp">
@@ -25,16 +37,15 @@
android:layout_alignParentEnd="true"
android:layout_margin="5dp"
android:background="@drawable/border"
android:padding="10dp"
android:orientation="horizontal">
android:orientation="horizontal"
android:padding="10dp">
<TextView
android:id="@+id/scouting_notice_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
android:text="Scouting Notice">
</TextView>
android:text="Scouting Notice"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"></TextView>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -46,75 +57,4 @@
</LinearLayout>
</ScrollView>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/file_indicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#60ff0000"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<Button
android:id="@+id/back_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Back"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/next_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Next"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/alliance_pos_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Temp"
android:textAlignment="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/matchnum"
app:layout_constraintStart_toEndOf="@+id/back_button"
app:layout_constraintTop_toBottomOf="@id/username" />
<TextView
android:id="@+id/username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Temp"
android:textAlignment="center"
app:layout_constraintEnd_toStartOf="@+id/matchnum"
app:layout_constraintStart_toEndOf="@+id/back_button"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/matchnum"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Temp"
android:textAlignment="center"
android:textSize="24sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/bar_team_num"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Temp"
android:textSize="24sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/next_button"
app:layout_constraintStart_toEndOf="@+id/matchnum"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/file_indicator_box"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="@drawable/border"
android:padding="3dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<View
android:id="@+id/match_indicator_background"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageButton
android:id="@+id/match_indicator_back_button"
android:layout_width="50dp"
android:layout_height="50dp"
android:scaleType="fitCenter"
android:src="@drawable/triangle_left"
android:text="Back"
android:background="@color/zxing_transparent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageButton
android:id="@+id/match_indicator_next_button"
android:layout_width="50dp"
android:layout_height="50dp"
android:scaleType="fitCenter"
android:src="@drawable/triangle_right"
android:text="Next"
android:background="@color/zxing_transparent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/match_indicator_alliance_pos_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:text="Temp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
android:textAlignment="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/match_indicator_next_button"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/match_indicator_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:text="Temp"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/match_indicator_back_button"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/match_indicator_matchnum"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Temp"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/match_indicator_bar_team_num"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="3dp"
android:layout_marginTop="16dp"
android:text="Temp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
app:layout_constraintBottom_toBottomOf="@+id/match_indicator_matchnum"
app:layout_constraintStart_toEndOf="@+id/match_indicator_matchnum"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</RelativeLayout>
View File
+127
View File
@@ -0,0 +1,127 @@
from ast import mod
import os
import json
import hashlib
from datetime import datetime
from bottle import Bottle, run, get, put, static_file, response, request,HTTPResponse
from random import SystemRandom
from utils import *
app = Bottle()
file_metadata = {}
def save_metadata():
global file_metadata
write(METADATA_PATH4, json.dumps(file_metadata))
def load_metadata():
global file_metadata
data = read(METADATA_PATH4)
if data is not None:
file_metadata = json.loads(data)
api_key = None
cryptogen = SystemRandom()
def aquire_key():
global api_key
global cryptogen
if api_key is None:
try:
api_key = read(API_KEY_PATH).decode("utf-8").strip()
except:
ran = cryptogen.randrange(10**80)
api_key = "%064x" % ran
write(API_KEY_PATH, api_key)
@app.route('/')
def list_html():
global file_metadata
load_metadata()
content = '<html><body><table><tr>'
for heading in ['File', 'Size', 'Modified', 'Sha256']:
content += f'<th>{heading}</th>'
content += "</tr>"
print(file_metadata)
for filename in file_metadata.keys():
content += "<tr>"
content += f'<td><a href="/api/{filename}">{filename}</a></td>'
content += f'<td>{file_metadata[filename]["size"]}B</td>'
content += f'<td>{file_metadata[filename]["modified"]}</td>'
content += f'<td>{file_metadata[filename]["sha256"]}</td>'
content += "</tr>"
content += '</table></body></html>'
return content
@app.route('/api/metadata')
def metadata():
global file_metadata
load_metadata()
response.content_type = 'application/json'
return json.dumps(file_metadata)
@app.route('/api/<filename>', method='PUT')
def upload(filename):
global api_key
try:
sentkey = request.headers[API_KEY_HEADER]
if sentkey != api_key:
return HTTPResponse(status=403, body=f"Invalid Key")
except:
return HTTPResponse(status=403, body="You must specify an 'api_key' header")
global file_metadata
load_metadata()
data = request.body.read()
try:
modified = request.headers[MODIFIED_HEADER]
except:
modified = (datetime.now() - datetime(1970, 1, 1)).total_seconds()
sha256_hash = hashlib.sha256()
sha256_hash.update(data)
file_metadata[filename] = {
'size': len(data),
'modified': modified,
'sha256': sha256_hash.hexdigest()
}
save_metadata()
write(os.path.join(DATA_ROOT, filename), data)
# save_metadata()
# response.content_type = 'application/json'
# return json.dumps(file_metadata)
@app.route('/api/<filename>')
def download(filename):
data = read(os.path.join(DATA_ROOT, filename))
if data is not None:
response.content_type = 'application/octet-stream'
return data
else:
HTTPResponse(status=404, body="File not found")
if __name__ == '__main__':
mkdir(DATA_ROOT)
aquire_key()
app.run(host='0.0.0.0', port=8080)
+2
View File
@@ -0,0 +1,2 @@
bottle
random
+40
View File
@@ -0,0 +1,40 @@
import os
ROOT = os.path.dirname(__file__)
DATA_ROOT = os.path.join(os.path.dirname(__file__), 'server_data')
METADATA_PATH4 = os.path.join(ROOT, 'metadata.json')
API_KEY_PATH = os.path.join(ROOT, 'api_key.txt')
MODIFIED_HEADER = 'modified'
API_KEY_HEADER = 'api_key'
def mkdir(path):
if not os.path.exists(path):
os.makedirs(path)
def ls(path):
try:
return os.listdir(path)
except:
return []
def read(path):
if not os.path.exists(path):
return None
try:
with open(path, 'rb') as f:
return f.read()
except Exception as e:
print(f"Error reading file {path}: {e}")
return None
def write(path, data):
if not os.path.exists(path):
with open(path, mode='ab'): pass
if isinstance(data, str):
data = str.encode(data)
with open(path, 'wb') as f:
f.write(data)