Running multiCameraServer.py on PC instead of RPi

We are looking at using a small PC instead of an RPi to connect the driver’s cameras so I was looking at the Rpi cameraserver code.

on the PC:
python v3.11.0 installed
pip install robotpy[cscore]
python .\multiCameraServer.py frc.json

Using robot simulation on the PC for initial testing, I start our robot sim using VSCode using real-drivers station, the wait for the Robot Simulation panel to come up. Next, I start the cameraServer in client mode with the python command above.

Camera light comes on, and I see the client connection in the Sim panel’s NetworkTable Info:

The problem is I don’t see anything on SmartDashboard when I add a camera steam or mjpg viewer.

With ShuffleBoard there is “no data available” under the camera server.

frc.json:

    "team": 2202,
    "ntmode": "client",
    "cameras": [
        {
            "name": "UVC Camera",
            "path":0,
            "pixel format": "mjpeg",
            "width": 320,
            "height": 240,
            "fps": 30            
        }
    ]
}

Anyone know if what I’m trying is even possible? My camera is a web-cam, but I was able to use it with openCv just fine.

Thanks in advance for any pointers,
Derek

It would be helpful to see the contents of multiCameraServer.py–there’s been a few different versions of it. In particular, it probably needs a workaround for [cscore] AddListener immediate notifications aren't implemented · Issue #5055 · wpilibsuite/allwpilib · GitHub . One was added in Workaround wpilibsuite/allwpilib#5055 · wpilibsuite/WPILibPi@acac6a6 · GitHub , but this workaround no longer works with the 2024 WPILib release–that function was removed, so the new workaround is CameraServer.removeCamera("unused").

Here’s the code I’m using. There was no CameraServer.setSize(), so that was commented out.

Thanks for the quick reply.

#!/usr/bin/env python3

# Copyright (c) FIRST and other WPILib contributors.
# Open Source Software; you can modify and/or share it under the terms of
# the WPILib BSD license file in the root directory of this project.

import json
import os
import time
import sys

from cscore import CameraServer, VideoSource, UsbCamera, MjpegServer
from ntcore import NetworkTableInstance, EventFlags

##JSON template removed for brevity

configFile = "frc.json"

class CameraConfig:
    pass

team:int = 0
server = False
cameraConfigs = []
switchedCameraConfigs = []
cameras = []

def parseError(str):
    """Report parse error."""
    print("config error in '" + configFile + "': " + str, file=sys.stderr)

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

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

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

    # stream properties
    cam.streamConfig = config.get("stream")

    cam.config = 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
    global server
    #global configFile

    # parse file
    try:
        print(os.getcwd())
        dir_path = os.path.dirname(os.path.realpath(__file__))
        print(dir_path)
        #configFile = os.path.join(dir_path, configFile)
        with open(configFile, "rt", encoding="utf-8") as f:
            j = json.load(f)
    except OSError as err:
        print("could not open '{}': {}".format(configFile, err), file=sys.stderr)
        return False

    # top level must be an object
    if not isinstance(j, dict):
        parseError("must be JSON object")
        return False

    # team number
    try:
        team = j["team"]
    except KeyError:
        parseError("could not read team number")
        return False

    # ntmode (optional)
    if "ntmode" in j:
        str = j["ntmode"]
        if str.lower() == "client":
            server = False
        elif str.lower() == "server":
            server = True
        else:
            parseError("could not understand ntmode value '{}'".format(str))

    # cameras
    try:
        cameras = j["cameras"]
    except KeyError:
        parseError("could not read cameras")
        return False
    for camera in cameras:
        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):
    """Start running the camera."""
    print("Starting camera '{}' on {}".format(config.name, config.path))
    camera = UsbCamera(config.name, config.path)
    server = CameraServer.startAutomaticCapture(camera=camera)

    camera.setConfigJson(json.dumps(config.config))
    camera.setConnectionStrategy(VideoSource.ConnectionStrategy.kConnectionKeepOpen)

    if config.streamConfig is not None:
        server.setConfigJson(json.dumps(config.streamConfig))

    return camera

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

    def listener(event):
        data = event.data
        if data is not None:
            value = data.value.value()
            if isinstance(value, int):
                if value >= 0 and value < len(cameras):
                    server.setSource(cameras[value])
            elif 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

    NetworkTableInstance.getDefault().addListener(
        NetworkTableInstance.getDefault().getEntry(config.key),
        EventFlags.kImmediate | EventFlags.kValueAll,
        listener)

    return server

if __name__ == "__main__":
    print("working directory:" + os.getcwd())
    if len(sys.argv) >= 2:
        configFile = sys.argv[1]

    #List cameras
    mycameras = UsbCamera.enumerateUsbCameras()
    for c in mycameras:
        print("name:", c.name,"path:", c.path)

    # read configuration
    if not readConfig():
        sys.exit(1)

    # start NetworkTables
    ntinst = NetworkTableInstance.getDefault()
    if server:
        print("Setting up NetworkTables server")
        ntinst.startServer()
    else:
        print("Setting up NetworkTables client for team {}".format(team))
        ntinst.startClient4("wpilibpi")
        ntinst.setServerTeam(team)
        ntinst.startDSClient()

    # start cameras
    # work around wpilibsuite/allwpilib#5055
    ###dpl api change?  
    ###CameraServer.setSize(CameraServer.kSize160x120)
    for config in cameraConfigs:
        cameras.append(startCamera(config))

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

    # loop forever
    while True:
        time.sleep(10)

