From fe3fcde2fb151e9e84c2e21c343721a203c301ed Mon Sep 17 00:00:00 2001
From: Henrik Lindgren <henrik@hlindgren.xyz>
Date: Sun, 9 Oct 2022 18:45:41 +0200
Subject: [PATCH] add control panel and pose feature

---
 exporter.py | 195 ++++++++++++++++++++++++++++++++++++++++++++--------
 1 file changed, 166 insertions(+), 29 deletions(-)

diff --git a/exporter.py b/exporter.py
index 40463f4..4c72224 100644
--- a/exporter.py
+++ b/exporter.py
@@ -3,9 +3,15 @@ import json
 
 import bpy
 from bpy_extras.io_utils import ExportHelper
-from bpy.props import StringProperty
+from bpy.props import StringProperty, IntProperty, PointerProperty
 from bpy.types import Operator
 
+import requests
+
+
+DOFBOT_IP = '192.168.50.172'
+DOFBOT_PORT = 5000
+
 
 def print(data):
     for window in bpy.context.window_manager.windows:
@@ -33,17 +39,17 @@ def get_servo_objects():
 
 def get_moving_angle(servo_name, rot_deg):
     if servo_name == 'bottom_rotation':
-        return rot_deg.z
+        return rot_deg.z + 90
     if servo_name == 'joint_1':
-        return rot_deg.y
+        return rot_deg.y + 90
     if servo_name == 'joint_2':
-        return rot_deg.y
+        return rot_deg.y + 90
     if servo_name == 'joint_3':
-        return rot_deg.y
+        return rot_deg.y + 90
     if servo_name == 'claw_rotation':
-        return rot_deg.z
+        return rot_deg.z + 90
     if servo_name == 'claw_grip':
-        return rot_deg.x
+        return rot_deg.x*2
 
 
 def frame2time(frame):
@@ -58,9 +64,15 @@ class RotationDegrees:
         self.z = round(math.degrees(euler_rot.z))
 
 
-# This method of stepping through frame by frame and looking at the objects matrix_world
-# was taken from the below export script by @astronotter
-# https://gist.github.com/astronotter/faec735fc5e270df57741705052096ac
+def get_servo_angle(servo_object):
+    rot_deg = RotationDegrees(servo_object.rotation_euler)
+    angle = get_moving_angle(servo_object.name, rot_deg)
+    
+    # print((frame, servo_object.name, angle, servo_object.rotation_euler))
+    
+    return angle
+
+
 def get_servo_data():
     frame_range = range(bpy.context.scene.frame_start, bpy.context.scene.frame_end + 1)
     
@@ -68,21 +80,35 @@ def get_servo_data():
         previous_angle = None
         for frame in frame_range:
             bpy.context.scene.frame_set(frame)
-            matrix = obj.matrix_world.copy()
-            # posx, posy, posz = matrix.to_translation()[:]
-            # scalex, scaley, scalez = matrix.to_scale()[:]
-            rot_deg = RotationDegrees(matrix.to_euler())
-            angle = get_moving_angle(obj.name, rot_deg)
-            
+            angle = get_servo_angle(obj)
+        
             if previous_angle == angle:
                 continue
-            
+
             previous_angle = angle
-            
             yield (obj.name, [frame2time(frame), angle])
+    
+    bpy.context.scene.frame_set(1)
+
+
+def collect_servo_data_snapshot():
+    servos = {
+            'bottom_rotation': [],
+            'joint_1': [],
+            'joint_2': [],
+            'joint_3': [],
+            'claw_rotation': [],
+            'claw_grip': []
+            }
+    
+    for obj in get_servo_objects():
+        angle = get_servo_angle(obj)
+        servos[obj.name].append([2000, angle])
+    
+    return servos
 
 
-def calc_servo_data():
+def collect_servo_data():
     servos = {
             'bottom_rotation': [],
             'joint_1': [],
@@ -98,10 +124,44 @@ def calc_servo_data():
     return servos
 
 
+class DofbotProperties(bpy.types.PropertyGroup):
+    dofbot_ip: StringProperty(
+        name="IP",
+        description="The IP address of the Dofbot",
+        default=DOFBOT_IP,
+    )
+    
+    dofbot_port: StringProperty(
+        name="Port",
+        description="The port of the Dofbot control API",
+        default=str(DOFBOT_PORT),
+    )
+
+
+class DofbotRunAnimation(Operator):
+    """Run animation on dofbot"""
+    bl_label = "Run animation"
+    bl_idname = "dofbot.run_animation"
+     
+    def execute(self, context):
+        run_animation()
+        return {'FINISHED'}
+
+
+class DofbotSetCurrentPose(Operator):
+    """Set Dofbot to the current pose"""
+    bl_label = "Set pose"
+    bl_idname = "dofbot.set_current_pose"
+     
+    def execute(self, context):
+        set_current_pose()
+        return {'FINISHED'}
+
+
 class ExportDofbotAnimation(Operator, ExportHelper):
     """Exports Dofbot animation as json"""
-    bl_idname = "export_dofbot.json_animation"  # important since its how bpy.ops.import_test.some_data is constructed
-    bl_label = "Export Dofbot Json Animation"
+    bl_idname = 'dofbot.export_json_animation'  # important since its how bpy.ops.import_test.some_data is constructed
+    bl_label = '' # "Export Dofbot Json Animation"
 
     # ExportHelper mixin class uses this
     filename_ext = ".json"
@@ -113,31 +173,108 @@ class ExportDofbotAnimation(Operator, ExportHelper):
     )
 
     def execute(self, context):
