Skip to content

Commit 3cefa74

Browse files
committed
Merge branch 'feature/sd_support' into develop
2 parents 6a19bad + e85f10c commit 3cefa74

26 files changed

+2157
-904
lines changed

.github/workflows/ci.yml

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ jobs:
77
name: "Build"
88
runs-on: ubuntu-latest
99
steps:
10-
- uses: actions/checkout@v3
10+
- uses: actions/checkout@v4
1111
- name: Build add-on
1212
run: |
1313
cp -R addon servo_animation
1414
cp README.md LICENSE servo_animation
1515
zip -r blender_servo_animation_addon servo_animation
1616
- name: Archive add-on ZIP
17-
uses: actions/upload-artifact@v3
17+
uses: actions/upload-artifact@v4
1818
with:
1919
name: blender_servo_animation_addon.zip
2020
path: |
@@ -23,9 +23,9 @@ jobs:
2323
name: "Lint"
2424
runs-on: ubuntu-latest
2525
steps:
26-
- uses: actions/checkout@v3
26+
- uses: actions/checkout@v4
2727
- name: Set up Python 3.10
28-
uses: actions/setup-python@v3
28+
uses: actions/setup-python@v5
2929
with:
3030
python-version: "3.10"
3131
cache: "pip"
@@ -48,10 +48,11 @@ jobs:
4848
- {version: "2.90.0", dir: "2.90"}
4949
- {version: "3.1.0", dir: "3.1"}
5050
- {version: "3.5.0", dir: "3.5"}
51+
- {version: "4.0.0", dir: "4.0"}
5152
steps:
52-
- uses: actions/checkout@v3
53+
- uses: actions/checkout@v4
5354
- name: Set up Python
54-
uses: actions/setup-python@v3
55+
uses: actions/setup-python@v5
5556
with:
5657
python-version: "3.10"
5758
cache: "pip"
@@ -61,7 +62,7 @@ jobs:
6162
sudo apt-get install --no-install-recommends -y unzip wget xz-utils libxi6 libxxf86vm1 libxfixes3 libxrender1 libgl1
6263
- name: Restore Blender cache
6364
id: restore-blender-cache
64-
uses: actions/cache/restore@v3
65+
uses: actions/cache/restore@v4
6566
with:
6667
path: blender-${{ matrix.blender.version }}
6768
key: blender-${{ matrix.blender.version }}
@@ -74,7 +75,7 @@ jobs:
7475
mkdir -p blender-${{ matrix.blender.version }}
7576
tar xf *.tar.xz -C blender-${{ matrix.blender.version }} --strip-components 1
7677
- name: Cache Blender
77-
uses: actions/cache/save@v3
78+
uses: actions/cache/save@v4
7879
if: steps.restore-blender-cache.outputs.cache-hit != 'true'
7980
with:
8081
path: blender-${{ matrix.blender.version }}
@@ -83,7 +84,7 @@ jobs:
8384
run: |
8485
sudo ln -s "$(pwd)/blender-${{ matrix.blender.version }}/blender" /usr/bin/blender
8586
- name: Download add-on
86-
uses: actions/download-artifact@v3
87+
uses: actions/download-artifact@v4
8788
with:
8889
name: blender_servo_animation_addon.zip
8990
- name: Install add-on

