Skip to content

Commit

Permalink
Add NetworkTable-selectable switched camera support (#117)
Browse files Browse the repository at this point in the history
Fixes #100.

Also expand the newest added camera.
  • Loading branch information
PeterJohnson committed Feb 19, 2019
1 parent e84e55c commit 9fe4460
Show file tree
Hide file tree
Showing 6 changed files with 577 additions and 141 deletions.
87 changes: 84 additions & 3 deletions deps/examples/cpp-multiCameraServer/main.cpp
Expand Up @@ -52,6 +52,14 @@
}
}
]
"switched cameras": [
{
"name": <virtual camera name>
"key": <network table key used for selection>
// if NT value is a string, it's treated as a name
// if NT value is a double, it's treated as an integer index
}
]
}
*/

Expand All @@ -69,7 +77,14 @@ struct CameraConfig {
wpi::json streamConfig;
};

struct SwitchedCameraConfig {
std::string name;
std::string key;
};

std::vector<CameraConfig> cameraConfigs;
std::vector<SwitchedCameraConfig> switchedCameraConfigs;
std::vector<cs::VideoSource> cameras;

wpi::raw_ostream& ParseError() {
return wpi::errs() << "config error in '" << configFile << "': ";
Expand Down Expand Up @@ -104,6 +119,30 @@ bool ReadCameraConfig(const wpi::json& config) {
return true;
}

bool ReadSwitchedCameraConfig(const wpi::json& config) {
SwitchedCameraConfig c;

// name
try {
c.name = config.at("name").get<std::string>();
} catch (const wpi::json::exception& e) {
ParseError() << "could not read switched camera name: " << e.what() << '\n';
return false;
}

// key
try {
c.key = config.at("key").get<std::string>();
} catch (const wpi::json::exception& e) {
ParseError() << "switched camera '" << c.name
<< "': could not read key: " << e.what() << '\n';
return false;
}

switchedCameraConfigs.emplace_back(std::move(c));
return true;
}

bool ReadConfig() {
// open config file
std::error_code ec;
Expand Down Expand Up @@ -164,6 +203,18 @@ bool ReadConfig() {
return false;
}

// switched cameras (optional)
if (j.count("switched cameras") != 0) {
try {
for (auto&& camera : j.at("switched cameras")) {
if (!ReadSwitchedCameraConfig(camera)) return false;
}
} catch (const wpi::json::exception& e) {
ParseError() << "could not read switched cameras: " << e.what() << '\n';
return false;
}
}

return true;
}

Expand All @@ -183,6 +234,34 @@ cs::UsbCamera StartCamera(const CameraConfig& config) {
return camera;
}

cs::MjpegServer StartSwitchedCamera(const SwitchedCameraConfig& config) {
wpi::outs() << "Starting switched camera '" << config.name << "' on "
<< config.key << '\n';
auto server =
frc::CameraServer::GetInstance()->AddSwitchedCamera(config.name);

nt::NetworkTableInstance::GetDefault()
.GetEntry(config.key)
.AddListener(
[server](const auto& event) mutable {
if (event.value->IsDouble()) {
int i = event.value->GetDouble();
if (i >= 0 && i < cameras.size()) server.SetSource(cameras[i]);
} else if (event.value->IsString()) {
auto str = event.value->GetString();
for (int i = 0; i < cameraConfigs.size(); ++i) {
if (str == cameraConfigs[i].name) {
server.SetSource(cameras[i]);
break;
}
}
}
},
NT_NOTIFY_IMMEDIATE | NT_NOTIFY_NEW | NT_NOTIFY_UPDATE);

return server;
}

// example pipeline
class MyPipeline : public frc::VisionPipeline {
public:
Expand Down Expand Up @@ -211,9 +290,11 @@ int main(int argc, char* argv[]) {
}

// start cameras
std::vector<cs::VideoSource> cameras;
for (auto&& cameraConfig : cameraConfigs)
cameras.emplace_back(StartCamera(cameraConfig));
for (const auto& config : cameraConfigs)
cameras.emplace_back(StartCamera(config));

// start switched cameras
for (const auto& config : switchedCameraConfigs) StartSwitchedCamera(config);

// start image processing on camera 0 if present
if (cameras.size() >= 1) {
Expand Down
92 changes: 89 additions & 3 deletions deps/examples/java-multiCameraServer/src/main/java/Main.java
Expand Up @@ -22,6 +22,7 @@
import edu.wpi.cscore.UsbCamera;
import edu.wpi.cscore.VideoSource;
import edu.wpi.first.cameraserver.CameraServer;
import edu.wpi.first.networktables.EntryListenerFlags;
import edu.wpi.first.networktables.NetworkTableInstance;
import edu.wpi.first.vision.VisionPipeline;
import edu.wpi.first.vision.VisionThread;
Expand Down Expand Up @@ -60,6 +61,14 @@
}
}
]
"switched cameras": [
{
"name": <virtual camera name>
"key": <network table key used for selection>
// if NT value is a string, it's treated as a name
// if NT value is a double, it's treated as an integer index
}
]
}
*/