-        servo_data = calc_servo_data()
+        servo_data = collect_servo_data()
         
         with open(self.filepath, 'w') as f:
             f.write(json.dumps(servo_data))
             return {'FINISHED'}
 
 
-# Only needed if you want to add into a dynamic menu
-def menu_func_export(self, context):
-    self.layout.operator(ExportDofbotAnimation.bl_idname, text="Dofbot Animation Export")
+class ExportDofbotPose(Operator, ExportHelper):
+    """Exports Dofbot pose as json"""
+    bl_idname = 'dofbot.export_json_pose'  # important since its how bpy.ops.import_test.some_data is constructed
+    bl_label = '' # "Export Dofbot Json Pose"
+
+    # ExportHelper mixin class uses this
+    filename_ext = ".json"
+
+    filter_glob: StringProperty(
+        default="*.json",
+        options={'HIDDEN'},
+        maxlen=255,  # Max internal buffer length, longer would be clamped.
+    )
+
+    def execute(self, context):
+        servo_data = collect_servo_data_snapshot()
+        
+        with open(self.filepath, 'w') as f:
+            f.write(json.dumps(servo_data))
+            return {'FINISHED'}
 
 
+class DofbotPanel(bpy.types.Panel):
+    """Open Dofbot Control Panel"""
+    bl_label = 'Dofbot Control'
+    bl_idname = 'wm.dofbot_panel'
+    bl_space_type = 'VIEW_3D'
+    bl_region_type = 'UI'
+    bl_category = 'Dofbot Control'
+    
+    def draw(self, context):
+        layout = self.layout
+        dofbot = context.scene.dofbot
+        
+        row = layout.row()
+        row.prop(dofbot, 'dofbot_ip')
+        row = layout.row()
+        row.prop(dofbot, 'dofbot_port')
+
+        row = layout.row()
+        row.operator('dofbot.run_animation', icon='PLAY')
+        row.operator('dofbot.export_json_animation', icon='EXPORT')
+        
+        row = layout.row()
+        row.operator('dofbot.set_current_pose', icon='FF')
+        row.operator('dofbot.export_json_pose', icon='EXPORT')
+        
+
 # Register and add to the "file selector" menu (required to use F3 search "Text Export Operator" for quick access).
 def register():
+    bpy.utils.register_class(DofbotProperties)
+    bpy.types.Scene.dofbot = PointerProperty(type=DofbotProperties)
+
+    bpy.utils.register_class(DofbotPanel)
+    bpy.utils.register_class(DofbotRunAnimation)
+    bpy.utils.register_class(DofbotSetCurrentPose)
     bpy.utils.register_class(ExportDofbotAnimation)
-    bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
+    bpy.utils.register_class(ExportDofbotPose)
 
 
 def unregister():
+    bpy.utils.unregister_class(DofbotProperties)
+    del bpy.types.scene.dofbot
+
+    bpy.utils.unregister_class(DofbotPanel)
+    bpy.utils.unregister_class(DofbotRunAnimation)
+    bpy.utils.unregister_class(DofbotSetCurrentPose)
     bpy.utils.unregister_class(ExportDofbotAnimation)
-    bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
+    bpy.utils.unregister_class(ExportDofbotPose)
+
+
+def send_motion(servo_data):
+    requests.post('http://' + DOFBOT_IP + ':' + str(DOFBOT_PORT) + '/motion', json = servo_data)
+
+
+def run_animation():
+    servo_data = collect_servo_data()
+    send_motion(servo_data)
+
+
+def set_current_pose():
+    servo_data = collect_servo_data_snapshot()
+    send_motion(servo_data)
+
+
+def export_animation():
+    bpy.ops.export_dofbot.json_animation('INVOKE_DEFAULT')
 
 
 if __name__ == "__main__":
     register()
 
-    # test call
-    bpy.ops.export_dofbot.json_animation('INVOKE_DEFAULT')
+#    # test call
+#    bpy.ops.export_dofbot.json_animation('INVOKE_DEFAULT')
+
+
+
+
-- 
GitLab