.vscode/settings.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
{
2-
"python.linting.enabled": true,
3-
"python.linting.pylintEnabled": true,
42
"files.exclude": {
53
"**/__pycache__": true
64
},

addon/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from .ui.menu_panel import MenuPanel
77
from .ops.json_export import JsonExport
88
from .ops.arduino_export import ArduinoExport
9+
from .ops.binary_export import BinaryExport
910
from .ops.stop_live_mode import StopLiveMode
1011
from .ops.install_dependencies import InstallDependencies
1112
from .ops.start_live_mode import StartLiveMode
@@ -32,6 +33,7 @@
3233
MenuPanel,
3334
ArduinoExport,
3435
JsonExport,
36+
BinaryExport,
3537
StopLiveMode,
3638
StartLiveMode,
3739
InstallDependencies,
@@ -42,6 +44,7 @@
4244
def menu_func_export(self, _):
4345
self.layout.operator(ArduinoExport.bl_idname)
4446
self.layout.operator(JsonExport.bl_idname)
47+
self.layout.operator(BinaryExport.bl_idname)
4548

4649

4750
def menu_func_timeline(self, _):

addon/ops/arduino_export.py

Lines changed: 42 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,24 @@
1-
import re
21
import bpy
32

43
from bpy.types import Operator
54
from bpy_extras.io_utils import ExportHelper
65
from .base_export import BaseExport
7-
from ..utils.servo_settings import get_pose_bone_by_servo_id
86

97

108
class ArduinoExport(Operator, BaseExport, ExportHelper):
11-
bl_idname = "export_anim.servo_positions_arduino"
12-
bl_label = "Animation Servo Positions (.h)"
9+
bl_idname = "export_anim.servo_animation_arduino"
10+
bl_label = "Servo Animation (.h)"
1311
bl_description = "Save an Arduino header file with servo position values of the active armature"
1412

1513
filename_ext = ".h"
16-
position_chunk_size = 50
14+
chunk_size = 12
1715

1816
filter_glob: bpy.props.StringProperty(
1917
default="*.h",
2018
options={'HIDDEN'},
2119
maxlen=255
2220
)
2321

24-
progmem: bpy.props.BoolProperty(
25-
name="Add PROGMEM modifier",
26-
description=(
27-
"Add the PROGMEM modifier to each position array which enables "
28-
"an Arduino micro controller to handle large arrays"
29-
),
30-
default=True
31-
)
32-
33-
animation_variables: bpy.props.BoolProperty(
34-
name="Add animation variables",
35-
description="Add the fps and frames count as constant variables",
36-
default=True
37-
)
38-
3922
namespace: bpy.props.BoolProperty(
4023
name="Add scene namespace",
4124
description=(
@@ -44,53 +27,60 @@ class ArduinoExport(Operator, BaseExport, ExportHelper):
4427
)
4528
)
4629

47-
def export(self, positions, context):
48-
variable_type = 'int' if self.precision == 0 else 'float'
30+
def export(self, positions, filepath, context):
4931
fps, frames, seconds = self.get_time_meta(context.scene)
5032
filename = self.get_blend_filename()
5133

5234
content = (
5335
"/*\n Blender Servo Animation Positions\n\n "
5436
f"FPS: {fps}\n Frames: {frames}\n Seconds: {seconds}\n "
55-
f"Bones: {len(positions)}\n Armature: {context.object.name}\n "
56-
f"Scene: {context.scene.name}\n File: {filename}\n*/\n"
37+
f"Bones: {len(positions[0])}\n Armature: {context.object.name}\n "
38+
f"Scene: {context.scene.name}\n File: {filename}\n*/\n\n"
39+
"#include <Arduino.h>\n"
5740
)
5841

59-
if self.progmem or self.animation_variables:
60-
content += "\n#include <Arduino.h>\n"
42+
commands = self.get_commands(positions)
43+
length = len(commands)
44+
lines = self.join_by_chunk_size(commands, self.chunk_size)
6145

6246
if self.namespace:
63-
content += f"\nnamespace {context.scene.name} {{\n"
47+
scene_name = self.format_scene_name()
48+
content += f"\nnamespace {scene_name} {{\n"
6449

65-
if self.animation_variables:
66-
content += (
67-
f"\nconst byte FPS = {fps};"
68-
f"\nconst int FRAMES = {frames};\n"
69-
)
50+
content += (
51+
f"\nconst byte FPS = {fps};"
52+
f"\nconst int FRAMES = {frames};"
53+
f"\nconst int LENGTH = {length};\n\n"
54+
)
7055

71-
for servo_id in positions:
72-
pose_bone = get_pose_bone_by_servo_id(servo_id, context.scene)
73-
bone_positions = list(map(str, positions[servo_id]))
74-
variable_name = re.sub('[^a-zA-Z0-9_]', '', pose_bone.bone.name)
75-
array_size = "FRAMES" if self.animation_variables else frames
76-
content += (
77-
f"\n// Servo ID: {servo_id}\n"
78-
f"const {variable_type} {variable_name}[{array_size}] "
79-
)
56+
content += f'const byte PROGMEM ANIMATION_DATA[LENGTH] = {{\n{lines}}};\n'
8057

81-
if self.progmem:
82-
content += 'PROGMEM '
58+
if self.namespace:
59+
content += f"\n}} // namespace {scene_name}\n"
8360

84-
content += '= {\n'
61+
with open(filepath, 'w', encoding='utf-8') as file:
62+
file.write(content)
8563

86-
for i in range(0, len(bone_positions), self.position_chunk_size):
87-
content += ' ' + \
88-
', '.join(
89-
bone_positions[i:i + self.position_chunk_size]) + ',\n'
64+
@classmethod
65+
def join_by_chunk_size(cls, iterable, chunk_size):
66+
output = ''
67+
str_iterable = list(map(cls.format_hex, iterable))
9068

91-
content += '};\n'
69+
for i in range(0, len(str_iterable), chunk_size):
70+
output += ' ' + ', '.join(str_iterable[i:i + chunk_size]) + ',\n'
9271

93-
if self.namespace:
94-
content += f"\n}} // namespace {context.scene.name}\n"
72+
return output
73+
74+
@classmethod
75+
def format_hex(cls, byte):
76+
return f'{byte:#04x}'
77+
78+
@classmethod
79+
def format_scene_name(cls):
80+
valid_chars = set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_')
81+
scene_name = ''.join(c if c in valid_chars else '_' for c in bpy.context.scene.name)
82+
83+
if scene_name[0].isdigit():
84+
scene_name = '_' + scene_name
9585

96-
return content
86+
return scene_name

addon/ops/base_export.py

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66

77

88
class BaseExport:
9-
precision: bpy.props.IntProperty(
10-
name="Precision",
11-
description="The number of decimal digits to round to",
12-
default=0,
13-
min=0,
14-
max=6
9+
COMMAND_START = 0x3C
10+
COMMAND_END = 0x3E
11+
LINE_BREAK = 10
12+
13+
skip_duplicates: bpy.props.BoolProperty(
14+
name="Skip unchanged positions",
15+
description="Skip positions which haven't changed since the last frame",
16+
default=True
1517
)
1618

1719
@classmethod
@@ -32,8 +34,8 @@ def execute(self, context):
3234
bpy.ops.servo_animation.stop_live_mode()
3335

3436
try:
35-
positions = calculate_positions(context, self.precision)
36-
content = self.export(positions, context)
37+
positions = calculate_positions(context, self.skip_duplicates)
38+
self.export(positions, self.filepath, context)
3739
except RuntimeError as error:
3840
self.report({'ERROR'}, str(error))
3941

@@ -44,16 +46,33 @@ def execute(self, context):
4446
if original_live_mode is True:
4547
bpy.ops.servo_animation.start_live_mode('INVOKE_DEFAULT')
4648

47-
with open(self.filepath, 'w', encoding='utf-8') as file:
48-
file.write(content)
49-
5049
end = time.time()
5150
duration = round(end - start)
51+
unit = "second" if duration == 1 else "seconds"
5252
self.report(
53-
{'INFO'}, f"Animation servo positions exported after {duration} seconds")
53+
{'INFO'}, f"Animation servo positions exported after {duration} {unit}")
5454

5555
return {'FINISHED'}
5656

57+
def get_commands(self, positions):
58+
commands = []
59+
60+
for frame_positions in positions:
61+
for servo_id in frame_positions:
62+
position = frame_positions[servo_id]
63+
commands += self.get_command(servo_id, position)
64+
65+
commands.append(self.LINE_BREAK)
66+
67+
return commands
68+
69+
def get_command(self, servo_id, position):
70+
command = [self.COMMAND_START, servo_id]
71+
command += position.to_bytes(2, 'big')
72+
command += [self.COMMAND_END]
73+
74+
return command
75+
5776
@staticmethod
5877
def get_time_meta(scene):
5978
fps = scene.render.fps

addon/ops/binary_export.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import bpy
2+
3+
from bpy.types import Operator
4+
from bpy_extras.io_utils import ExportHelper
5+
from .base_export import BaseExport
6+
7+
8+
class BinaryExport(Operator, BaseExport, ExportHelper):
9+
bl_idname = "export_anim.servo_animation_binary"
10+
bl_label = "Servo Animation (.bin)"
11+
bl_description = "Save a binary file with servo position values of the active armature"
12+
13+
filename_ext = ".bin"
14+
15+
filter_glob: bpy.props.StringProperty(
16+
default="*.bin",
17+
options={'HIDDEN'},
18+
maxlen=255
19+
)
20+
21+
def export(self, positions, filepath, _context):
22+
commands = self.get_commands(positions)
23+
24+
with open(filepath, 'wb') as file:
25+
file.write(bytes(commands))

0 commit comments

Comments
 (0)