Expand All @@ -74,9 +83,17 @@ public static class CameraConfig {
public JsonElement streamConfig;
}

@SuppressWarnings("MemberName")
public static class SwitchedCameraConfig {
public String name;
public String key;
};

public static int team;
public static boolean server;
public static List<CameraConfig> cameraConfigs = new ArrayList<>();
public static List<SwitchedCameraConfig> switchedCameraConfigs = new ArrayList<>();
public static List<VideoSource> cameras = new ArrayList<>();

private Main() {
}
Expand Down Expand Up @@ -119,6 +136,32 @@ public static boolean readCameraConfig(JsonObject config) {
return true;
}

/**
* Read single switched camera configuration.
*/
public static boolean readSwitchedCameraConfig(JsonObject config) {
SwitchedCameraConfig cam = new SwitchedCameraConfig();

// name
JsonElement nameElement = config.get("name");
if (nameElement == null) {
parseError("could not read switched camera name");
return false;
}
cam.name = nameElement.getAsString();

// path
JsonElement keyElement = config.get("key");
if (keyElement == null) {
parseError("switched camera '" + cam.name + "': could not read key");
return false;
}
cam.key = keyElement.getAsString();

switchedCameraConfigs.add(cam);
return true;
}

/**
* Read configuration file.
*/
Expand Down Expand Up @@ -173,6 +216,15 @@ public static boolean readConfig() {
}
}

if (obj.has("switched cameras")) {
JsonArray switchedCameras = obj.get("switched cameras").getAsJsonArray();
for (JsonElement camera : switchedCameras) {
if (!readSwitchedCameraConfig(camera.getAsJsonObject())) {
return false;
}
}
}

return true;
}

Expand All @@ -197,6 +249,36 @@ public static VideoSource startCamera(CameraConfig config) {
return camera;
}

/**
* Start running the switched camera.
*/
public static MjpegServer startSwitchedCamera(SwitchedCameraConfig config) {
System.out.println("Starting switched camera '" + config.name + "' on " + config.key);
MjpegServer server = CameraServer.getInstance().addSwitchedCamera(config.name);

NetworkTableInstance.getDefault()
.getEntry(config.key)
.addListener(event -> {
if (event.value.isDouble()) {
int i = (int) event.value.getDouble();
if (i >= 0 && i < cameras.size()) {
server.setSource(cameras.get(i));
}
} else if (event.value.isString()) {
String str = event.value.getString();
for (int i = 0; i < cameraConfigs.size(); i++) {
if (str.equals(cameraConfigs.get(i).name)) {
server.setSource(cameras.get(i));
break;
}
}
}
},
EntryListenerFlags.kImmediate | EntryListenerFlags.kNew | EntryListenerFlags.kUpdate);

return server;
}