In case it helps, here are the python libs:

pip list
Package                  Version
------------------------ ----------
asttokens                2.4.1     
bcrypt                   4.1.2     
certifi                  2024.2.2  
cffi                     1.16.0    
charset-normalizer       3.3.2     
click                    8.1.7     
cloudpickle              3.0.0
colorama                 0.4.6
comm                     0.2.1
comtypes                 1.3.0
cryptography             42.0.4
debugpy                  1.8.1
decorator                5.1.1
ecdsa                    0.18.0
executing                2.0.1
gorilla                  0.4.0
idna                     3.6
imagezmq                 1.1.1
importlib-metadata       7.0.1
imutils                  0.5.4
iniconfig                2.0.0
ipykernel                6.29.2
ipython                  8.21.0
jedi                     0.19.1
jupyter_client           8.6.0
jupyter_core             5.7.1
jwcrypto                 1.5.4
matplotlib-inline        0.1.6
nest-asyncio             1.6.0
numpy                    1.26.4
opencv-contrib-python    4.9.0.80
packaging                23.2
pandas                   2.2.0
paramiko                 3.4.0
parso                    0.8.3
Pint                     0.23
pip                      24.0
platformdirs             4.2.0
pluggy                   1.4.0
prompt-toolkit           3.0.43
psutil                   5.9.8
pure-eval                0.2.2
pyasn1                   0.5.1
pycparser                2.21
pyfrc                    2024.0.1
Pygments                 2.17.2
pygrabber                0.2
PyNaCl                   1.5.0
pynetconsole             2.0.4
pyntcore                 2024.3.1.0
pytest                   8.0.1
pytest-reraise           2.1.2
python-dateutil          2.8.2
python-jose              3.3.0
pytz                     2024.1
pywin32                  306
pyzmq                    25.1.2
requests                 2.31.0
requests-futures         1.0.1
requests-toolbelt        1.0.0
robotpy                  2024.3.1.0
robotpy-cli              2024.0.0
robotpy-cscore           2024.3.1.0
robotpy-hal              2024.3.1.0
robotpy-halsim-gui       2024.3.1.0
robotpy-installer        2024.2.2
robotpy-wpilib-utilities 2024.0.0
robotpy-wpimath          2024.3.1.0
robotpy-wpinet           2024.3.1.0
robotpy-wpiutil          2024.3.1.0
rsa                      4.9
setuptools               65.5.0
six                      1.16.0
stack-data               0.6.3
tomli                    2.0.1
tomlkit                  0.12.3
tornado                  6.4
traitlets                5.14.1
typing_extensions        4.9.0
tzdata                   2024.1
urllib3                  2.2.1
wcwidth                  0.2.13
wpilib                   2024.3.1.0
zipp                     3.17.0

Ok, have you tried adding the removeCamera() call in that location?

On a hunch, I made a small change to startCamera(config):

    #server = CameraServer.startAutomaticCapture(camera=camera)
    server = CameraServer.startAutomaticCapture(config.path)

where config.path = 0 for my camera. This seems to start the camera stream and now shows in Shuffleboard & Smart Dashboard.

The commented line does not start the stream.

I get one error on the output of the python script, but it continues to stream:

Setting up NetworkTables client for team 2202
Starting camera 'UVC Camera' on 0
CS: UVC Camera: Attempting to connect to USB camera on \\?\usb#vid_fefe&pid_4321&mi_00#b&1577f843&1&0000#{e5323777-f976-4f5b-9b55-b94699c46e44}\global
CS: USB Camera 0: Attempting to connect to USB camera on \\?\usb#vid_fefe&pid_4321&mi_00#b&1577f843&1&0000#{e5323777-f976-4f5b-9b55-b94699c46e44}\global
CS: UVC Camera: Connected to USB camera on \\?\usb#vid_fefe&pid_4321&mi_00#b&1577f843&1&0000#{e5323777-f976-4f5b-9b55-b94699c46e44}\global
CS: USB Camera 0: Connected to USB camera on \\?\usb#vid_fefe&pid_4321&mi_00#b&1577f843&1&0000#{e5323777-f976-4f5b-9b55-b94699c46e44}\global
CS: UVC Camera: SetConfigJson: setting video mode to pixelFormat 1, width 320, height 240, fps 30
Source Reader error: 0xC00D3EA3

I’m not following your request on “… tried removeCamera()”, which location?

Ok, I added the removeCamera call after the config loop and keeping orignal code. That seems to work.

def startCamera(config):
    """Start running the camera."""
    print("Starting camera '{}' on {}".format(config.name, config.path))
    camera = UsbCamera(config.name, config.path)
    server = CameraServer.startAutomaticCapture(camera=camera)
    ###server = CameraServer.startAutomaticCapture(config.path)

    camera.setConfigJson(json.dumps(config.config))
    camera.setConnectionStrategy(VideoSource.ConnectionStrategy.kConnectionKeepOpen)

    if config.streamConfig is not None:
        server.setConfigJson(json.dumps(config.streamConfig))
    return camera

Call to remove at end:

# start cameras
    # work around wpilibsuite/allwpilib#5055
    ###CameraServer.setSize(CameraServer.kSize160x120)
    for config in cameraConfigs:
        cameras.append(startCamera(config))
    
    CameraServer.removeCamera("unused")

This seems to work and removes the “Source Reader error”.