mirror of
https://github.com/wpilibsuite/allwpilib
synced 2026-06-27 02:01:42 +00:00
[glass] Add glass: an application for display of robot data
This reuses many pieces of the current simulation GUI. The common pieces have been refactored into the libglass library. The libglass library is designed to be usable for other standalone data visualization applications (e.g. viewing data logs). The name "glass" comes from "glass cockpit", as the application features several multi-function displays that can be adjusted to display robot information as needed.
This commit is contained in:
46
glass/src/libnt/native/cpp/NTDigitalInput.cpp
Normal file
46
glass/src/libnt/native/cpp/NTDigitalInput.cpp
Normal file
@@ -0,0 +1,46 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#include "glass/networktables/NTDigitalInput.h"
|
||||
|
||||
#include <wpi/Twine.h>
|
||||
|
||||
using namespace glass;
|
||||
|
||||
NTDigitalInputModel::NTDigitalInputModel(wpi::StringRef path)
|
||||
: NTDigitalInputModel{nt::GetDefaultInstance(), path} {}
|
||||
|
||||
NTDigitalInputModel::NTDigitalInputModel(NT_Inst inst, wpi::StringRef path)
|
||||
: m_nt{inst},
|
||||
m_value{m_nt.GetEntry(path + "/Value")},
|
||||
m_name{m_nt.GetEntry(path + "/.name")},
|
||||
m_valueData{"NT_DIn:" + path},
|
||||
m_nameValue{path.rsplit('/').second} {
|
||||
m_nt.AddListener(m_value);
|
||||
m_nt.AddListener(m_name);
|
||||
|
||||
m_valueData.SetDigital(true);
|
||||
Update();
|
||||
}
|
||||
|
||||
void NTDigitalInputModel::Update() {
|
||||
for (auto&& event : m_nt.PollListener()) {
|
||||
if (event.entry == m_value) {
|
||||
if (event.value && event.value->IsBoolean()) {
|
||||
m_valueData.SetValue(event.value->GetBoolean());
|
||||
}
|
||||
} else if (event.entry == m_name) {
|
||||
if (event.value && event.value->IsString()) {
|
||||
m_nameValue = event.value->GetString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool NTDigitalInputModel::Exists() {
|
||||
return m_nt.IsConnected() && nt::GetEntryType(m_value) != NT_UNASSIGNED;
|
||||
}
|
||||
55
glass/src/libnt/native/cpp/NTDigitalOutput.cpp
Normal file
55
glass/src/libnt/native/cpp/NTDigitalOutput.cpp
Normal file
@@ -0,0 +1,55 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#include "glass/networktables/NTDigitalOutput.h"
|
||||
|
||||
#include <wpi/Twine.h>
|
||||
|
||||
using namespace glass;
|
||||
|
||||
NTDigitalOutputModel::NTDigitalOutputModel(wpi::StringRef path)
|
||||
: NTDigitalOutputModel{nt::GetDefaultInstance(), path} {}
|
||||
|
||||
NTDigitalOutputModel::NTDigitalOutputModel(NT_Inst inst, wpi::StringRef path)
|
||||
: m_nt{inst},
|
||||
m_value{m_nt.GetEntry(path + "/Value")},
|
||||
m_name{m_nt.GetEntry(path + "/.name")},
|
||||
m_controllable{m_nt.GetEntry(path + "/.controllable")},
|
||||
m_valueData{"NT_DOut:" + path} {
|
||||
m_nt.AddListener(m_value);
|
||||
m_nt.AddListener(m_name);
|
||||
m_nt.AddListener(m_controllable);
|
||||
|
||||
m_valueData.SetDigital(true);
|
||||
Update();
|
||||
}
|
||||
|
||||
void NTDigitalOutputModel::SetValue(bool val) {
|
||||
nt::SetEntryValue(m_value, nt::Value::MakeBoolean(val));
|
||||
}
|
||||
|
||||
void NTDigitalOutputModel::Update() {
|
||||
for (auto&& event : m_nt.PollListener()) {
|
||||
if (event.entry == m_value) {
|
||||
if (event.value && event.value->IsBoolean()) {
|
||||
m_valueData.SetValue(event.value->GetBoolean());
|
||||
}
|
||||
} else if (event.entry == m_name) {
|
||||
if (event.value && event.value->IsString()) {
|
||||
m_nameValue = event.value->GetString();
|
||||
}
|
||||
} else if (event.entry == m_controllable) {
|
||||
if (event.value && event.value->IsBoolean()) {
|
||||
m_controllableValue = event.value->GetBoolean();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool NTDigitalOutputModel::Exists() {
|
||||
return m_nt.IsConnected() && nt::GetEntryType(m_value) != NT_UNASSIGNED;
|
||||
}
|
||||
93
glass/src/libnt/native/cpp/NTFMS.cpp
Normal file
93
glass/src/libnt/native/cpp/NTFMS.cpp
Normal file
@@ -0,0 +1,93 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#include "glass/networktables/NTFMS.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <wpi/Twine.h>
|
||||
#include <wpi/timestamp.h>
|
||||
|
||||
using namespace glass;
|
||||
|
||||
NTFMSModel::NTFMSModel(wpi::StringRef path)
|
||||
: NTFMSModel{nt::GetDefaultInstance(), path} {}
|
||||
|
||||
NTFMSModel::NTFMSModel(NT_Inst inst, wpi::StringRef path)
|
||||
: m_nt{inst},
|
||||
m_gameSpecificMessage{m_nt.GetEntry(path + "/GameSpecificMessage")},
|
||||
m_alliance{m_nt.GetEntry(path + "/IsRedAlliance")},
|
||||
m_station{m_nt.GetEntry(path + "/StationNumber")},
|
||||
m_controlWord{m_nt.GetEntry(path + "/FMSControlData")},
|
||||
m_fmsAttached{"NT_FMS:FMSAttached:" + path},
|
||||
m_dsAttached{"NT_FMS:DSAttached:" + path},
|
||||
m_allianceStationId{"NT_FMS:AllianceStationID:" + path},
|
||||
m_estop{"NT_FMS:EStop:" + path},
|
||||
m_enabled{"NT_FMS:RobotEnabled:" + path},
|
||||
m_test{"NT_FMS:TestMode:" + path},
|
||||
m_autonomous{"NT_FMS:AutonomousMode:" + path} {
|
||||
m_nt.AddListener(m_alliance);
|
||||
m_nt.AddListener(m_station);
|
||||
m_nt.AddListener(m_controlWord);
|
||||
|
||||
m_fmsAttached.SetDigital(true);
|
||||
m_dsAttached.SetDigital(true);
|
||||
m_estop.SetDigital(true);
|
||||
m_enabled.SetDigital(true);
|
||||
m_test.SetDigital(true);
|
||||
m_autonomous.SetDigital(true);
|
||||
Update();
|
||||
}
|
||||
|
||||
wpi::StringRef NTFMSModel::GetGameSpecificMessage(
|
||||
wpi::SmallVectorImpl<char>& buf) {
|
||||
buf.clear();
|
||||
auto value = nt::GetEntryValue(m_gameSpecificMessage);
|
||||
if (value && value->IsString()) {
|
||||
auto str = value->GetString();
|
||||
buf.append(str.begin(), str.end());
|
||||
}
|
||||
return wpi::StringRef{buf.data(), buf.size()};
|
||||
}
|
||||
|
||||
void NTFMSModel::Update() {
|
||||
for (auto&& event : m_nt.PollListener()) {
|
||||
if (event.entry == m_alliance) {
|
||||
if (event.value && event.value->IsBoolean()) {
|
||||
int allianceStationId = m_allianceStationId.GetValue();
|
||||
allianceStationId %= 3;
|
||||
// true if red
|
||||
allianceStationId += 3 * (event.value->GetBoolean() ? 0 : 1);
|
||||
m_allianceStationId.SetValue(allianceStationId);
|
||||
}
|
||||
} else if (event.entry == m_station) {
|
||||
if (event.value && event.value->IsDouble()) {
|
||||
int allianceStationId = m_allianceStationId.GetValue();
|
||||
bool isRed = (allianceStationId < 3);
|
||||
// the NT value is 1-indexed
|
||||
m_allianceStationId.SetValue(event.value->GetDouble() - 1 +
|
||||
3 * (isRed ? 0 : 1));
|
||||
}
|
||||
} else if (event.entry == m_controlWord) {
|
||||
if (event.value && event.value->IsDouble()) {
|
||||
uint32_t controlWord = event.value->GetDouble();
|
||||
// See HAL_ControlWord definition
|
||||
auto time = wpi::Now();
|
||||
m_enabled.SetValue(((controlWord & 0x01) != 0) ? 1 : 0, time);
|
||||
m_autonomous.SetValue(((controlWord & 0x02) != 0) ? 1 : 0, time);
|
||||
m_test.SetValue(((controlWord & 0x04) != 0) ? 1 : 0, time);
|
||||
m_estop.SetValue(((controlWord & 0x08) != 0) ? 1 : 0, time);
|
||||
m_fmsAttached.SetValue(((controlWord & 0x10) != 0) ? 1 : 0, time);
|
||||
m_dsAttached.SetValue(((controlWord & 0x20) != 0) ? 1 : 0, time);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool NTFMSModel::Exists() {
|
||||
return m_nt.IsConnected() && nt::GetEntryType(m_controlWord) != NT_UNASSIGNED;
|
||||
}
|
||||
236
glass/src/libnt/native/cpp/NTField2D.cpp
Normal file
236
glass/src/libnt/native/cpp/NTField2D.cpp
Normal file
@@ -0,0 +1,236 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#include "glass/networktables/NTField2D.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <ntcore_cpp.h>
|
||||
#include <wpi/SmallVector.h>
|
||||
|
||||
#include "glass/DataSource.h"
|
||||
|
||||
using namespace glass;
|
||||
|
||||
class NTField2DModel::GroupModel : public FieldObjectGroupModel {
|
||||
public:
|
||||
GroupModel(wpi::StringRef name, NT_Entry entry)
|
||||
: m_name{name}, m_entry{entry} {}
|
||||
|
||||
wpi::StringRef GetName() const { return m_name; }
|
||||
NT_Entry GetEntry() const { return m_entry; }
|
||||
|
||||
void NTUpdate(const nt::Value& value);
|
||||
|
||||
void Update() override {
|
||||
if (auto value = nt::GetEntryValue(m_entry)) NTUpdate(*value);
|
||||
}
|
||||
bool Exists() override { return nt::GetEntryType(m_entry) != NT_UNASSIGNED; }
|
||||
bool IsReadOnly() override { return false; }
|
||||
|
||||
void ForEachFieldObject(
|
||||
wpi::function_ref<void(FieldObjectModel& model)> func) override;
|
||||
|
||||
private:
|
||||
std::string m_name;
|
||||
NT_Entry m_entry;
|
||||
|
||||
// keep count of objects rather than resizing vector, as there is a fair
|
||||
// amount of overhead associated with the latter (DataSource record keeping)
|
||||
size_t m_count = 0;
|
||||
class ObjectModel;
|
||||
std::vector<std::unique_ptr<ObjectModel>> m_objects;
|
||||
};
|
||||
|
||||
class NTField2DModel::GroupModel::ObjectModel : public FieldObjectModel {
|
||||
public:
|
||||
ObjectModel(wpi::StringRef name, NT_Entry entry, int index)
|
||||
: m_entry{entry},
|
||||
m_index{index},
|
||||
m_x{name + "[" + wpi::Twine{index} + "]/x"},
|
||||
m_y{name + "[" + wpi::Twine{index} + "]/y"},
|
||||
m_rot{name + "[" + wpi::Twine{index} + "]/rot"} {}
|
||||
|
||||
void SetExists(bool exists) { m_exists = exists; }
|
||||
|
||||
void Update() override {}
|
||||
bool Exists() override { return m_exists; }
|
||||
bool IsReadOnly() override { return false; }
|
||||
|
||||
DataSource* GetXData() override { return &m_x; }
|
||||
DataSource* GetYData() override { return &m_y; }
|
||||
DataSource* GetRotationData() override { return &m_rot; }
|
||||
|
||||
void SetPose(double x, double y, double rot) override;
|
||||
void SetPosition(double x, double y) override;
|
||||
void SetRotation(double rot) override;
|
||||
|
||||
private:
|
||||
void SetPoseImpl(double x, double y, double rot, bool setX, bool setY,
|
||||
bool setRot);
|
||||
|
||||
NT_Entry m_entry;
|
||||
int m_index;
|
||||
bool m_exists = true;
|
||||
|
||||
public:
|
||||
DataSource m_x;
|
||||
DataSource m_y;
|
||||
DataSource m_rot;
|
||||
};
|
||||
|
||||
void NTField2DModel::GroupModel::NTUpdate(const nt::Value& value) {
|
||||
if (!value.IsDoubleArray()) {
|
||||
m_count = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
auto arr = value.GetDoubleArray();
|
||||
// must be triples
|
||||
if ((arr.size() % 3) != 0) {
|
||||
m_count = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
m_count = arr.size() / 3;
|
||||
if (m_count > m_objects.size()) {
|
||||
m_objects.reserve(m_count);
|
||||
for (size_t i = m_objects.size(); i < m_count; ++i)
|
||||
m_objects.emplace_back(std::make_unique<ObjectModel>(m_name, m_entry, i));
|
||||
}
|
||||
if (m_count < m_objects.size()) {
|
||||
for (size_t i = m_count; i < m_objects.size(); ++i)
|
||||
m_objects[i]->SetExists(false);
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < m_count; ++i) {
|
||||
auto& obj = m_objects[i];
|
||||
obj->SetExists(true);
|
||||
obj->m_x.SetValue(arr[i * 3], value.last_change());
|
||||
obj->m_y.SetValue(arr[i * 3 + 1], value.last_change());
|
||||
obj->m_rot.SetValue(arr[i * 3 + 2], value.last_change());
|
||||
}
|
||||
}
|
||||
|
||||
void NTField2DModel::GroupModel::ForEachFieldObject(
|
||||
wpi::function_ref<void(FieldObjectModel& model)> func) {
|
||||
for (size_t i = 0; i < m_count; ++i) {
|
||||
func(*m_objects[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void NTField2DModel::GroupModel::ObjectModel::SetPose(double x, double y,
|
||||
double rot) {
|
||||
SetPoseImpl(x, y, rot, true, true, true);
|
||||
}
|
||||
|
||||
void NTField2DModel::GroupModel::ObjectModel::SetPosition(double x, double y) {
|
||||
SetPoseImpl(x, y, 0, true, true, false);
|
||||
}
|
||||
|
||||
void NTField2DModel::GroupModel::ObjectModel::SetRotation(double rot) {
|
||||
SetPoseImpl(0, 0, rot, false, false, true);
|
||||
}
|
||||
|
||||
void NTField2DModel::GroupModel::ObjectModel::SetPoseImpl(double x, double y,
|
||||
double rot, bool setX,
|
||||
bool setY,
|
||||
bool setRot) {
|
||||
// get from NT, validate type and size
|
||||
auto value = nt::GetEntryValue(m_entry);
|
||||
if (!value || !value->IsDoubleArray()) return;
|
||||
auto origArr = value->GetDoubleArray();
|
||||
if (origArr.size() < static_cast<size_t>((m_index + 1) * 3)) return;
|
||||
|
||||
// copy existing array
|
||||
wpi::SmallVector<double, 8> arr;
|
||||
arr.reserve(origArr.size());
|
||||
for (auto&& elem : origArr) arr.emplace_back(elem);
|
||||
|
||||
// update value
|
||||
if (setX) arr[m_index * 3 + 0] = x;
|
||||
if (setY) arr[m_index * 3 + 1] = y;
|
||||
if (setRot) arr[m_index * 3 + 2] = rot;
|
||||
|
||||
// set back to NT
|
||||
nt::SetEntryValue(m_entry, nt::Value::MakeDoubleArray(arr));
|
||||
}
|
||||
|
||||
NTField2DModel::NTField2DModel(wpi::StringRef path)
|
||||
: NTField2DModel{nt::GetDefaultInstance(), path} {}
|
||||
|
||||
NTField2DModel::NTField2DModel(NT_Inst inst, wpi::StringRef path)
|
||||
: m_nt{inst},
|
||||
m_path{(path + "/").str()},
|
||||
m_name{m_nt.GetEntry(path + "/.name")} {
|
||||
m_nt.AddListener(m_path, NT_NOTIFY_LOCAL | NT_NOTIFY_NEW | NT_NOTIFY_DELETE |
|
||||
NT_NOTIFY_UPDATE | NT_NOTIFY_IMMEDIATE);
|
||||
}
|
||||
|
||||
NTField2DModel::~NTField2DModel() {}
|
||||
|
||||
void NTField2DModel::Update() {
|
||||
for (auto&& event : m_nt.PollListener()) {
|
||||
// .name
|
||||
if (event.entry == m_name) {
|
||||
if (event.value && event.value->IsString()) {
|
||||
m_nameValue = event.value->GetString();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// common case: update of existing entry; search by entry
|
||||
if (event.flags & NT_NOTIFY_UPDATE) {
|
||||
auto it = std::find_if(
|
||||
m_groups.begin(), m_groups.end(),
|
||||
[&](const auto& e) { return e->GetEntry() == event.entry; });
|
||||
if (it != m_groups.end()) {
|
||||
(*it)->NTUpdate(*event.value);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// handle create/delete
|
||||
if (wpi::StringRef{event.name}.startswith(m_path)) {
|
||||
auto name = wpi::StringRef{event.name}.drop_front(m_path.size());
|
||||
if (name.empty() || name[0] == '.') continue;
|
||||
auto it = std::lower_bound(m_groups.begin(), m_groups.end(), name,
|
||||
[](const auto& e, wpi::StringRef name) {
|
||||
return e->GetName() < name;
|
||||
});
|
||||
bool match = (it != m_groups.end() && (*it)->GetName() == name);
|
||||
if (event.flags & NT_NOTIFY_DELETE) {
|
||||
if (match) m_groups.erase(it);
|
||||
continue;
|
||||
} else if (event.flags & NT_NOTIFY_NEW) {
|
||||
if (!match)
|
||||
it = m_groups.emplace(
|
||||
it, std::make_unique<GroupModel>(event.name, event.entry));
|
||||
} else if (!match) {
|
||||
continue;
|
||||
}
|
||||
if (event.flags & (NT_NOTIFY_NEW | NT_NOTIFY_UPDATE)) {
|
||||
(*it)->NTUpdate(*event.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool NTField2DModel::Exists() {
|
||||
return m_nt.IsConnected() && nt::GetEntryType(m_name) != NT_UNASSIGNED;
|
||||
}
|
||||
|
||||
bool NTField2DModel::IsReadOnly() { return false; }
|
||||
|
||||
void NTField2DModel::ForEachFieldObjectGroup(
|
||||
wpi::function_ref<void(FieldObjectGroupModel& model, wpi::StringRef name)>
|
||||
func) {
|
||||
for (auto&& group : m_groups) {
|
||||
if (group->Exists())
|
||||
func(*group, wpi::StringRef{group->GetName()}.drop_front(m_path.size()));
|
||||
}
|
||||
}
|
||||
64
glass/src/libnt/native/cpp/NTStringChooser.cpp
Normal file
64
glass/src/libnt/native/cpp/NTStringChooser.cpp
Normal file
@@ -0,0 +1,64 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#include "glass/networktables/NTStringChooser.h"
|
||||
|
||||
using namespace glass;
|
||||
|
||||
NTStringChooserModel::NTStringChooserModel(wpi::StringRef path)
|
||||
: NTStringChooserModel{nt::GetDefaultInstance(), path} {}
|
||||
|
||||
NTStringChooserModel::NTStringChooserModel(NT_Inst inst, wpi::StringRef path)
|
||||
: m_nt{inst},
|
||||
m_default{m_nt.GetEntry(path + "/default")},
|
||||
m_selected{m_nt.GetEntry(path + "/selected")},
|
||||
m_active{m_nt.GetEntry(path + "/active")},
|
||||
m_options{m_nt.GetEntry(path + "/options")} {
|
||||
m_nt.AddListener(m_default);
|
||||
m_nt.AddListener(m_selected);
|
||||
m_nt.AddListener(m_active);
|
||||
m_nt.AddListener(m_options);
|
||||
|
||||
Update();
|
||||
}
|
||||
|
||||
void NTStringChooserModel::SetDefault(wpi::StringRef val) {
|
||||
nt::SetEntryValue(m_default, nt::Value::MakeString(val));
|
||||
}
|
||||
|
||||
void NTStringChooserModel::SetSelected(wpi::StringRef val) {
|
||||
nt::SetEntryValue(m_selected, nt::Value::MakeString(val));
|
||||
}
|
||||
|
||||
void NTStringChooserModel::SetActive(wpi::StringRef val) {
|
||||
nt::SetEntryValue(m_active, nt::Value::MakeString(val));
|
||||
}
|
||||
|
||||
void NTStringChooserModel::SetOptions(wpi::ArrayRef<std::string> val) {
|
||||
nt::SetEntryValue(m_options, nt::Value::MakeStringArray(val));
|
||||
}
|
||||
|
||||
void NTStringChooserModel::Update() {
|
||||
for (auto&& event : m_nt.PollListener()) {
|
||||
if (event.entry == m_default && event.value && event.value->IsString()) {
|
||||
m_defaultValue = event.value->GetString();
|
||||
} else if (event.entry == m_selected && event.value &&
|
||||
event.value->IsString()) {
|
||||
m_selectedValue = event.value->GetString();
|
||||
} else if (event.entry == m_active && event.value &&
|
||||
event.value->IsString()) {
|
||||
m_activeValue = event.value->GetString();
|
||||
} else if (event.entry == m_options && event.value &&
|
||||
event.value->IsStringArray()) {
|
||||
m_optionsValue = event.value->GetStringArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool NTStringChooserModel::Exists() {
|
||||
return m_nt.IsConnected() && nt::GetEntryType(m_options) != NT_UNASSIGNED;
|
||||
}
|
||||
610
glass/src/libnt/native/cpp/NetworkTables.cpp
Normal file
610
glass/src/libnt/native/cpp/NetworkTables.cpp
Normal file
@@ -0,0 +1,610 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#include "glass/networktables/NetworkTables.h"
|
||||
|
||||
#include <cinttypes>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <initializer_list>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include <imgui.h>
|
||||
#include <networktables/NetworkTableValue.h>
|
||||
#include <ntcore_cpp.h>
|
||||
#include <wpi/ArrayRef.h>
|
||||
#include <wpi/Format.h>
|
||||
#include <wpi/SmallString.h>
|
||||
#include <wpi/StringRef.h>
|
||||
#include <wpi/raw_ostream.h>
|
||||
|
||||
#include "glass/Context.h"
|
||||
#include "glass/DataSource.h"
|
||||
|
||||
using namespace glass;
|
||||
|
||||
static std::string BooleanArrayToString(wpi::ArrayRef<int> in) {
|
||||
std::string rv;
|
||||
wpi::raw_string_ostream os{rv};
|
||||
os << '[';
|
||||
bool first = true;
|
||||
for (auto v : in) {
|
||||
if (!first) os << ',';
|
||||
first = false;
|
||||
if (v)
|
||||
os << "true";
|
||||
else
|
||||
os << "false";
|
||||
}
|
||||
os << ']';
|
||||
return rv;
|
||||
}
|
||||
|
||||
static std::string DoubleArrayToString(wpi::ArrayRef<double> in) {
|
||||
std::string rv;
|
||||
wpi::raw_string_ostream os{rv};
|
||||
os << '[';
|
||||
bool first = true;
|
||||
for (auto v : in) {
|
||||
if (!first) os << ',';
|
||||
first = false;
|
||||
os << wpi::format("%.6f", v);
|
||||
}
|
||||
os << ']';
|
||||
return rv;
|
||||
}
|
||||
|
||||
static std::string StringArrayToString(wpi::ArrayRef<std::string> in) {
|
||||
std::string rv;
|
||||
wpi::raw_string_ostream os{rv};
|
||||
os << '[';
|
||||
bool first = true;
|
||||
for (auto&& v : in) {
|
||||
if (!first) os << ',';
|
||||
first = false;
|
||||
os << '"';
|
||||
os.write_escaped(v);
|
||||
os << '"';
|
||||
}
|
||||
os << ']';
|
||||
return rv;
|
||||
}
|
||||
|
||||
NetworkTablesModel::NetworkTablesModel()
|
||||
: NetworkTablesModel{nt::GetDefaultInstance()} {}
|
||||
|
||||
NetworkTablesModel::NetworkTablesModel(NT_Inst inst)
|
||||
: m_inst{inst}, m_poller{nt::CreateEntryListenerPoller(inst)} {
|
||||
nt::AddPolledEntryListener(m_poller, "",
|
||||
NT_NOTIFY_LOCAL | NT_NOTIFY_NEW |
|
||||
NT_NOTIFY_UPDATE | NT_NOTIFY_DELETE |
|
||||
NT_NOTIFY_FLAGS | NT_NOTIFY_IMMEDIATE);
|
||||
}
|
||||
|
||||
NetworkTablesModel::~NetworkTablesModel() {
|
||||
nt::DestroyEntryListenerPoller(m_poller);
|
||||
}
|
||||
|
||||
NetworkTablesModel::Entry::Entry(nt::EntryNotification&& event)
|
||||
: entry{event.entry},
|
||||
name{std::move(event.name)},
|
||||
value{std::move(event.value)},
|
||||
flags{nt::GetEntryFlags(event.entry)} {
|
||||
UpdateValue();
|
||||
}
|
||||
|
||||
void NetworkTablesModel::Entry::UpdateValue() {
|
||||
switch (value->type()) {
|
||||
case NT_BOOLEAN:
|
||||
if (!source)
|
||||
source = std::make_unique<DataSource>(wpi::Twine{"NT:"} + name);
|
||||
source->SetValue(value->GetBoolean() ? 1 : 0);
|
||||
source->SetDigital(true);
|
||||
break;
|
||||
case NT_DOUBLE:
|
||||
if (!source)
|
||||
source = std::make_unique<DataSource>(wpi::Twine{"NT:"} + name);
|
||||
source->SetValue(value->GetDouble());
|
||||
source->SetDigital(false);
|
||||
break;
|
||||
case NT_BOOLEAN_ARRAY:
|
||||
valueStr = BooleanArrayToString(value->GetBooleanArray());
|
||||
break;
|
||||
case NT_DOUBLE_ARRAY:
|
||||
valueStr = DoubleArrayToString(value->GetDoubleArray());
|
||||
break;
|
||||
case NT_STRING_ARRAY:
|
||||
valueStr = StringArrayToString(value->GetStringArray());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void NetworkTablesModel::Update() {
|
||||
bool timedOut = false;
|
||||
bool updateTree = false;
|
||||
for (auto&& event : nt::PollEntryListener(m_poller, 0, &timedOut)) {
|
||||
auto& entry = m_entries[event.entry];
|
||||
if (event.flags & NT_NOTIFY_NEW) {
|
||||
if (!entry) {
|
||||
entry = std::make_unique<Entry>(std::move(event));
|
||||
m_sortedEntries.emplace_back(entry.get());
|
||||
updateTree = true;
|
||||
}
|
||||
}
|
||||
if (!entry) continue;
|
||||
if (event.flags & NT_NOTIFY_DELETE) {
|
||||
auto it = std::find(m_sortedEntries.begin(), m_sortedEntries.end(),
|
||||
entry.get());
|
||||
// will be removed completely below
|
||||
if (it != m_sortedEntries.end()) *it = nullptr;
|
||||
m_entries.erase(event.entry);
|
||||
updateTree = true;
|
||||
continue;
|
||||
}
|
||||
if (event.flags & NT_NOTIFY_UPDATE) {
|
||||
entry->value = std::move(event.value);
|
||||
entry->UpdateValue();
|
||||
}
|
||||
if (event.flags & NT_NOTIFY_FLAGS) {
|
||||
entry->flags = nt::GetEntryFlags(event.entry);
|
||||
}
|
||||
}
|
||||
|
||||
// shortcut common case (updates)
|
||||
if (!updateTree) return;
|
||||
|
||||
// remove deleted entries
|
||||
m_sortedEntries.erase(
|
||||
std::remove(m_sortedEntries.begin(), m_sortedEntries.end(), nullptr),
|
||||
m_sortedEntries.end());
|
||||
|
||||
// sort by name
|
||||
std::sort(m_sortedEntries.begin(), m_sortedEntries.end(),
|
||||
[](const auto& a, const auto& b) { return a->name < b->name; });
|
||||
|
||||
// rebuild tree
|
||||
m_root.clear();
|
||||
wpi::SmallVector<wpi::StringRef, 16> parts;
|
||||
for (auto& entry : m_sortedEntries) {
|
||||
parts.clear();
|
||||
wpi::StringRef{entry->name}.split(parts, '/', -1, false);
|
||||
|
||||
// get to leaf
|
||||
auto nodes = &m_root;
|
||||
for (auto part : wpi::ArrayRef(parts.begin(), parts.end()).drop_back()) {
|
||||
auto it =
|
||||
std::find_if(nodes->begin(), nodes->end(),
|
||||
[&](const auto& node) { return node.name == part; });
|
||||
if (it == nodes->end()) {
|
||||
nodes->emplace_back(part);
|
||||
// path is from the beginning of the string to the end of the current
|
||||
// part; this works because part is a reference to the internals of
|
||||
// entry->name
|
||||
nodes->back().path.assign(entry->name.data(),
|
||||
part.end() - entry->name.data());
|
||||
it = nodes->end() - 1;
|
||||
}
|
||||
nodes = &it->children;
|
||||
}
|
||||
|
||||
auto it = std::find_if(nodes->begin(), nodes->end(), [&](const auto& node) {
|
||||
return node.name == parts.back();
|
||||
});
|
||||
if (it == nodes->end()) {
|
||||
nodes->emplace_back(parts.back());
|
||||
// no need to set path, as it's identical to entry->name
|
||||
it = nodes->end() - 1;
|
||||
}
|
||||
it->entry = entry;
|
||||
}
|
||||
}
|
||||
|
||||
bool NetworkTablesModel::Exists() { return nt::IsConnected(m_inst); }
|
||||
|
||||
static std::shared_ptr<nt::Value> StringToBooleanArray(wpi::StringRef in) {
|
||||
in = in.trim();
|
||||
if (in.empty())
|
||||
return nt::NetworkTableValue::MakeBooleanArray(
|
||||
std::initializer_list<bool>{});
|
||||
if (in.front() == '[') in = in.drop_front();
|
||||
if (in.back() == ']') in = in.drop_back();
|
||||
in = in.trim();
|
||||
|
||||
wpi::SmallVector<wpi::StringRef, 16> inSplit;
|
||||
wpi::SmallVector<int, 16> out;
|
||||
|
||||
in.split(inSplit, ',', -1, false);
|
||||
for (auto val : inSplit) {
|
||||
val = val.trim();
|
||||
if (val.equals_lower("true")) {
|
||||
out.emplace_back(1);
|
||||
} else if (val.equals_lower("false")) {
|
||||
out.emplace_back(0);
|
||||
} else {
|
||||
wpi::errs() << "GUI: NetworkTables: Could not understand value '" << val
|
||||
<< "'\n";
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
return nt::NetworkTableValue::MakeBooleanArray(out);
|
||||
}
|
||||
|
||||
static std::shared_ptr<nt::Value> StringToDoubleArray(wpi::StringRef in) {
|
||||
in = in.trim();
|
||||
if (in.empty())
|
||||
return nt::NetworkTableValue::MakeBooleanArray(
|
||||
std::initializer_list<bool>{});
|
||||
if (in.front() == '[') in = in.drop_front();
|
||||
if (in.back() == ']') in = in.drop_back();
|
||||
in = in.trim();
|
||||
|
||||
wpi::SmallVector<wpi::StringRef, 16> inSplit;
|
||||
wpi::SmallVector<double, 16> out;
|
||||
|
||||
in.split(inSplit, ',', -1, false);
|
||||
for (auto val : inSplit) {
|
||||
val = val.trim();
|
||||
wpi::SmallString<32> valStr = val;
|
||||
double d;
|
||||
if (std::sscanf(valStr.c_str(), "%lf", &d) == 1) {
|
||||
out.emplace_back(d);
|
||||
} else {
|
||||
wpi::errs() << "GUI: NetworkTables: Could not understand value '" << val
|
||||
<< "'\n";
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
return nt::NetworkTableValue::MakeDoubleArray(out);
|
||||
}
|
||||
|
||||
static int fromxdigit(char ch) {
|
||||
if (ch >= 'a' && ch <= 'f')
|
||||
return (ch - 'a' + 10);
|
||||
else if (ch >= 'A' && ch <= 'F')
|
||||
return (ch - 'A' + 10);
|
||||
else
|
||||
return ch - '0';
|
||||
}
|
||||
|
||||
static wpi::StringRef UnescapeString(wpi::StringRef source,
|
||||
wpi::SmallVectorImpl<char>& buf) {
|
||||
assert(source.size() >= 2 && source.front() == '"' && source.back() == '"');
|
||||
buf.clear();
|
||||
buf.reserve(source.size() - 2);
|
||||
for (auto s = source.begin() + 1, end = source.end() - 1; s != end; ++s) {
|
||||
if (*s != '\\') {
|
||||
buf.push_back(*s);
|
||||
continue;
|
||||
}
|
||||
switch (*++s) {
|
||||
case 't':
|
||||
buf.push_back('\t');
|
||||
break;
|
||||
case 'n':
|
||||
buf.push_back('\n');
|
||||
break;
|
||||
case 'x': {
|
||||
if (!isxdigit(*(s + 1))) {
|
||||
buf.push_back('x'); // treat it like a unknown escape
|
||||
break;
|
||||
}
|
||||
int ch = fromxdigit(*++s);
|
||||
if (std::isxdigit(*(s + 1))) {
|
||||
ch <<= 4;
|
||||
ch |= fromxdigit(*++s);
|
||||
}
|
||||
buf.push_back(static_cast<char>(ch));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
buf.push_back(*s);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return wpi::StringRef{buf.data(), buf.size()};
|
||||
}
|
||||
|
||||
static std::shared_ptr<nt::Value> StringToStringArray(wpi::StringRef in) {
|
||||
in = in.trim();
|
||||
if (in.empty())
|
||||
return nt::NetworkTableValue::MakeStringArray(
|
||||
std::initializer_list<std::string>{});
|
||||
if (in.front() == '[') in = in.drop_front();
|
||||
if (in.back() == ']') in = in.drop_back();
|
||||
in = in.trim();
|
||||
|
||||
wpi::SmallVector<wpi::StringRef, 16> inSplit;
|
||||
std::vector<std::string> out;
|
||||
wpi::SmallString<32> buf;
|
||||
|
||||
in.split(inSplit, ',', -1, false);
|
||||
for (auto val : inSplit) {
|
||||
val = val.trim();
|
||||
if (val.empty()) continue;
|
||||
if (val.front() != '"' || val.back() != '"') {
|
||||
wpi::errs() << "GUI: NetworkTables: Could not understand value '" << val
|
||||
<< "'\n";
|
||||
return nullptr;
|
||||
}
|
||||
out.emplace_back(UnescapeString(val, buf));
|
||||
}
|
||||
|
||||
return nt::NetworkTableValue::MakeStringArray(std::move(out));
|
||||
}
|
||||
|
||||
static void EmitEntryValueReadonly(NetworkTablesModel::Entry& entry) {
|
||||
auto& val = entry.value;
|
||||
if (!val) return;
|
||||
|
||||
switch (val->type()) {
|
||||
case NT_BOOLEAN:
|
||||
ImGui::LabelText("boolean", "%s", val->GetBoolean() ? "true" : "false");
|
||||
break;
|
||||
case NT_DOUBLE:
|
||||
ImGui::LabelText("double", "%.6f", val->GetDouble());
|
||||
break;
|
||||
case NT_STRING: {
|
||||
// GetString() comes from a std::string, so it's null terminated
|
||||
ImGui::LabelText("string", "%s", val->GetString().data());
|
||||
break;
|
||||
}
|
||||
case NT_BOOLEAN_ARRAY:
|
||||
ImGui::LabelText("boolean[]", "%s", entry.valueStr.c_str());
|
||||
break;
|
||||
case NT_DOUBLE_ARRAY:
|
||||
ImGui::LabelText("double[]", "%s", entry.valueStr.c_str());
|
||||
break;
|
||||
case NT_STRING_ARRAY:
|
||||
ImGui::LabelText("string[]", "%s", entry.valueStr.c_str());
|
||||
break;
|
||||
case NT_RAW:
|
||||
ImGui::LabelText("raw", "[...]");
|
||||
break;
|
||||
case NT_RPC:
|
||||
ImGui::LabelText("rpc", "[...]");
|
||||
break;
|
||||
default:
|
||||
ImGui::LabelText("other", "?");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static constexpr size_t kTextBufferSize = 4096;
|
||||
|
||||
static char* GetTextBuffer(wpi::StringRef in) {
|
||||
static char textBuffer[kTextBufferSize];
|
||||
size_t len = (std::min)(in.size(), kTextBufferSize - 1);
|
||||
std::memcpy(textBuffer, in.data(), len);
|
||||
textBuffer[len] = '\0';
|
||||
return textBuffer;
|
||||
}
|
||||
|
||||
static void EmitEntryValueEditable(NetworkTablesModel::Entry& entry) {
|
||||
auto& val = entry.value;
|
||||
if (!val) return;
|
||||
|
||||
ImGui::PushID(entry.name.c_str());
|
||||
switch (val->type()) {
|
||||
case NT_BOOLEAN: {
|
||||
static const char* boolOptions[] = {"false", "true"};
|
||||
int v = val->GetBoolean() ? 1 : 0;
|
||||
if (ImGui::Combo("boolean", &v, boolOptions, 2))
|
||||
nt::SetEntryValue(entry.entry, nt::NetworkTableValue::MakeBoolean(v));
|
||||
break;
|
||||
}
|
||||
case NT_DOUBLE: {
|
||||
double v = val->GetDouble();
|
||||
if (ImGui::InputDouble("double", &v, 0, 0, "%.6f",
|
||||
ImGuiInputTextFlags_EnterReturnsTrue))
|
||||
nt::SetEntryValue(entry.entry, nt::NetworkTableValue::MakeDouble(v));
|
||||
break;
|
||||
}
|
||||
case NT_STRING: {
|
||||
char* v = GetTextBuffer(val->GetString());
|
||||
if (ImGui::InputText("string", v, kTextBufferSize,
|
||||
ImGuiInputTextFlags_EnterReturnsTrue))
|
||||
nt::SetEntryValue(entry.entry, nt::NetworkTableValue::MakeString(v));
|
||||
break;
|
||||
}
|
||||
case NT_BOOLEAN_ARRAY: {
|
||||
char* v = GetTextBuffer(entry.valueStr);
|
||||
if (ImGui::InputText("boolean[]", v, kTextBufferSize,
|
||||
ImGuiInputTextFlags_EnterReturnsTrue)) {
|
||||
if (auto outv = StringToBooleanArray(v))
|
||||
nt::SetEntryValue(entry.entry, std::move(outv));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case NT_DOUBLE_ARRAY: {
|
||||
char* v = GetTextBuffer(entry.valueStr);
|
||||
if (ImGui::InputText("double[]", v, kTextBufferSize,
|
||||
ImGuiInputTextFlags_EnterReturnsTrue)) {
|
||||
if (auto outv = StringToDoubleArray(v))
|
||||
nt::SetEntryValue(entry.entry, std::move(outv));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case NT_STRING_ARRAY: {
|
||||
char* v = GetTextBuffer(entry.valueStr);
|
||||
if (ImGui::InputText("string[]", v, kTextBufferSize,
|
||||
ImGuiInputTextFlags_EnterReturnsTrue)) {
|
||||
if (auto outv = StringToStringArray(v))
|
||||
nt::SetEntryValue(entry.entry, std::move(outv));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case NT_RAW:
|
||||
ImGui::LabelText("raw", "[...]");
|
||||
break;
|
||||
case NT_RPC:
|
||||
ImGui::LabelText("rpc", "[...]");
|
||||
break;
|
||||
default:
|
||||
ImGui::LabelText("other", "?");
|
||||
break;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
static void EmitEntry(NetworkTablesModel::Entry& entry, const char* name,
|
||||
NetworkTablesFlags flags) {
|
||||
if (entry.source) {
|
||||
ImGui::Selectable(name);
|
||||
entry.source->EmitDrag();
|
||||
} else {
|
||||
ImGui::Text("%s", name);
|
||||
}
|
||||
ImGui::NextColumn();
|
||||
|
||||
if (flags & NetworkTablesFlags_ReadOnly)
|
||||
EmitEntryValueReadonly(entry);
|
||||
else
|
||||
EmitEntryValueEditable(entry);
|
||||
ImGui::NextColumn();
|
||||
|
||||
if (flags & NetworkTablesFlags_ShowFlags) {
|
||||
if ((entry.flags & NT_PERSISTENT) != 0)
|
||||
ImGui::Text("Persistent");
|
||||
else if (entry.flags != 0)
|
||||
ImGui::Text("%02x", entry.flags);
|
||||
ImGui::NextColumn();
|
||||
}
|
||||
|
||||
if (flags & NetworkTablesFlags_ShowTimestamp) {
|
||||
if (entry.value)
|
||||
ImGui::Text("%" PRIu64, entry.value->last_change());
|
||||
else
|
||||
ImGui::TextUnformatted("");
|
||||
ImGui::NextColumn();
|
||||
}
|
||||
ImGui::Separator();
|
||||
}
|
||||
|
||||
static void EmitTree(const std::vector<NetworkTablesModel::TreeNode>& tree,
|
||||
NetworkTablesFlags flags) {
|
||||
for (auto&& node : tree) {
|
||||
if (node.entry) {
|
||||
EmitEntry(*node.entry, node.name.c_str(), flags);
|
||||
}
|
||||
|
||||
if (!node.children.empty()) {
|
||||
bool open =
|
||||
TreeNodeEx(node.name.c_str(), ImGuiTreeNodeFlags_SpanFullWidth);
|
||||
ImGui::NextColumn();
|
||||
ImGui::NextColumn();
|
||||
if (flags & NetworkTablesFlags_ShowFlags) ImGui::NextColumn();
|
||||
if (flags & NetworkTablesFlags_ShowTimestamp) ImGui::NextColumn();
|
||||
ImGui::Separator();
|
||||
if (open) {
|
||||
EmitTree(node.children, flags);
|
||||
TreePop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void glass::DisplayNetworkTables(NetworkTablesModel* model,
|
||||
NetworkTablesFlags flags) {
|
||||
auto inst = model->GetInstance();
|
||||
|
||||
if (flags & NetworkTablesFlags_ShowConnections) {
|
||||
if (CollapsingHeader("Connections")) {
|
||||
ImGui::Columns(4, "connections");
|
||||
ImGui::Text("Id");
|
||||
ImGui::NextColumn();
|
||||
ImGui::Text("Address");
|
||||
ImGui::NextColumn();
|
||||
ImGui::Text("Updated");
|
||||
ImGui::NextColumn();
|
||||
ImGui::Text("Proto");
|
||||
ImGui::NextColumn();
|
||||
ImGui::Separator();
|
||||
for (auto&& i : nt::GetConnections(inst)) {
|
||||
ImGui::Text("%s", i.remote_id.c_str());
|
||||
ImGui::NextColumn();
|
||||
ImGui::Text("%s", i.remote_ip.c_str());
|
||||
ImGui::NextColumn();
|
||||
ImGui::Text("%llu",
|
||||
static_cast<unsigned long long>( // NOLINT(runtime/int)
|
||||
i.last_update));
|
||||
ImGui::NextColumn();
|
||||
ImGui::Text("%d.%d", i.protocol_version >> 8,
|
||||
i.protocol_version & 0xff);
|
||||
ImGui::NextColumn();
|
||||
}
|
||||
ImGui::Columns();
|
||||
}
|
||||
|
||||
if (!CollapsingHeader("Values", ImGuiTreeNodeFlags_DefaultOpen)) return;
|
||||
}
|
||||
|
||||
const bool showFlags = (flags & NetworkTablesFlags_ShowFlags);
|
||||
const bool showTimestamp = (flags & NetworkTablesFlags_ShowTimestamp);
|
||||
|
||||
static bool first = true;
|
||||
ImGui::Columns(2 + (showFlags ? 1 : 0) + (showTimestamp ? 1 : 0), "values");
|
||||
if (first) ImGui::SetColumnWidth(-1, 0.5f * ImGui::GetWindowWidth());
|
||||
ImGui::Text("Name");
|
||||
ImGui::NextColumn();
|
||||
ImGui::Text("Value");
|
||||
ImGui::NextColumn();
|
||||
if (showFlags) {
|
||||
if (first) ImGui::SetColumnWidth(-1, 12 * ImGui::GetFontSize());
|
||||
ImGui::Text("Flags");
|
||||
ImGui::NextColumn();
|
||||
}
|
||||
if (showTimestamp) {
|
||||
ImGui::Text("Changed");
|
||||
ImGui::NextColumn();
|
||||
}
|
||||
ImGui::Separator();
|
||||
first = false;
|
||||
|
||||
if (flags & NetworkTablesFlags_TreeView) {
|
||||
EmitTree(model->GetTreeRoot(), flags);
|
||||
} else {
|
||||
for (auto entry : model->GetEntries()) {
|
||||
EmitEntry(*entry, entry->name.c_str(), flags);
|
||||
}
|
||||
}
|
||||
ImGui::Columns();
|
||||
}
|
||||
|
||||
void NetworkTablesView::Display() {
|
||||
if (ImGui::BeginPopupContextItem()) {
|
||||
auto& storage = GetStorage();
|
||||
auto pTreeView = storage.GetBoolRef(
|
||||
"tree", m_defaultFlags & NetworkTablesFlags_TreeView);
|
||||
auto pShowConnections = storage.GetBoolRef(
|
||||
"connections", m_defaultFlags & NetworkTablesFlags_ShowConnections);
|
||||
auto pShowFlags = storage.GetBoolRef(
|
||||
"flags", m_defaultFlags & NetworkTablesFlags_ShowFlags);
|
||||
auto pShowTimestamp = storage.GetBoolRef(
|
||||
"timestamp", m_defaultFlags & NetworkTablesFlags_ShowTimestamp);
|
||||
|
||||
ImGui::MenuItem("Tree View", "", pTreeView);
|
||||
ImGui::MenuItem("Show Connections", "", pShowConnections);
|
||||
ImGui::MenuItem("Show Flags", "", pShowFlags);
|
||||
ImGui::MenuItem("Show Timestamp", "", pShowTimestamp);
|
||||
|
||||
m_flags &=
|
||||
~(NetworkTablesFlags_TreeView | NetworkTablesFlags_ShowConnections |
|
||||
NetworkTablesFlags_ShowFlags | NetworkTablesFlags_ShowTimestamp);
|
||||
m_flags |= (*pTreeView ? NetworkTablesFlags_TreeView : 0) |
|
||||
(*pShowConnections ? NetworkTablesFlags_ShowConnections : 0) |
|
||||
(*pShowFlags ? NetworkTablesFlags_ShowFlags : 0) |
|
||||
(*pShowTimestamp ? NetworkTablesFlags_ShowTimestamp : 0);
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
DisplayNetworkTables(m_model, m_flags);
|
||||
}
|
||||
22
glass/src/libnt/native/cpp/NetworkTablesHelper.cpp
Normal file
22
glass/src/libnt/native/cpp/NetworkTablesHelper.cpp
Normal file
@@ -0,0 +1,22 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#include "glass/networktables/NetworkTablesHelper.h"
|
||||
|
||||
using namespace glass;
|
||||
|
||||
NetworkTablesHelper::NetworkTablesHelper(NT_Inst inst)
|
||||
: m_inst{inst}, m_poller{nt::CreateEntryListenerPoller(inst)} {}
|
||||
|
||||
NetworkTablesHelper::~NetworkTablesHelper() {
|
||||
nt::DestroyEntryListenerPoller(m_poller);
|
||||
}
|
||||
|
||||
bool NetworkTablesHelper::IsConnected() const {
|
||||
return nt::GetNetworkMode(m_inst) == NT_NET_MODE_SERVER ||
|
||||
nt::IsConnected(m_inst);
|
||||
}
|
||||
177
glass/src/libnt/native/cpp/NetworkTablesProvider.cpp
Normal file
177
glass/src/libnt/native/cpp/NetworkTablesProvider.cpp
Normal file
@@ -0,0 +1,177 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#include "glass/networktables/NetworkTablesProvider.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <ntcore_cpp.h>
|
||||
#include <wpi/SmallString.h>
|
||||
#include <wpigui.h>
|
||||
|
||||
using namespace glass;
|
||||
|
||||
NetworkTablesProvider::NetworkTablesProvider(const wpi::Twine& iniName)
|
||||
: NetworkTablesProvider{iniName, nt::GetDefaultInstance()} {}
|
||||
|
||||
NetworkTablesProvider::NetworkTablesProvider(const wpi::Twine& iniName,
|
||||
NT_Inst inst)
|
||||
: Provider{iniName + "Window"}, m_nt{inst}, m_typeCache{iniName} {
|
||||
m_nt.AddListener("", NT_NOTIFY_LOCAL | NT_NOTIFY_NEW | NT_NOTIFY_DELETE |
|
||||
NT_NOTIFY_IMMEDIATE);
|
||||
}
|
||||
|
||||
void NetworkTablesProvider::GlobalInit() {
|
||||
Provider::GlobalInit();
|
||||
wpi::gui::AddInit([this] { m_typeCache.Initialize(); });
|
||||
}
|
||||
|
||||
void NetworkTablesProvider::DisplayMenu() {
|
||||
wpi::SmallVector<wpi::StringRef, 6> path;
|
||||
wpi::SmallString<64> name;
|
||||
for (auto&& entry : m_viewEntries) {
|
||||
path.clear();
|
||||
wpi::StringRef{entry->name}.split(path, '/', -1, false);
|
||||
|
||||
bool fullDepth = true;
|
||||
int depth = 0;
|
||||
for (; depth < (static_cast<int>(path.size()) - 1); ++depth) {
|
||||
name = path[depth];
|
||||
if (!ImGui::BeginMenu(name.c_str())) {
|
||||
fullDepth = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (fullDepth) {
|
||||
bool visible = entry->window && entry->window->IsVisible();
|
||||
bool wasVisible = visible;
|
||||
// FIXME: enabled?
|
||||
// data is the last item, so is guaranteed to be null-terminated
|
||||
ImGui::MenuItem(path.back().data(), nullptr, &visible, true);
|
||||
if (!wasVisible && visible) {
|
||||
Show(entry.get(), entry->window);
|
||||
} else if (wasVisible && !visible && entry->window) {
|
||||
entry->window->SetVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
for (; depth > 0; --depth) ImGui::EndMenu();
|
||||
}
|
||||
}
|
||||
|
||||
void NetworkTablesProvider::Update() {
|
||||
Provider::Update();
|
||||
|
||||
// add/remove entries from NT changes
|
||||
for (auto&& event : m_nt.PollListener()) {
|
||||
// look for .type fields
|
||||
wpi::StringRef eventName{event.name};
|
||||
if (!eventName.endswith("/.type") || !event.value ||
|
||||
!event.value->IsString())
|
||||
continue;
|
||||
auto tableName = eventName.drop_back(6);
|
||||
|
||||
// only handle ones where we have a builder
|
||||
auto builderIt = m_typeMap.find(event.value->GetString());
|
||||
if (builderIt == m_typeMap.end()) continue;
|
||||
|
||||
if (event.flags & NT_NOTIFY_DELETE) {
|
||||
auto it = std::find_if(
|
||||
m_viewEntries.begin(), m_viewEntries.end(), [&](const auto& elem) {
|
||||
return static_cast<Entry*>(elem->modelEntry)->typeEntry ==
|
||||
event.entry;
|
||||
});
|
||||
if (it != m_viewEntries.end()) {
|
||||
m_viewEntries.erase(it);
|
||||
}
|
||||
} else if (event.flags & NT_NOTIFY_NEW) {
|
||||
GetOrCreateView(builderIt->second, event.entry, tableName);
|
||||
// cache the type
|
||||
m_typeCache[tableName].SetName(event.value->GetString());
|
||||
}
|
||||
}
|
||||
|
||||
// check for visible windows that need displays (typically this is due to
|
||||
// file loading)
|
||||
for (auto&& window : m_windows) {
|
||||
if (!window->IsVisible() || window->HasView()) continue;
|
||||
auto id = window->GetId();
|
||||
auto typeIt = m_typeCache.find(id);
|
||||
if (typeIt == m_typeCache.end()) continue;
|
||||
|
||||
// only handle ones where we have a builder
|
||||
auto builderIt = m_typeMap.find(typeIt->second.GetName());
|
||||
if (builderIt == m_typeMap.end()) continue;
|
||||
|
||||
auto entry = GetOrCreateView(
|
||||
builderIt->second, nt::GetEntry(m_nt.GetInstance(), id + "/.type"), id);
|
||||
if (entry) Show(entry, window.get());
|
||||
}
|
||||
}
|
||||
|
||||
void NetworkTablesProvider::Register(wpi::StringRef typeName,
|
||||
CreateModelFunc createModel,
|
||||
CreateViewFunc createView) {
|
||||
m_typeMap[typeName] = Builder{std::move(createModel), std::move(createView)};
|
||||
}
|
||||
|
||||
void NetworkTablesProvider::Show(ViewEntry* entry, Window* window) {
|
||||
// if there's already a window, just show it
|
||||
if (entry->window) {
|
||||
entry->window->SetVisible(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// get or create model
|
||||
if (!entry->modelEntry->model)
|
||||
entry->modelEntry->model =
|
||||
entry->modelEntry->createModel(m_nt.GetInstance(), entry->name.c_str());
|
||||
if (!entry->modelEntry->model) return;
|
||||
|
||||
// the window might exist and we're just not associated to it yet
|
||||
if (!window) window = GetOrAddWindow(entry->name, true);
|
||||
if (!window) return;
|
||||
entry->window = window;
|
||||
|
||||
// create view
|
||||
auto view = entry->createView(window, entry->modelEntry->model.get(),
|
||||
entry->name.c_str());
|
||||
if (!view) return;
|
||||
window->SetView(std::move(view));
|
||||
|
||||
entry->window->SetVisible(true);
|
||||
}
|
||||
|
||||
NetworkTablesProvider::ViewEntry* NetworkTablesProvider::GetOrCreateView(
|
||||
const Builder& builder, NT_Entry typeEntry, wpi::StringRef name) {
|
||||
// get view entry if it already exists
|
||||
auto viewIt = FindViewEntry(name);
|
||||
if (viewIt != m_viewEntries.end() && (*viewIt)->name == name) {
|
||||
// make sure typeEntry is set in model
|
||||
static_cast<Entry*>((*viewIt)->modelEntry)->typeEntry = typeEntry;
|
||||
return viewIt->get();
|
||||
}
|
||||
|
||||
// get or create model entry
|
||||
auto modelIt = FindModelEntry(name);
|
||||
if (modelIt != m_modelEntries.end() && (*modelIt)->name == name) {
|
||||
static_cast<Entry*>(modelIt->get())->typeEntry = typeEntry;
|
||||
} else {
|
||||
modelIt = m_modelEntries.emplace(
|
||||
modelIt, std::make_unique<Entry>(typeEntry, name, builder));
|
||||
}
|
||||
|
||||
// create new view entry
|
||||
viewIt = m_viewEntries.emplace(
|
||||
viewIt,
|
||||
std::make_unique<ViewEntry>(
|
||||
name, modelIt->get(), [](Model*, const char*) { return true; },
|
||||
builder.createView));
|
||||
|
||||
return viewIt->get();
|
||||
}
|
||||
73
glass/src/libnt/native/cpp/StandardNetworkTables.cpp
Normal file
73
glass/src/libnt/native/cpp/StandardNetworkTables.cpp
Normal file
@@ -0,0 +1,73 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#include "glass/networktables/NTDigitalInput.h"
|
||||
#include "glass/networktables/NTDigitalOutput.h"
|
||||
#include "glass/networktables/NTFMS.h"
|
||||
#include "glass/networktables/NTField2D.h"
|
||||
#include "glass/networktables/NTStringChooser.h"
|
||||
#include "glass/networktables/NetworkTablesProvider.h"
|
||||
|
||||
using namespace glass;
|
||||
|
||||
void glass::AddStandardNetworkTablesViews(NetworkTablesProvider& provider) {
|
||||
provider.Register(
|
||||
NTFMSModel::kType,
|
||||
[](NT_Inst inst, const char* path) {
|
||||
return std::make_unique<NTFMSModel>(inst, path);
|
||||
},
|
||||
[](Window* win, Model* model, const char*) {
|
||||
win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize);
|
||||
return MakeFunctionView(
|
||||
[=] { DisplayFMS(static_cast<FMSModel*>(model)); });
|
||||
});
|
||||
provider.Register(
|
||||
NTDigitalInputModel::kType,
|
||||
[](NT_Inst inst, const char* path) {
|
||||
return std::make_unique<NTDigitalInputModel>(inst, path);
|
||||
},
|
||||
[](Window* win, Model* model, const char*) {
|
||||
win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize);
|
||||
return MakeFunctionView([=] {
|
||||
DisplayDIO(static_cast<NTDigitalInputModel*>(model), 0, true);
|
||||
});
|
||||
});
|
||||
provider.Register(
|
||||
NTDigitalOutputModel::kType,
|
||||
[](NT_Inst inst, const char* path) {
|
||||
return std::make_unique<NTDigitalOutputModel>(inst, path);
|
||||
},
|
||||
[](Window* win, Model* model, const char*) {
|
||||
win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize);
|
||||
return MakeFunctionView([=] {
|
||||
DisplayDIO(static_cast<NTDigitalOutputModel*>(model), 0, true);
|
||||
});
|
||||
});
|
||||
provider.Register(
|
||||
NTField2DModel::kType,
|
||||
[](NT_Inst inst, const char* path) {
|
||||
return std::make_unique<NTField2DModel>(inst, path);
|
||||
},
|
||||
[=](Window* win, Model* model, const char* path) {
|
||||
win->SetDefaultPos(200, 200);
|
||||
win->SetDefaultSize(400, 200);
|
||||
win->SetPadding(0, 0);
|
||||
return std::make_unique<Field2DView>(
|
||||
static_cast<NTField2DModel*>(model));
|
||||
});
|
||||
provider.Register(
|
||||
NTStringChooserModel::kType,
|
||||
[](NT_Inst inst, const char* path) {
|
||||
return std::make_unique<NTStringChooserModel>(inst, path);
|
||||
},
|
||||
[](Window* win, Model* model, const char*) {
|
||||
win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize);
|
||||
return MakeFunctionView([=] {
|
||||
DisplayStringChooser(static_cast<NTStringChooserModel*>(model));
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user