/**
* Example pipeline.
*/
Expand Down Expand Up @@ -233,9 +315,13 @@ public static void main(String... args) {
}

// start cameras
List<VideoSource> cameras = new ArrayList<>();
for (CameraConfig cameraConfig : cameraConfigs) {
cameras.add(startCamera(cameraConfig));
for (CameraConfig config : cameraConfigs) {
cameras.add(startCamera(config));
}

// start switched cameras
for (SwitchedCameraConfig config : switchedCameraConfigs) {
startSwitchedCamera(config);
}

// start image processing on camera 0 if present
Expand Down
71 changes: 68 additions & 3 deletions deps/examples/python-multiCameraServer/multiCameraServer.py
Expand Up @@ -12,6 +12,7 @@

from cscore import CameraServer, VideoSource, UsbCamera, MjpegServer
from networktables import NetworkTablesInstance
import ntcore

# JSON format:
# {
Expand Down Expand Up @@ -44,6 +45,14 @@
# }
# }
# ]
# "switched cameras": [
# {
# "name": <virtual camera name>
# "key": <network table key used for selection>
# // if NT value is a string, it's treated as a name
# // if NT value is a double, it's treated as an integer index
# }
# ]
# }

configFile = "/boot/frc.json"
Expand All @@ -53,6 +62,8 @@ class CameraConfig: pass
team = None
server = False
cameraConfigs = []
switchedCameraConfigs = []
cameras = []

def parseError(str):
"""Report parse error."""
Expand Down Expand Up @@ -84,6 +95,27 @@ def readCameraConfig(config):
cameraConfigs.append(cam)
return True

def readSwitchedCameraConfig(config):
"""Read single switched camera configuration."""
cam = CameraConfig()

# name
try:
cam.name = config["name"]
except KeyError:
parseError("could not read switched camera name")
return False

# path
try:
cam.key = config["key"]
except KeyError:
parseError("switched camera '{}': could not read key".format(cam.name))
return False

switchedCameraConfigs.append(cam)
return True

def readConfig():
"""Read configuration file."""
global team
Expand Down Expand Up @@ -129,6 +161,12 @@ def readConfig():
if not readCameraConfig(camera):
return False

# switched cameras
if "switched cameras" in j:
for camera in j["switched cameras"]:
if not readSwitchedCameraConfig(camera):
return False

return True

def startCamera(config):
Expand All @@ -146,6 +184,30 @@ def startCamera(config):

return camera

def startSwitchedCamera(config):
"""Start running the switched camera."""
print("Starting switched camera '{}' on {}".format(config.name, config.key))
server = CameraServer.getInstance().addSwitchedCamera(config.name)

def listener(fromobj, key, value, isNew):
if isinstance(value, float):
i = int(value)
if i >= 0 and i < len(cameras):
server.setSource(cameras[i])
elif isinstance(value, str):
for i in range(len(cameraConfigs)):
if value == cameraConfigs[i].name:
server.setSource(cameras[i])
break

NetworkTablesInstance.getDefault().getEntry(config.key).addListener(
listener,
ntcore.constants.NT_NOTIFY_IMMEDIATE |
ntcore.constants.NT_NOTIFY_NEW |
ntcore.constants.NT_NOTIFY_UPDATE)

return server

if __name__ == "__main__":
if len(sys.argv) >= 2:
configFile = sys.argv[1]
Expand All @@ -164,9 +226,12 @@ def startCamera(config):
ntinst.startClientTeam(team)

# start cameras
cameras = []
for cameraConfig in cameraConfigs:
cameras.append(startCamera(cameraConfig))
for config in cameraConfigs:
cameras.append(startCamera(config))

# start switched cameras
for config in switchedCameraConfigs:
startSwitchedCamera(config)

# loop forever
while True:
Expand Down

0 comments on commit 9fe4460

Please sign in to comment.