Repository: robotpy/mostrobotpy (language: python)

Issue:
Missing hal.simulation functions for registering sim callbacks
### Problem description

Some of the `hal.simulation` functions for registering callbacks are missing. For example, there is no `registerSimValueChangedCallback`, even though there is a `cancelSimValueChangedCallback`. These callbacks are necessary for vendors to use WPILib sim GUI inputs.

I believe all the relevant callback APIs are in SimDeviceData.h: https://github.com/wpilibsuite/allwpilib/blob/main/hal/src/main/native/include/hal/simulation/SimDeviceData.h

### Operating System

Windows

### Installed Python Packages

```text
pyfrc                          2024.0.1
pyntcore                       2024.3.2.0
robotpy                        2024.3.2.1
robotpy-cli                    2024.0.0
robotpy-commands-v2            2024.3.1
robotpy-hal                    2024.3.2.0
robotpy-halsim-gui             2024.3.2.0
robotpy-installer              2024.2.2
robotpy-wpilib-utilities       2024.0.0
robotpy-wpimath                2024.3.2.0
robotpy-wpinet                 2024.3.2.0
robotpy-wpiutil                2024.3.2.0
wpilib                         2024.3.2.0
```


### Reproducible example code

_No response_


Discussion / hints:
https://github.com/robotpy/mostrobotpy/blob/main/subprojects/robotpy-hal/gen/simulation/SimDeviceData.yml

They are all manually ignored right now for some reason.
Nobody ever needed them so I never bothered. It shouldn't be that bad. Which specific callbacks do you need?
We specifically only need `registerSimValueChangedCallback` right now, as that's the callback that's triggered when users change the value of SimDevice fields in the sim GUI. 

Fix patch:
diff --git a/subprojects/robotpy-hal/gen/simulation/SimDeviceData.yml b/subprojects/robotpy-hal/gen/simulation/SimDeviceData.yml
index e6a28dc1..793bac0a 100644
--- a/subprojects/robotpy-hal/gen/simulation/SimDeviceData.yml
+++ b/subprojects/robotpy-hal/gen/simulation/SimDeviceData.yml
@@ -1,5 +1,9 @@
 ---
 
+extra_includes:
+- sim_value_cb.h
+- pybind11/functional.h
+
 strip_prefixes:
 - HALSIM_
 
@@ -9,26 +13,83 @@ functions:
   HALSIM_RegisterSimDeviceCreatedCallback:
     ignore: true
   HALSIM_CancelSimDeviceCreatedCallback:
+    ignore: true
   HALSIM_RegisterSimDeviceFreedCallback:
     ignore: true
   HALSIM_CancelSimDeviceCreatedCallback:
+    ignore: true
   HALSIM_CancelSimDeviceFreedCallback:
+    ignore: true
   HALSIM_GetSimDeviceHandle:
   HALSIM_GetSimDeviceName:
   HALSIM_GetSimValueDeviceHandle:
   HALSIM_EnumerateSimDevices:
     ignore: true
   HALSIM_RegisterSimValueCreatedCallback:
-    ignore: true
+    param_override:
+      param:
+        ignore: true
+    cpp_code: |
+      [](hal::SimDevice &simdevice, std::function<void(const char *, HAL_SimValueHandle, HAL_SimValueDirection, HAL_Value)> fn, bool initialNotify) -> std::unique_ptr<SimValueCB> {
+        auto cb = std::make_unique<SimValueCB>(fn, HALSIM_CancelSimDeviceCreatedCallback);
+        auto uid = HALSIM_RegisterSimValueCreatedCallback(simdevice, cb.get(),
+          [](const char* name, void* param,
+             HAL_SimValueHandle handle,
+             int32_t direction,
+             const struct HAL_Value* value) {
+          ((SimValueCB*)param)->m_fn(name, handle, (HAL_SimValueDirection)direction, *value);
+        }, initialNotify);
+        cb->SetUID(uid);
+        return std::move(cb);
+      }
   HALSIM_CancelSimDeviceCreatedCallback:
+    ignore: true
   HALSIM_CancelSimValueCreatedCallback:
-  HALSIM_RegisterSimValueChangedCallback:
     ignore: true
+  HALSIM_RegisterSimValueChangedCallback:
+    param_override:
+      handle:
+        name: value
+      param:
+        ignore: true
+    cpp_code: |
+      [](hal::SimValue &simvalue, std::function<void(const char *, HAL_SimValueHandle, HAL_SimValueDirection, HAL_Value)> fn, bool initialNotify) -> std::unique_ptr<SimValueCB> {
+        auto cb = std::make_unique<SimValueCB>(fn, HALSIM_CancelSimValueChangedCallback);
+        auto uid = HALSIM_RegisterSimValueChangedCallback(simvalue, cb.get(),
+          [](const char* name, void* param,
+             HAL_SimValueHandle handle,
+             int32_t direction,
+             const struct HAL_Value* value) {
+          ((SimValueCB*)param)->m_fn(name, handle, (HAL_SimValueDirection)direction, *value);
+        }, initialNotify);
+        cb->SetUID(uid);
+        return std::move(cb);
+      }
   HALSIM_CancelSimDeviceCreatedCallback:
+    ignore: true
   HALSIM_CancelSimValueChangedCallback:
-  HALSIM_RegisterSimValueResetCallback:
     ignore: true
+  HALSIM_RegisterSimValueResetCallback:
+    param_override:
+      handle:
+        name: value
+      param:
+        ignore: true
+    cpp_code: |
+      [](hal::SimValue &simvalue, std::function<void(const char *, HAL_SimValueHandle, HAL_SimValueDirection, HAL_Value)> fn, bool initialNotify) -> std::unique_ptr<SimValueCB> {
+        auto cb = std::make_unique<SimValueCB>(fn, HALSIM_CancelSimValueResetCallback);
+        auto uid = HALSIM_RegisterSimValueChangedCallback(simvalue, cb.get(),
+          [](const char* name, void* param,
+             HAL_SimValueHandle handle,
+             int32_t direction,
+             const struct HAL_Value* value) {
+          ((SimValueCB*)param)->m_fn(name, handle, (HAL_SimValueDirection)direction, *value);
+        }, initialNotify);
+        cb->SetUID(uid);
+        return std::move(cb);
+      }
   HALSIM_CancelSimValueResetCallback:
+    ignore: true
   HALSIM_GetSimValueHandle:
   HALSIM_EnumerateSimValues:
     ignore: true
diff --git a/subprojects/robotpy-hal/hal/simulation/main.cpp b/subprojects/robotpy-hal/hal/simulation/main.cpp
index affebc05..354b75c1 100644
--- a/subprojects/robotpy-hal/hal/simulation/main.cpp
+++ b/subprojects/robotpy-hal/hal/simulation/main.cpp
@@ -3,6 +3,7 @@
 #include <pybind11/functional.h>
 
 #include "sim_cb.h"
+#include "sim_value_cb.h"
 
 void HALSIM_ResetGlobalHandles();
 
@@ -12,6 +13,10 @@ RPYBUILD_PYBIND11_MODULE(m) {
   cls_SimCB.doc() = "Simulation callback handle";
   cls_SimCB.def("cancel", &SimCB::Cancel, py::doc("Cancel the callback"));
 
+  py::class_<SimValueCB> cls_SimValueCB(m, "SimValueCB");
+  cls_SimValueCB.doc() = "Simulation callback handle";
+  cls_SimValueCB.def("cancel", &SimValueCB::Cancel, py::doc("Cancel the callback"));
+
   initWrapper(m);
 
   m.def(
diff --git a/subprojects/robotpy-hal/hal/simulation/sim_value_cb.h b/subprojects/robotpy-hal/hal/simulation/sim_value_cb.h
new file mode 100644
index 00000000..d7477caa
--- /dev/null
+++ b/subprojects/robotpy-hal/hal/simulation/sim_value_cb.h
@@ -0,0 +1,37 @@
+
+#pragma once
+
+#include <hal/SimDevice.h>
+
+class SimValueCB {
+public:
+
+    using FnType = std::function<void(const char *, HAL_SimValueHandle, HAL_SimValueDirection, HAL_Value)>;
+
+    SimValueCB(FnType fn, std::function<void(int32_t)> cancel) :
+        m_fn(fn),
+        m_cancel(cancel)
+    {}
+
+    void SetUID(int32_t uid) {
+        m_uid = uid;
+    }
+
+    ~SimValueCB() {
+        Cancel();
+    }
+
+    void Cancel() {
+        if (m_valid) {
+            m_cancel(m_uid);
+            m_valid = false;
+        }
+    }
+
+    FnType m_fn;
+
+private:
+    bool m_valid = true;
+    int32_t m_uid;
+    std::function<void(int32_t)> m_cancel;
+};
\ No newline at end of file


Test patch:
diff --git a/subprojects/robotpy-hal/tests/test_hal_simulation.py b/subprojects/robotpy-hal/tests/test_hal_simulation.py
index ea8ddc66..f9fd666b 100644
--- a/subprojects/robotpy-hal/tests/test_hal_simulation.py
+++ b/subprojects/robotpy-hal/tests/test_hal_simulation.py
@@ -1,5 +1,41 @@
+import hal
 import hal.simulation
 
+import typing
 
-def test_hal_simulation():
-    pass
+
+def test_value_changed_callback():
+
+    recv: typing.Optional[typing.Tuple[bool, str, int]] = None
+
+    def created_cb(
+        name: str, handle: int, direction: hal.SimValueDirection, value: hal.Value
+    ):
+        nonlocal recv
+        recv = (True, name, value.value)
+
+    def cb(name: str, handle: int, direction: hal.SimValueDirection, value: hal.Value):
+        nonlocal recv
+        recv = (False, name, value.value)
+
+    dev = hal.SimDevice("simd")
+
+    # Must keep the returned value alive or the callback will be unregistered
+    devunused = hal.simulation.registerSimValueCreatedCallback(dev, created_cb, True)
+    assert recv is None
+
+    val = dev.createInt("answer", 0, 42)
+
+    assert recv == (True, "answer", 42)
+    recv = None
+
+    # Must keep the returned value alive or the callback will be unregistered
+    unused = hal.simulation.registerSimValueChangedCallback(val, cb, True)
+
+    assert recv == (False, "answer", 42)
+    recv = None
+
+    val.set(84)
+
+    assert recv == (False, "answer", 84)
+    recv = None