mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-22 01:11:40 +00:00
@@ -129,6 +129,7 @@ public class Main {
|
||||
}
|
||||
|
||||
// Attempt to load the JNI Libraries
|
||||
System.out.println("Loading CameraServer...");
|
||||
try {
|
||||
CameraServerJNI.forceLoad();
|
||||
CameraServerCvJNI.forceLoad();
|
||||
@@ -139,9 +140,11 @@ public class Main {
|
||||
throw new RuntimeException("Failed to load JNI Libraries!");
|
||||
}
|
||||
|
||||
System.out.println("Checking Settings...");
|
||||
ConfigManager.initializeSettings();
|
||||
|
||||
if (!CurrentPlatform.isWindows()) {
|
||||
System.out.println("Initializing Script Manager...");
|
||||
ScriptManager.initialize();
|
||||
} else {
|
||||
System.out.println("Scripts not yet supported on Windows. ScriptEvents will be ignored.");
|
||||
@@ -159,16 +162,17 @@ public class Main {
|
||||
|
||||
boolean visionSourcesOk = VisionManager.initializeSources();
|
||||
if (!visionSourcesOk) {
|
||||
System.out.println("No cameras connected!");
|
||||
System.err.println("No cameras connected!");
|
||||
return;
|
||||
}
|
||||
|
||||
boolean visionProcessesOk = VisionManager.initializeProcesses();
|
||||
if (!visionProcessesOk) {
|
||||
System.err.println("Failed to start threads!");
|
||||
System.err.println("Failed to initialize vision processes!");
|
||||
return;
|
||||
}
|
||||
|
||||
System.out.println("Starting vision processes...");
|
||||
VisionManager.startProcesses();
|
||||
|
||||
System.out.printf("Starting Web server at port %d\n", uiPort);
|
||||
|
||||
@@ -84,7 +84,7 @@ public class CameraConfig {
|
||||
|
||||
void saveConfig(CameraJsonConfig config) {
|
||||
try {
|
||||
JacksonHelper.serializer(configPath, config);
|
||||
JacksonHelper.serializer(configPath, config, true);
|
||||
FileHelper.setFilePerms(configPath);
|
||||
} catch (IOException e) {
|
||||
System.err.println("Failed to save camera config file: " + configPath.toString());
|
||||
@@ -97,7 +97,7 @@ public class CameraConfig {
|
||||
|
||||
public void saveDriverMode(CVPipelineSettings driverMode) {
|
||||
try {
|
||||
JacksonHelper.serializer(driverModePath, driverMode);
|
||||
JacksonHelper.serializer(driverModePath, driverMode, true);
|
||||
FileHelper.setFilePerms(driverModePath);
|
||||
} catch (IOException e) {
|
||||
System.err.println("Failed to save camera drivermode file: " + driverModePath.toString());
|
||||
@@ -108,7 +108,7 @@ public class CameraConfig {
|
||||
public void saveCalibration(List<CameraCalibrationConfig> cal) {
|
||||
CameraCalibrationConfig[] configs = cal.toArray(new CameraCalibrationConfig[0]);
|
||||
try {
|
||||
JacksonHelper.serializer(calibrationPath, configs);
|
||||
JacksonHelper.serializer(calibrationPath, configs, true);
|
||||
FileHelper.setFilePerms(calibrationPath);
|
||||
} catch (IOException e) {
|
||||
System.err.println("Failed to save camera calibration file: " + calibrationPath.toString());
|
||||
@@ -122,7 +122,7 @@ public class CameraConfig {
|
||||
System.err.println("Failed to create camera config folder: " + configFolderPath.toString());
|
||||
}
|
||||
FileHelper.setFilePerms(configFolderPath);
|
||||
} catch(Exception e) {
|
||||
} catch (Exception e) {
|
||||
System.err.println("Failed to create camera config folder: " + configFolderPath.toString());
|
||||
}
|
||||
}
|
||||
@@ -131,7 +131,7 @@ public class CameraConfig {
|
||||
private void checkConfig() {
|
||||
if (!configExists()) {
|
||||
try {
|
||||
JacksonHelper.serializer(configPath, preliminaryConfig);
|
||||
JacksonHelper.serializer(configPath, preliminaryConfig, true);
|
||||
FileHelper.setFilePerms(configPath);
|
||||
} catch (IOException e) {
|
||||
System.err.println("Failed to create camera config file: " + configPath.toString());
|
||||
@@ -144,7 +144,7 @@ public class CameraConfig {
|
||||
try {
|
||||
CVPipelineSettings newDriverModeSettings = new CVPipelineSettings();
|
||||
newDriverModeSettings.nickname = "DRIVERMODE";
|
||||
JacksonHelper.serializer(driverModePath, newDriverModeSettings);
|
||||
JacksonHelper.serializer(driverModePath, newDriverModeSettings, true);
|
||||
FileHelper.setFilePerms(driverModePath);
|
||||
} catch (IOException e) {
|
||||
System.err.println("Failed to create camera drivermode file: " + driverModePath.toString());
|
||||
@@ -156,7 +156,7 @@ public class CameraConfig {
|
||||
if (!calibrationExists()) {
|
||||
try {
|
||||
List<CameraCalibrationConfig> calibrations = new ArrayList<>();
|
||||
JacksonHelper.serializer(calibrationPath, calibrations.toArray());
|
||||
JacksonHelper.serializer(calibrationPath, calibrations.toArray(), true);
|
||||
} catch (IOException e) {
|
||||
System.err.println("Failed to create camera calibration file: " + calibrationPath.toString());
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
|
||||
public class ConfigManager {
|
||||
private ConfigManager() {}
|
||||
private ConfigManager() {
|
||||
}
|
||||
|
||||
public static final Path SettingsPath = Paths.get(ProgramDirectoryUtilities.getProgramDirectory(), "settings");
|
||||
private static final Path settingsFilePath = Paths.get(SettingsPath.toString(), "settings.json");
|
||||
@@ -22,13 +23,18 @@ public class ConfigManager {
|
||||
|
||||
public static GeneralSettings settings = new GeneralSettings();
|
||||
|
||||
private static boolean settingsFolderExists() { return Files.exists(SettingsPath); }
|
||||
private static boolean settingsFileExists() { return settingsFolderExists() && Files.exists(settingsFilePath); }
|
||||
private static boolean settingsFolderExists() {
|
||||
return Files.exists(SettingsPath);
|
||||
}
|
||||
|
||||
private static boolean settingsFileExists() {
|
||||
return settingsFolderExists() && Files.exists(settingsFilePath);
|
||||
}
|
||||
|
||||
private static void checkSettingsFolder() {
|
||||
if (!settingsFolderExists()) {
|
||||
try {
|
||||
if( !(new File(SettingsPath.toUri()).mkdirs()) ) {
|
||||
if (!(new File(SettingsPath.toUri()).mkdirs())) {
|
||||
System.err.println("Failed to create settings folder: " + SettingsPath.toString());
|
||||
}
|
||||
Files.createDirectory(SettingsPath);
|
||||
@@ -36,7 +42,7 @@ public class ConfigManager {
|
||||
new ShellExec().executeBashCommand("sudo chmod -R 0777 " + SettingsPath.toString());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
if(!(e instanceof java.nio.file.FileAlreadyExistsException))
|
||||
if (!(e instanceof java.nio.file.FileAlreadyExistsException))
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
@@ -46,7 +52,7 @@ public class ConfigManager {
|
||||
boolean settingsFileEmpty = settingsFileExists() && new File(settingsFilePath.toString()).length() == 0;
|
||||
if (settingsFileEmpty || !settingsFileExists()) {
|
||||
try {
|
||||
JacksonHelper.serializer(settingsFilePath, settings);
|
||||
JacksonHelper.serializer(settingsFilePath, settings, true);
|
||||
FileHelper.setFilePerms(settingsFilePath);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
@@ -69,7 +75,7 @@ public class ConfigManager {
|
||||
|
||||
private static void saveSettingsFile() {
|
||||
try {
|
||||
JacksonHelper.serializer(settingsFilePath, settings);
|
||||
JacksonHelper.serializer(settingsFilePath, settings, true);
|
||||
FileHelper.setFilePerms(settingsFilePath);
|
||||
} catch (IOException e) {
|
||||
System.err.println("Failed to save settings.json!");
|
||||
|
||||
@@ -21,6 +21,7 @@ public class PipelineConfig {
|
||||
|
||||
/**
|
||||
* Construct a new PipelineConfig
|
||||
*
|
||||
* @param cameraConfig the CameraConfig (parent folder, kinda?)
|
||||
*/
|
||||
PipelineConfig(CameraConfig cameraConfig) {
|
||||
@@ -28,7 +29,7 @@ public class PipelineConfig {
|
||||
}
|
||||
|
||||
private void checkFolder() {
|
||||
if ( !(new File(cameraConfig.pipelineFolderPath.toUri()).mkdirs())) {
|
||||
if (!(new File(cameraConfig.pipelineFolderPath.toUri()).mkdirs())) {
|
||||
if (Files.notExists(cameraConfig.pipelineFolderPath)) {
|
||||
System.err.println("Failed to create pipelines folder.");
|
||||
}
|
||||
@@ -46,7 +47,7 @@ public class PipelineConfig {
|
||||
|
||||
private boolean folderHasPipelines() {
|
||||
File[] folderContents = getPipelineFiles();
|
||||
if(folderContents == null) return false;
|
||||
if (folderContents == null) return false;
|
||||
return folderContents.length > 0;
|
||||
}
|
||||
|
||||
@@ -75,14 +76,14 @@ public class PipelineConfig {
|
||||
|
||||
if (settings instanceof StandardCVPipelineSettings) {
|
||||
try {
|
||||
JacksonHelper.serialize(path, (StandardCVPipelineSettings)settings, StandardCVPipelineSettings.class, new StandardCVPipelineSettingsSerializer());
|
||||
JacksonHelper.serialize(path, (StandardCVPipelineSettings) settings, StandardCVPipelineSettings.class, new StandardCVPipelineSettingsSerializer(), true);
|
||||
FileHelper.setFilePerms(path);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
JacksonHelper.serializer(path, settings);
|
||||
JacksonHelper.serializer(path, settings, true);
|
||||
FileHelper.setFilePerms(path);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
@@ -91,13 +92,13 @@ public class PipelineConfig {
|
||||
}
|
||||
|
||||
public void save(List<CVPipelineSettings> settings) {
|
||||
for(CVPipelineSettings setting : settings) {
|
||||
for (CVPipelineSettings setting : settings) {
|
||||
save(setting);
|
||||
}
|
||||
}
|
||||
|
||||
public void delete(CVPipelineSettings setting) {
|
||||
if(pipelineExists(setting)) {
|
||||
if (pipelineExists(setting)) {
|
||||
try {
|
||||
Files.delete(getPipelinePath(setting));
|
||||
} catch (IOException e) {
|
||||
@@ -124,17 +125,17 @@ public class PipelineConfig {
|
||||
File[] pipelineFiles = getPipelineFiles();
|
||||
List<CVPipelineSettings> deserializedList = new ArrayList<>();
|
||||
|
||||
if(pipelineFiles == null || pipelineFiles.length < 1) {
|
||||
if (pipelineFiles == null || pipelineFiles.length < 1) {
|
||||
// TODO handle no pipelines to load
|
||||
System.err.println("no pipes to load! loading default");
|
||||
} else {
|
||||
for(File pipelineFile : pipelineFiles) {
|
||||
try {
|
||||
var pipe = JacksonHelper.deserialize(Paths.get(pipelineFile.getPath()), StandardCVPipelineSettings.class, new StandardCVPipelineSettingsDeserializer());
|
||||
deserializedList.add(pipe);
|
||||
} catch (IOException e) {
|
||||
System.err.println("couldn't load cvpipeline2d");
|
||||
}
|
||||
for (File pipelineFile : pipelineFiles) {
|
||||
try {
|
||||
var pipe = JacksonHelper.deserialize(Paths.get(pipelineFile.getPath()), StandardCVPipelineSettings.class, new StandardCVPipelineSettingsDeserializer());
|
||||
deserializedList.add(pipe);
|
||||
} catch (IOException e) {
|
||||
System.err.println("couldn't load cvpipeline2d");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ public class NetworkTablesManager {
|
||||
|
||||
private NetworkTablesManager() {}
|
||||
|
||||
private static final NetworkTableInstance NTInst = NetworkTableInstance.getDefault();
|
||||
private static final NetworkTableInstance ntInstance = NetworkTableInstance.getDefault();
|
||||
|
||||
public static final String kRootTableName = "/chameleon-vision";
|
||||
public static final NetworkTable kRootTable = NetworkTableInstance.getDefault().getTable(kRootTableName);
|
||||
@@ -48,11 +48,16 @@ public class NetworkTablesManager {
|
||||
public static void setClientMode(String host) {
|
||||
isServer = false;
|
||||
System.out.println("Starting NT Client");
|
||||
NTInst.stopServer();
|
||||
ntInstance.stopServer();
|
||||
if (host != null) {
|
||||
NTInst.startClient(host);
|
||||
ntInstance.startClient(host);
|
||||
} else {
|
||||
NTInst.startClientTeam(getTeamNumber());
|
||||
ntInstance.startClientTeam(getTeamNumber());
|
||||
if(ntInstance.isConnected()) {
|
||||
System.out.println("[NetworkTablesManager] Connected to the robot!");
|
||||
} else {
|
||||
System.out.println("[NetworkTablesManager] Could NOT to the robot! Will retry in the background...");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +68,7 @@ public class NetworkTablesManager {
|
||||
public static void setServerMode() {
|
||||
isServer = true;
|
||||
System.out.println("Starting NT Server");
|
||||
NTInst.stopClient();
|
||||
NTInst.startServer();
|
||||
ntInstance.stopClient();
|
||||
ntInstance.startServer();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@ import java.util.concurrent.LinkedBlockingDeque;
|
||||
|
||||
public class ScriptManager {
|
||||
|
||||
private ScriptManager() {}
|
||||
private ScriptManager() {
|
||||
}
|
||||
|
||||
private static final List<ScriptEvent> events = new ArrayList<>();
|
||||
private static final LinkedBlockingDeque<ScriptEventType> queuedEvents = new LinkedBlockingDeque<>(25);
|
||||
@@ -67,9 +68,12 @@ public class ScriptManager {
|
||||
|
||||
protected static final Path scriptConfigPath = Paths.get(ConfigManager.SettingsPath.toString(), "scripts.json");
|
||||
|
||||
private ScriptConfigManager() {}
|
||||
private ScriptConfigManager() {
|
||||
}
|
||||
|
||||
static boolean fileExists() { return Files.exists(scriptConfigPath); }
|
||||
static boolean fileExists() {
|
||||
return Files.exists(scriptConfigPath);
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
if (!fileExists()) {
|
||||
@@ -79,7 +83,7 @@ public class ScriptManager {
|
||||
}
|
||||
|
||||
try {
|
||||
JacksonHelper.serializer(scriptConfigPath, eventsConfig.toArray(new ScriptConfig[0]));
|
||||
JacksonHelper.serializer(scriptConfigPath, eventsConfig.toArray(new ScriptConfig[0]), true);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ public class Helpers {
|
||||
"WantedBy=multi-user.target\n" +
|
||||
"\n";
|
||||
|
||||
|
||||
|
||||
private Helpers() {
|
||||
}
|
||||
|
||||
@@ -55,4 +57,5 @@ public class Helpers {
|
||||
Process p = Runtime.getRuntime().exec("systemctl enable chameleonVision.service");
|
||||
p.waitFor();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,16 +9,24 @@ import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class JacksonHelper {
|
||||
private JacksonHelper() {} // no construction, utility class
|
||||
private JacksonHelper() {
|
||||
} // no construction, utility class
|
||||
|
||||
public static <T> void serializer(Path path, T object) throws IOException {
|
||||
serializer(path, object, false);
|
||||
}
|
||||
|
||||
public static <T> void serializer(Path path, T object, boolean forceSync) throws IOException {
|
||||
PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder().allowIfBaseType(object.getClass()).build();
|
||||
ObjectMapper objectMapper = JsonMapper.builder().activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT).build();
|
||||
objectMapper.writerWithDefaultPrettyPrinter().writeValue(new File(path.toString()), object);
|
||||
String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object);
|
||||
saveJsonString(json, path, forceSync);
|
||||
}
|
||||
|
||||
public static <T> T deserialize(Path path, Class<T> ref) throws IOException {
|
||||
@@ -43,13 +51,27 @@ public class JacksonHelper {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static <T> void serialize(Path path, T object, Class<T> ref, StdSerializer<T> serializer) throws IOException {
|
||||
serialize(path, object, ref, serializer, false);
|
||||
}
|
||||
|
||||
public static <T> void serialize(Path path, T object, Class<T> ref, StdSerializer<T> serializer, boolean forceSync) throws IOException {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
SimpleModule module = new SimpleModule();
|
||||
module.addSerializer(ref, serializer);
|
||||
objectMapper.registerModule(module);
|
||||
String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object);
|
||||
saveJsonString(json, path, forceSync);
|
||||
}
|
||||
|
||||
objectMapper.writerWithDefaultPrettyPrinter().writeValue(new File(path.toString()), object);
|
||||
private static void saveJsonString(String json, Path path, boolean forceSync) throws IOException {
|
||||
FileOutputStream fileOutputStream = new FileOutputStream(path.toFile());
|
||||
fileOutputStream.write(json.getBytes());
|
||||
fileOutputStream.flush();
|
||||
if (forceSync) {
|
||||
FileDescriptor fileDescriptor = fileOutputStream.getFD();
|
||||
fileDescriptor.sync();
|
||||
}
|
||||
fileOutputStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ public class VisionManager {
|
||||
if (usbCameraInfosByCameraName.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
System.out.printf("[VisionManager] Found %s cameras!\n", usbCameraInfosByCameraName.size());
|
||||
|
||||
// load the config
|
||||
List<CameraJsonConfig> preliminaryConfigs = new ArrayList<>();
|
||||
@@ -74,7 +75,7 @@ public class VisionManager {
|
||||
});
|
||||
|
||||
loadedCameraConfigs.addAll(ConfigManager.initializeCameras(preliminaryConfigs));
|
||||
|
||||
System.out.printf("[VisionManager] Loaded %s cameras!\n", loadedCameraConfigs.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -91,6 +92,8 @@ public class VisionManager {
|
||||
}
|
||||
currentUIVisionProcess = getVisionProcessByIndex(0);
|
||||
ConfigManager.settings.currentCamera = visionProcesses.get(0).name;
|
||||
|
||||
System.out.printf("[VisionManager] Loaded %s vision processes! Current process: %s\n", visionProcesses.size(), visionProcesses.get(0).name);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -86,18 +86,13 @@ public class VisionProcess {
|
||||
}
|
||||
|
||||
public void start() {
|
||||
System.out.println("Starting NetworkTables.");
|
||||
System.out.printf("[%s Process] Creating network table...\n", getCamera().getProperties().getNickname());
|
||||
initNT(defaultTable);
|
||||
|
||||
System.out.println("Starting vision thread.");
|
||||
System.out.printf("[%s Process] Starting vision thread...\n", getCamera().getProperties().getNickname());
|
||||
var visionThread = new Thread(visionRunnable);
|
||||
visionThread.setName(getCamera().getProperties().name + " - Vision Thread");
|
||||
visionThread.start();
|
||||
|
||||
// System.out.println("Starting stream thread.");
|
||||
// var streamThread = new Thread(streamRunnable);
|
||||
// streamThread.setName(getCamera().getProperties().name + " - Stream Thread");
|
||||
// streamThread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -299,7 +294,7 @@ public class VisionProcess {
|
||||
public void addCalibration(CameraCalibrationConfig cal) {
|
||||
cameraCapture.addCalibrationData(cal);
|
||||
System.out.println("saving to file");
|
||||
fileConfig.saveCalibration(cameraCapture.getConfig());
|
||||
fileConfig.saveCalibration(cameraCapture.getAllCalibrationData());
|
||||
}
|
||||
|
||||
public void setIs3d(Boolean value) {
|
||||
@@ -329,6 +324,9 @@ public class VisionProcess {
|
||||
public void run() {
|
||||
var lastUpdateTimeNanos = System.nanoTime();
|
||||
var lastStreamTimeMs = System.currentTimeMillis();
|
||||
|
||||
System.out.printf("[%s Process] Vision Process Thread -- first run!\n", getCamera().getProperties().getNickname());
|
||||
|
||||
while (!Thread.interrupted()) {
|
||||
|
||||
// blocking call, will block until camera has a new frame.
|
||||
@@ -357,14 +355,19 @@ public class VisionProcess {
|
||||
try {
|
||||
var currentTime = System.currentTimeMillis();
|
||||
if ((currentTime - lastStreamTimeMs) / 1000d > 1.0 / 30.0) {
|
||||
cameraStreamer.runStream(lastPipelineResult.outputMat);
|
||||
// System.out.println("Ran stream in " + (System.currentTimeMillis() - currentTime) + "ms!");
|
||||
lastStreamTimeMs = currentTime;
|
||||
lastPipelineResult.outputMat.release();
|
||||
if(lastPipelineResult != null) {
|
||||
cameraStreamer.runStream(lastPipelineResult.outputMat);
|
||||
lastStreamTimeMs = currentTime;
|
||||
lastPipelineResult.outputMat.release();
|
||||
} else {
|
||||
System.err.printf("[%s Process] Last pipeline result was null!\n", getCamera().getProperties().getNickname());
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
Debug.printInfo("Vision running faster than stream.");
|
||||
// Debug.printInfo("Vision running faster than stream.");
|
||||
System.err.printf("[%s Process] Exception in vision thread!\n", getCamera().getProperties().getNickname());
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
var deltaTimeNanos = System.nanoTime() - lastUpdateTimeNanos;
|
||||
|
||||
@@ -58,10 +58,6 @@ public class USBCameraCapture implements CameraCapture {
|
||||
calibrationList.add(newConfig);
|
||||
}
|
||||
|
||||
public List<CameraCalibrationConfig> getConfig() {
|
||||
return calibrationList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public USBCaptureProperties getProperties() {
|
||||
return properties;
|
||||
|
||||
Reference in New Issue
Block a user