diff --git a/.gitignore b/.gitignore index b7a67f4..42e9117 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.DS_Store + # Cursor hook event log cursor_events.log diff --git a/3d_models/.DS_Store b/3d_models/.DS_Store deleted file mode 100644 index a4afefa..0000000 Binary files a/3d_models/.DS_Store and /dev/null differ diff --git a/3d_models/README.md b/3d_models/README.md index dd7dffa..19fbc34 100644 --- a/3d_models/README.md +++ b/3d_models/README.md @@ -4,27 +4,30 @@ Printable parts and source CAD for the Tiny Engineer desk robot. ## Source CAD (edit / resize) -The full assembly — all components composed — lives in [`cad/TinyEngineer.f3d`](cad/TinyEngineer.f3d) (Autodesk Fusion). Open that file to adjust the model or adapt it to different hardware sizing (e.g. different servos). +The full assembly — all components composed — lives in [`cad/TinyEngineer.f3d`](cad/TinyEngineer.f3d) (Autodesk Fusion). Open that file to adjust the model or adapt it to different hardware sizing (e.g. different servos). Parametric servo sizes and the Fusion add-in: [docs/3d/parametric-design.md](../docs/3d/parametric-design.md). ## Printables -Individual parts are exported to [`parts/`](parts/) as `.3mf` files. They are ready to print in an orientation that does not need supports. Print with **PLA** or **PETG**. +Individual parts are exported under [`parts/{servo_id}/`](parts/). Print the **`.3mf` files in `parts/sg90/3mf/`**, **`parts/fs0307/3mf/`**, or **`parts/hd1370a/3mf/`** to match the servos you bought ([which servo](../docs/3d/parametric-design.md)). They are ready to print in an orientation that does not need supports. Print with **PLA** or **PETG**. `stl/` next to `3mf/` is the same mesh in binary STL. Assembly uses a bunch of **2 mm diameter screws** — no glue. Easy to dismount and modify later. Exact screw lengths/counts and full assembly steps are **not documented yet**. Use the part table below, then electrical and bring-up docs. ## Parts +Same filenames in each `parts/{servo_id}/3mf/` folder: + | File | Role | | --- | --- | | `Head.3mf` | Head (pitch) | | `Neck.3mf` | Neck (yaw) | -| `HandLeft.3mf` | Left hand | -| `HandRight.3mf` | Right hand | -| `Arm.3mf` | Arm (print twice) | +| `ForearmLeft.3mf` | Left forearm / hand | +| `ForearmRight.3mf` | Right forearm / hand | +| `UpperArm.3mf` | Upper arm | | `Chest.3mf` | Chest / torso | | `Belly.3mf` | Belly | | `Chair.3mf` | Chair | -| `Pillow.3mf` | Chair pillow | +| `SeatLeft.3mf` | Left chair seat | +| `SeatRight.3mf` | Right chair seat | | `Desk.3mf` | Desk structure | | `DeskTop.3mf` | Desk top surface | | `DeskPad.3mf` | Desk pad | @@ -33,10 +36,12 @@ Assembly uses a bunch of **2 mm diameter screws** — no glue. Easy to dismount | `Bell.3mf` | Service bell | | `LampBase.3mf` | Desk lamp base | | `LampCap.3mf` | Lamp cap | -| `LampGlass.3mf` | Lamp glass | +| `LampDiffuser.3mf` | Lamp diffuser | | `LampButton.3mf` | Lamp button | | `Mug.3mf` | Mug | -| `MugCoffee.3mf` | Coffee fill | +| `Coffee.3mf` | Coffee fill | +| `Hat.3mf` | Hat | +| `ServoSizingTester.3mf` | Servo pocket fit tester (print first when changing servo size) | | `AiEmblem.3mf` | AI emblem detail | ## Next steps diff --git a/3d_models/cad/TinyEngineer.f3d b/3d_models/cad/TinyEngineer.f3d index fe02475..5b012d5 100644 Binary files a/3d_models/cad/TinyEngineer.f3d and b/3d_models/cad/TinyEngineer.f3d differ diff --git a/3d_models/fusion/README.md b/3d_models/fusion/README.md new file mode 100644 index 0000000..818b0e4 --- /dev/null +++ b/3d_models/fusion/README.md @@ -0,0 +1,5 @@ +# Fusion add-in + +Add-in code lives in [`TinyEngineerTools/`](TinyEngineerTools/). Folder name, `TinyEngineerTools.py`, and `TinyEngineerTools.manifest` must stay the same. + +Install, servo parameters, configurator, and export: [docs/3d/parametric-design.md](../../docs/3d/parametric-design.md). diff --git a/3d_models/fusion/TinyEngineerTools/.gitignore b/3d_models/fusion/TinyEngineerTools/.gitignore new file mode 100644 index 0000000..600d2d3 --- /dev/null +++ b/3d_models/fusion/TinyEngineerTools/.gitignore @@ -0,0 +1 @@ +.vscode \ No newline at end of file diff --git a/3d_models/fusion/TinyEngineerTools/AddInIcon.svg b/3d_models/fusion/TinyEngineerTools/AddInIcon.svg new file mode 100644 index 0000000..dc82408 --- /dev/null +++ b/3d_models/fusion/TinyEngineerTools/AddInIcon.svg @@ -0,0 +1,2576 @@ + + + + + + Fusion Add-In Icon + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/3d_models/fusion/TinyEngineerTools/TinyEngineerTools.manifest b/3d_models/fusion/TinyEngineerTools/TinyEngineerTools.manifest new file mode 100644 index 0000000..33b1b03 --- /dev/null +++ b/3d_models/fusion/TinyEngineerTools/TinyEngineerTools.manifest @@ -0,0 +1,13 @@ +{ + "autodeskProduct": "Fusion", + "type": "addin", + "author": "", + "description": { + "": "TinyEngineer Tools" + }, + "version": "", + "runOnStartup": false, + "supportedOS": "windows|mac", + "editEnabled": true, + "iconFilename": "AddInIcon.svg" +} \ No newline at end of file diff --git a/3d_models/fusion/TinyEngineerTools/TinyEngineerTools.py b/3d_models/fusion/TinyEngineerTools/TinyEngineerTools.py new file mode 100644 index 0000000..735088e --- /dev/null +++ b/3d_models/fusion/TinyEngineerTools/TinyEngineerTools.py @@ -0,0 +1,32 @@ +"""TinyEngineer Tools Add-In.""" + +import os +import sys +import traceback + +import adsk.core + +_ADDIN_DIR = os.path.dirname(os.path.abspath(__file__)) +if _ADDIN_DIR not in sys.path: + sys.path.insert(0, _ADDIN_DIR) + +import parts_exporter +import servo + +ui = adsk.core.Application.get().userInterface + + +def run(context): + try: + servo.start() + parts_exporter.start() + except Exception: + ui.messageBox(f'Add-In startup error:\n\n{traceback.format_exc()}') + + +def stop(context): + try: + parts_exporter.stop() + servo.stop() + except Exception: + ui.messageBox(f'Add-In shutdown error:\n\n{traceback.format_exc()}') diff --git a/3d_models/fusion/TinyEngineerTools/parts_exporter.py b/3d_models/fusion/TinyEngineerTools/parts_exporter.py new file mode 100644 index 0000000..2e53da2 --- /dev/null +++ b/3d_models/fusion/TinyEngineerTools/parts_exporter.py @@ -0,0 +1,334 @@ +"""Export PRINT_LAYOUT children as separate 3MF and binary STL meshes.""" + +import os +import traceback + +import adsk +import adsk.core +import adsk.fusion + +import servo + +app = adsk.core.Application.get() +ui = app.userInterface + +COMMAND_ID = 'TinyEngineerPartsExporter' +COMMAND_NAME = 'Tiny Engineer Parts Exporter' +COMMAND_DESCRIPTION = 'Export each PRINT_LAYOUT child as 3MF and binary STL' +WORKSPACE_ID = 'FusionSolidEnvironment' +PANEL_ID = 'SolidScriptsAddinsPanel' +PRINT_LAYOUT_NAME = 'PRINT_LAYOUT' +_INVALID_FILENAME_CHARS = '<>:"/\\|?*' + +_handlers = [] + + +def start(): + cmd_def = ui.commandDefinitions.itemById(COMMAND_ID) + if not cmd_def: + cmd_def = ui.commandDefinitions.addButtonDefinition( + COMMAND_ID, COMMAND_NAME, COMMAND_DESCRIPTION + ) + + created = CommandCreatedHandler() + cmd_def.commandCreated.add(created) + _handlers.append(created) + + workspace = ui.workspaces.itemById(WORKSPACE_ID) + if not workspace: + ui.messageBox(f'Workspace not found: {WORKSPACE_ID}') + return + + panel = workspace.toolbarPanels.itemById(PANEL_ID) + if not panel: + ui.messageBox(f'Toolbar panel not found: {PANEL_ID}') + return + + control = panel.controls.itemById(COMMAND_ID) + if not control: + control = panel.controls.addCommand(cmd_def, servo.COMMAND_ID, False) + + control.isPromoted = True + control.isPromotedByDefault = True + + +def stop(): + workspace = ui.workspaces.itemById(WORKSPACE_ID) + if workspace: + panel = workspace.toolbarPanels.itemById(PANEL_ID) + if panel: + control = panel.controls.itemById(COMMAND_ID) + if control: + control.deleteMe() + + cmd_def = ui.commandDefinitions.itemById(COMMAND_ID) + if cmd_def: + cmd_def.deleteMe() + + _handlers.clear() + + +def _error(title): + ui.messageBox(f'{title}:\n\n{traceback.format_exc()}') + + +def _safe_filename(name): + cleaned = ''.join('_' if ch in _INVALID_FILENAME_CHARS else ch for ch in name) + cleaned = cleaned.strip().rstrip('.') + return cleaned or 'part' + + +def _unique_base(occ, used): + base = _safe_filename(occ.component.name) + if base not in used: + used.add(base) + return base + + suffixed = _safe_filename(f'{occ.component.name}_{occ.name}') + candidate = suffixed + index = 2 + while candidate in used: + candidate = f'{suffixed}_{index}' + index += 1 + used.add(candidate) + return candidate + + +def _occurrence_list(occurrences): + if not occurrences: + return [] + return [occurrences.item(i) for i in range(occurrences.count)] + + +def _find_print_layout(design): + root = design.rootComponent + if root.name == PRINT_LAYOUT_NAME: + return root, _occurrence_list(root.occurrences), root + + for occ in root.allOccurrences: + if occ.component.name == PRINT_LAYOUT_NAME: + return occ, _occurrence_list(occ.childOccurrences), occ.component + + return None, None, None + + +def _body_list(component): + bodies = component.bRepBodies + if not bodies: + return [] + return [bodies.item(i) for i in range(bodies.count)] + + +def _set_child_visible(children, visible_occ): + for occ in children: + occ.isLightBulbOn = False + if visible_occ: + visible_occ.isLightBulbOn = True + + +def _occurrence_ancestors(occ): + chain = [] + current = occ + while current: + chain.append(current) + current = current.assemblyContext + return chain + + +def _restore_visibility(occ_state, body_state, layout_component, folder_on): + for occ, was_on in occ_state: + occ.isLightBulbOn = was_on + for body, was_on in body_state: + body.isLightBulbOn = was_on + layout_component.isBodiesFolderLightBulbOn = folder_on + + +def _read_servo_id(design): + param = design.userParameters.itemByName(servo.SERVO_ID_PARAM) + if not param: + return None + try: + value = param.textValue + except Exception: + value = param.expression + value = (value or '').strip().strip('"').strip("'").strip() + return value.lower() if value else None + + +def _find_servo_entry(servo_id): + for name, data in servo._load_servos().items(): + if data.get(servo.SERVO_ID_PARAM) == servo_id: + return name, data + return None, None + + +def _write_servo_readme(servo_dir, title, data): + lines = [f'# {title}', ''] + for key, value in data.items(): + lines.append(f'- `{key}`: {value}') + lines.append('') + with open(os.path.join(servo_dir, 'README.md'), 'w', encoding='utf-8') as file: + file.write('\n'.join(lines)) + + +def _choose_folder(): + dialog = ui.createFolderDialog() + dialog.title = 'Select export folder' + if dialog.showDialog() != adsk.core.DialogResults.DialogOK: + return None + return dialog.folder + + +def _export_dirs(folder, servo_id): + servo_dir = os.path.join(folder, servo_id) + stl_dir = os.path.join(servo_dir, 'stl') + c3mf_dir = os.path.join(servo_dir, '3mf') + os.makedirs(stl_dir, exist_ok=True) + os.makedirs(c3mf_dir, exist_ok=True) + return servo_dir, stl_dir, c3mf_dir + + +def _export_meshes(export_mgr, geometry, stl_dir, c3mf_dir, base): + errors = [] + stl_path = os.path.join(stl_dir, f'{base}.stl') + c3mf_path = os.path.join(c3mf_dir, f'{base}.3mf') + + try: + stl = export_mgr.createSTLExportOptions(geometry, stl_path) + stl.isBinaryFormat = True + stl.sendToPrintUtility = False + export_mgr.execute(stl) + except Exception as exc: + errors.append(f'{base}.stl: {exc}') + + try: + c3mf = export_mgr.createC3MFExportOptions(geometry, c3mf_path) + c3mf.sendToPrintUtility = False + export_mgr.execute(c3mf) + except Exception as exc: + errors.append(f'{base}.3mf: {exc}') + + return errors + + +def _export_parts(): + design = adsk.fusion.Design.cast(app.activeProduct) + if not design: + ui.messageBox('Open a Fusion design first.') + return + + raw_id = _read_servo_id(design) + if not raw_id: + ui.messageBox( + 'Apply a servo in TinyEngineer Servo Configurator first ' + '(servo_id is missing).' + ) + return + + servo_id = _safe_filename(raw_id) + servo_name, servo_data = _find_servo_entry(raw_id) + if not servo_data: + ui.messageBox( + f'No servo in servos.json matches servo_id "{raw_id}".' + ) + return + + folder = _choose_folder() + if not folder: + return + + geometry, children, layout_component = _find_print_layout(design) + if geometry is None: + ui.messageBox(f'Component "{PRINT_LAYOUT_NAME}" was not found.') + return + + if not children: + ui.messageBox(f'"{PRINT_LAYOUT_NAME}" has no children to export.') + return + + occ_state = [(occ, occ.isLightBulbOn) for occ in children] + bodies = _body_list(layout_component) + body_state = [(body, body.isLightBulbOn) for body in bodies] + folder_on = layout_component.isBodiesFolderLightBulbOn + + layout_occ = adsk.fusion.Occurrence.cast(geometry) + layout_path = _occurrence_ancestors(layout_occ) if layout_occ else [] + layout_state = [(occ, occ.isLightBulbOn) for occ in layout_path] + + export_mgr = design.exportManager + servo_dir, stl_dir, c3mf_dir = _export_dirs(folder, servo_id) + _write_servo_readme(servo_dir, servo_name, servo_data) + used_names = set() + exported = 0 + errors = [] + cancelled = False + + progress = ui.createProgressDialog() + progress.isBackgroundTranslucent = False + progress.isCancelButtonShown = True + + try: + for occ, _ in layout_state: + occ.isLightBulbOn = True + layout_component.isBodiesFolderLightBulbOn = False + for body, _ in body_state: + body.isLightBulbOn = False + _set_child_visible(children, None) + + progress.show( + 'Tiny Engineer Parts Exporter', 'Exporting %v / %m', 0, len(children), 1 + ) + + for index, occ in enumerate(children): + adsk.doEvents() + if progress.wasCancelled: + cancelled = True + break + + base = _unique_base(occ, used_names) + progress.progressValue = index + progress.message = f'Exporting {base} (%v / %m)' + + _set_child_visible(children, occ) + part_errors = _export_meshes( + export_mgr, geometry, stl_dir, c3mf_dir, base + ) + if part_errors: + errors.extend(part_errors) + else: + exported += 1 + occ.isLightBulbOn = False + adsk.doEvents() + + if not cancelled: + progress.progressValue = len(children) + finally: + progress.hide() + _restore_visibility( + occ_state + layout_state, body_state, layout_component, folder_on + ) + + lines = [f'Exported {exported} part(s) to:\n{servo_dir}'] + if cancelled: + lines.append('Export cancelled.') + if errors: + lines.append('Failures:\n' + '\n'.join(errors)) + ui.messageBox('\n\n'.join(lines)) + + +class ExecuteHandler(adsk.core.CommandEventHandler): + def notify(self, args): + try: + _export_parts() + except Exception: + _error('Execute error') + + +class CommandCreatedHandler(adsk.core.CommandCreatedEventHandler): + def notify(self, args): + try: + execute_handler = ExecuteHandler() + args.command.execute.add(execute_handler) + _handlers.append(execute_handler) + except Exception: + _error('Command creation error') diff --git a/3d_models/fusion/TinyEngineerTools/servo.py b/3d_models/fusion/TinyEngineerTools/servo.py new file mode 100644 index 0000000..7c03755 --- /dev/null +++ b/3d_models/fusion/TinyEngineerTools/servo.py @@ -0,0 +1,229 @@ +"""Servo model picker. Writes dimensions into Fusion user parameters.""" + +import json +import os +import traceback + +import adsk.core +import adsk.fusion + +app = adsk.core.Application.get() +ui = app.userInterface + +COMMAND_ID = 'TinyEngineerTools' +COMMAND_NAME = 'TinyEngineer Servo Configurator' +COMMAND_DESCRIPTION = 'Select servo model and apply dimensions' +WORKSPACE_ID = 'FusionSolidEnvironment' +PANEL_ID = 'SolidScriptsAddinsPanel' +SERVO_ID_PARAM = 'servo_id' + +_handlers = [] + + +def start(): + cmd_def = ui.commandDefinitions.itemById(COMMAND_ID) + if not cmd_def: + cmd_def = ui.commandDefinitions.addButtonDefinition( + COMMAND_ID, COMMAND_NAME, COMMAND_DESCRIPTION + ) + + created = CommandCreatedHandler() + cmd_def.commandCreated.add(created) + _handlers.append(created) + + workspace = ui.workspaces.itemById(WORKSPACE_ID) + if not workspace: + ui.messageBox(f'Workspace not found: {WORKSPACE_ID}') + return + + panel = workspace.toolbarPanels.itemById(PANEL_ID) + if not panel: + ui.messageBox(f'Toolbar panel not found: {PANEL_ID}') + return + + control = panel.controls.itemById(COMMAND_ID) + if not control: + control = panel.controls.addCommand(cmd_def) + + control.isPromoted = True + control.isPromotedByDefault = True + + +def stop(): + workspace = ui.workspaces.itemById(WORKSPACE_ID) + if workspace: + panel = workspace.toolbarPanels.itemById(PANEL_ID) + if panel: + control = panel.controls.itemById(COMMAND_ID) + if control: + control.deleteMe() + + cmd_def = ui.commandDefinitions.itemById(COMMAND_ID) + if cmd_def: + cmd_def.deleteMe() + + _handlers.clear() + + +def _load_servos(): + path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'servos.json') + with open(path, encoding='utf-8') as file: + return json.load(file) + + +def _display_name(parameter_name): + if parameter_name.startswith('servo_'): + parameter_name = parameter_name[len('servo_'):] + return parameter_name.replace('_', ' ').title() + + +def _selected_servo(inputs, servos): + dropdown = adsk.core.DropDownCommandInput.cast( + inputs.itemById('servo_model') + ) + if not dropdown or not dropdown.selectedItem: + return None + return servos[dropdown.selectedItem.name] + + +def _update_preview(inputs, servos): + servo = _selected_servo(inputs, servos) + if not servo: + return + + for parameter_name, value in servo.items(): + preview = adsk.core.TextBoxCommandInput.cast( + inputs.itemById(f'preview_{parameter_name}') + ) + if preview: + preview.text = value + + +def _build_dialog(inputs, servos): + dropdown = inputs.addDropDownCommandInput( + 'servo_model', + 'Servo model', + adsk.core.DropDownStyles.TextListDropDownStyle, + ) + for index, servo_name in enumerate(servos): + dropdown.listItems.add(servo_name, index == 0, '') + + first_servo = next(iter(servos.values())) + for parameter_name in first_servo: + inputs.addTextBoxCommandInput( + f'preview_{parameter_name}', + _display_name(parameter_name), + '', + 1, + True, + ) + + _update_preview(inputs, servos) + + +def _apply_servo_id(params, value): + existing = params.itemByName(SERVO_ID_PARAM) + if existing: + try: + existing.textValue = value + return + except Exception: + pass + try: + existing.expression = f"'{value}'" + return + except Exception: + existing.deleteMe() + + params.add( + SERVO_ID_PARAM, + adsk.core.ValueInput.createByString(f"'{value}'"), + 'Text', + 'Selected servo id', + ) + + +def _apply_servo(design, servo_data): + params = design.userParameters + dimensions = { + name: value + for name, value in servo_data.items() + if name != SERVO_ID_PARAM + } + missing = [name for name in dimensions if not params.itemByName(name)] + if missing: + ui.messageBox( + 'The following Fusion parameters do not exist:\n\n' + + '\n'.join(missing) + ) + return + + servo_id = servo_data.get(SERVO_ID_PARAM) + if servo_id: + _apply_servo_id(params, servo_id) + + for name, value in dimensions.items(): + params.itemByName(name).expression = value + + +def _error(title): + ui.messageBox(f'{title}:\n\n{traceback.format_exc()}') + + +class InputChangedHandler(adsk.core.InputChangedEventHandler): + def __init__(self, servos): + super().__init__() + self.servos = servos + + def notify(self, args): + try: + if args.input.id == 'servo_model': + _update_preview(args.firingEvent.sender.commandInputs, self.servos) + except Exception: + _error('Input error') + + +class ExecuteHandler(adsk.core.CommandEventHandler): + def __init__(self, servos): + super().__init__() + self.servos = servos + + def notify(self, args): + try: + design = adsk.fusion.Design.cast(app.activeProduct) + if not design: + ui.messageBox('Open a Fusion design first.') + return + + servo_data = _selected_servo( + args.firingEvent.sender.commandInputs, self.servos + ) + if not servo_data: + ui.messageBox('No servo model selected.') + return + + _apply_servo(design, servo_data) + except Exception: + _error('Execute error') + + +class CommandCreatedHandler(adsk.core.CommandCreatedEventHandler): + def notify(self, args): + try: + servos = _load_servos() + if not servos: + ui.messageBox('No servo definitions found in servos.json.') + return + + command = args.command + _build_dialog(command.commandInputs, servos) + + input_handler = InputChangedHandler(servos) + command.inputChanged.add(input_handler) + _handlers.append(input_handler) + + execute_handler = ExecuteHandler(servos) + command.execute.add(execute_handler) + _handlers.append(execute_handler) + except Exception: + _error('Command creation error') diff --git a/3d_models/fusion/TinyEngineerTools/servos.json b/3d_models/fusion/TinyEngineerTools/servos.json new file mode 100644 index 0000000..84b4d2b --- /dev/null +++ b/3d_models/fusion/TinyEngineerTools/servos.json @@ -0,0 +1,53 @@ +{ + "Feetech FS0307": { + "servo_id": "fs0307", + "servo_body_width": "20 mm", + "servo_body_height": "13 mm", + "servo_mount_width": "3.8 mm", + "servo_mount_thickness": "1 mm", + "servo_gearbox_height": "3.7 mm", + "servo_gearbox_padding": "6.5 mm", + "servo_shaft_housing_height": "4 mm", + "servo_shaft_position": "4.5 mm", + "servo_shaft_length": "3.3 mm", + "servo_shaft_radius": "1.83 mm", + "servo_body_depth": "8.6 mm", + "servo_mount_hole_radius": "1 mm", + "servo_mount_hole_depth_offset": "0 mm", + "servo_mount_hole_width_offset": "0 mm" + }, + "Tower Pro SG90": { + "servo_id": "sg90", + "servo_body_width": "22.7 mm", + "servo_body_height": "17 mm", + "servo_mount_width": "4.8 mm", + "servo_mount_thickness": "2.4 mm", + "servo_gearbox_height": "4.4 mm", + "servo_gearbox_padding": "7.8 mm", + "servo_shaft_housing_height": "3.5 mm", + "servo_shaft_position": "5.8 mm", + "servo_shaft_length": "3.3 mm", + "servo_shaft_radius": "2.45 mm", + "servo_body_depth": "12.2 mm", + "servo_mount_hole_radius": "1.4 mm", + "servo_mount_hole_depth_offset": "0 mm", + "servo_mount_hole_width_offset": "0.07 mm" + }, + "PowerHD HD-1370A": { + "servo_id": "hd1370a", + "servo_body_width": "20 mm", + "servo_body_height": "13.1 mm", + "servo_mount_width": "3.7 mm", + "servo_mount_thickness": "0.9 mm", + "servo_gearbox_height": "3.9 mm", + "servo_gearbox_padding": "5.4 mm", + "servo_shaft_housing_height": "3.1 mm", + "servo_shaft_position": "4.5 mm", + "servo_shaft_length": "2.9 mm", + "servo_shaft_radius": "1.9 mm", + "servo_body_depth": "8.6 mm", + "servo_mount_hole_radius": "1 mm", + "servo_mount_hole_depth_offset": "0 mm", + "servo_mount_hole_width_offset": "0 mm" + } +} \ No newline at end of file diff --git a/3d_models/parts/AiEmblem.3mf b/3d_models/parts/AiEmblem.3mf deleted file mode 100644 index b26e6e4..0000000 Binary files a/3d_models/parts/AiEmblem.3mf and /dev/null differ diff --git a/3d_models/parts/Arm.3mf b/3d_models/parts/Arm.3mf deleted file mode 100644 index 76cbd35..0000000 Binary files a/3d_models/parts/Arm.3mf and /dev/null differ diff --git a/3d_models/parts/Bell.3mf b/3d_models/parts/Bell.3mf deleted file mode 100644 index d92c91c..0000000 Binary files a/3d_models/parts/Bell.3mf and /dev/null differ diff --git a/3d_models/parts/Belly.3mf b/3d_models/parts/Belly.3mf deleted file mode 100644 index 82eeeb1..0000000 Binary files a/3d_models/parts/Belly.3mf and /dev/null differ diff --git a/3d_models/parts/Chair.3mf b/3d_models/parts/Chair.3mf deleted file mode 100644 index a1d51b4..0000000 Binary files a/3d_models/parts/Chair.3mf and /dev/null differ diff --git a/3d_models/parts/Chest.3mf b/3d_models/parts/Chest.3mf deleted file mode 100644 index 75fae91..0000000 Binary files a/3d_models/parts/Chest.3mf and /dev/null differ diff --git a/3d_models/parts/Desk.3mf b/3d_models/parts/Desk.3mf deleted file mode 100644 index 2c22908..0000000 Binary files a/3d_models/parts/Desk.3mf and /dev/null differ diff --git a/3d_models/parts/DeskPad.3mf b/3d_models/parts/DeskPad.3mf deleted file mode 100644 index 106b93f..0000000 Binary files a/3d_models/parts/DeskPad.3mf and /dev/null differ diff --git a/3d_models/parts/DeskTop.3mf b/3d_models/parts/DeskTop.3mf deleted file mode 100644 index e219bfb..0000000 Binary files a/3d_models/parts/DeskTop.3mf and /dev/null differ diff --git a/3d_models/parts/HandLeft.3mf b/3d_models/parts/HandLeft.3mf deleted file mode 100644 index 9125869..0000000 Binary files a/3d_models/parts/HandLeft.3mf and /dev/null differ diff --git a/3d_models/parts/HandRight.3mf b/3d_models/parts/HandRight.3mf deleted file mode 100644 index 1ff465b..0000000 Binary files a/3d_models/parts/HandRight.3mf and /dev/null differ diff --git a/3d_models/parts/Hat.3mf b/3d_models/parts/Hat.3mf deleted file mode 100644 index 8599fc1..0000000 Binary files a/3d_models/parts/Hat.3mf and /dev/null differ diff --git a/3d_models/parts/Head.3mf b/3d_models/parts/Head.3mf deleted file mode 100644 index f076101..0000000 Binary files a/3d_models/parts/Head.3mf and /dev/null differ diff --git a/3d_models/parts/LampBase.3mf b/3d_models/parts/LampBase.3mf deleted file mode 100644 index b829856..0000000 Binary files a/3d_models/parts/LampBase.3mf and /dev/null differ diff --git a/3d_models/parts/LampButton.3mf b/3d_models/parts/LampButton.3mf deleted file mode 100644 index 78cc078..0000000 Binary files a/3d_models/parts/LampButton.3mf and /dev/null differ diff --git a/3d_models/parts/LampCap.3mf b/3d_models/parts/LampCap.3mf deleted file mode 100644 index ff51fab..0000000 Binary files a/3d_models/parts/LampCap.3mf and /dev/null differ diff --git a/3d_models/parts/LampGlass.3mf b/3d_models/parts/LampGlass.3mf deleted file mode 100644 index 68aa94e..0000000 Binary files a/3d_models/parts/LampGlass.3mf and /dev/null differ diff --git a/3d_models/parts/LaptopCase.3mf b/3d_models/parts/LaptopCase.3mf deleted file mode 100644 index 1c9fd71..0000000 Binary files a/3d_models/parts/LaptopCase.3mf and /dev/null differ diff --git a/3d_models/parts/LaptopScreen.3mf b/3d_models/parts/LaptopScreen.3mf deleted file mode 100644 index 7fb1ab0..0000000 Binary files a/3d_models/parts/LaptopScreen.3mf and /dev/null differ diff --git a/3d_models/parts/Mug.3mf b/3d_models/parts/Mug.3mf deleted file mode 100644 index aa35df6..0000000 Binary files a/3d_models/parts/Mug.3mf and /dev/null differ diff --git a/3d_models/parts/MugCoffee.3mf b/3d_models/parts/MugCoffee.3mf deleted file mode 100644 index 406545a..0000000 Binary files a/3d_models/parts/MugCoffee.3mf and /dev/null differ diff --git a/3d_models/parts/Neck.3mf b/3d_models/parts/Neck.3mf deleted file mode 100644 index fdbb60c..0000000 Binary files a/3d_models/parts/Neck.3mf and /dev/null differ diff --git a/3d_models/parts/Pillow.3mf b/3d_models/parts/Pillow.3mf deleted file mode 100644 index 720398c..0000000 Binary files a/3d_models/parts/Pillow.3mf and /dev/null differ diff --git a/3d_models/parts/fs0307/3mf/AiEmblem.3mf b/3d_models/parts/fs0307/3mf/AiEmblem.3mf new file mode 100644 index 0000000..e7924bd Binary files /dev/null and b/3d_models/parts/fs0307/3mf/AiEmblem.3mf differ diff --git a/3d_models/parts/fs0307/3mf/Bell.3mf b/3d_models/parts/fs0307/3mf/Bell.3mf new file mode 100644 index 0000000..bb0c339 Binary files /dev/null and b/3d_models/parts/fs0307/3mf/Bell.3mf differ diff --git a/3d_models/parts/fs0307/3mf/Belly.3mf b/3d_models/parts/fs0307/3mf/Belly.3mf new file mode 100644 index 0000000..a3efd95 Binary files /dev/null and b/3d_models/parts/fs0307/3mf/Belly.3mf differ diff --git a/3d_models/parts/fs0307/3mf/Chair.3mf b/3d_models/parts/fs0307/3mf/Chair.3mf new file mode 100644 index 0000000..3646f17 Binary files /dev/null and b/3d_models/parts/fs0307/3mf/Chair.3mf differ diff --git a/3d_models/parts/fs0307/3mf/Chest.3mf b/3d_models/parts/fs0307/3mf/Chest.3mf new file mode 100644 index 0000000..7afa1f5 Binary files /dev/null and b/3d_models/parts/fs0307/3mf/Chest.3mf differ diff --git a/3d_models/parts/fs0307/3mf/Coffee.3mf b/3d_models/parts/fs0307/3mf/Coffee.3mf new file mode 100644 index 0000000..46b4a49 Binary files /dev/null and b/3d_models/parts/fs0307/3mf/Coffee.3mf differ diff --git a/3d_models/parts/fs0307/3mf/Desk.3mf b/3d_models/parts/fs0307/3mf/Desk.3mf new file mode 100644 index 0000000..85c13ce Binary files /dev/null and b/3d_models/parts/fs0307/3mf/Desk.3mf differ diff --git a/3d_models/parts/fs0307/3mf/DeskPad.3mf b/3d_models/parts/fs0307/3mf/DeskPad.3mf new file mode 100644 index 0000000..114fe0a Binary files /dev/null and b/3d_models/parts/fs0307/3mf/DeskPad.3mf differ diff --git a/3d_models/parts/fs0307/3mf/DeskTop.3mf b/3d_models/parts/fs0307/3mf/DeskTop.3mf new file mode 100644 index 0000000..16c1d74 Binary files /dev/null and b/3d_models/parts/fs0307/3mf/DeskTop.3mf differ diff --git a/3d_models/parts/fs0307/3mf/ForearmLeft.3mf b/3d_models/parts/fs0307/3mf/ForearmLeft.3mf new file mode 100644 index 0000000..f96a24f Binary files /dev/null and b/3d_models/parts/fs0307/3mf/ForearmLeft.3mf differ diff --git a/3d_models/parts/fs0307/3mf/ForearmRight.3mf b/3d_models/parts/fs0307/3mf/ForearmRight.3mf new file mode 100644 index 0000000..70159f2 Binary files /dev/null and b/3d_models/parts/fs0307/3mf/ForearmRight.3mf differ diff --git a/3d_models/parts/fs0307/3mf/Hat.3mf b/3d_models/parts/fs0307/3mf/Hat.3mf new file mode 100644 index 0000000..d42ba0e Binary files /dev/null and b/3d_models/parts/fs0307/3mf/Hat.3mf differ diff --git a/3d_models/parts/fs0307/3mf/Head.3mf b/3d_models/parts/fs0307/3mf/Head.3mf new file mode 100644 index 0000000..4859b15 Binary files /dev/null and b/3d_models/parts/fs0307/3mf/Head.3mf differ diff --git a/3d_models/parts/fs0307/3mf/LampBase.3mf b/3d_models/parts/fs0307/3mf/LampBase.3mf new file mode 100644 index 0000000..8466c75 Binary files /dev/null and b/3d_models/parts/fs0307/3mf/LampBase.3mf differ diff --git a/3d_models/parts/fs0307/3mf/LampButton.3mf b/3d_models/parts/fs0307/3mf/LampButton.3mf new file mode 100644 index 0000000..767415b Binary files /dev/null and b/3d_models/parts/fs0307/3mf/LampButton.3mf differ diff --git a/3d_models/parts/fs0307/3mf/LampCap.3mf b/3d_models/parts/fs0307/3mf/LampCap.3mf new file mode 100644 index 0000000..5dadef0 Binary files /dev/null and b/3d_models/parts/fs0307/3mf/LampCap.3mf differ diff --git a/3d_models/parts/fs0307/3mf/LampDiffuser.3mf b/3d_models/parts/fs0307/3mf/LampDiffuser.3mf new file mode 100644 index 0000000..9c86f6b Binary files /dev/null and b/3d_models/parts/fs0307/3mf/LampDiffuser.3mf differ diff --git a/3d_models/parts/fs0307/3mf/LaptopCase.3mf b/3d_models/parts/fs0307/3mf/LaptopCase.3mf new file mode 100644 index 0000000..f6050eb Binary files /dev/null and b/3d_models/parts/fs0307/3mf/LaptopCase.3mf differ diff --git a/3d_models/parts/fs0307/3mf/LaptopScreen.3mf b/3d_models/parts/fs0307/3mf/LaptopScreen.3mf new file mode 100644 index 0000000..c676bf0 Binary files /dev/null and b/3d_models/parts/fs0307/3mf/LaptopScreen.3mf differ diff --git a/3d_models/parts/fs0307/3mf/Mug.3mf b/3d_models/parts/fs0307/3mf/Mug.3mf new file mode 100644 index 0000000..1ca0595 Binary files /dev/null and b/3d_models/parts/fs0307/3mf/Mug.3mf differ diff --git a/3d_models/parts/fs0307/3mf/Neck.3mf b/3d_models/parts/fs0307/3mf/Neck.3mf new file mode 100644 index 0000000..6f21f01 Binary files /dev/null and b/3d_models/parts/fs0307/3mf/Neck.3mf differ diff --git a/3d_models/parts/fs0307/3mf/SeatLeft.3mf b/3d_models/parts/fs0307/3mf/SeatLeft.3mf new file mode 100644 index 0000000..d90c7c7 Binary files /dev/null and b/3d_models/parts/fs0307/3mf/SeatLeft.3mf differ diff --git a/3d_models/parts/fs0307/3mf/SeatRight.3mf b/3d_models/parts/fs0307/3mf/SeatRight.3mf new file mode 100644 index 0000000..92cafac Binary files /dev/null and b/3d_models/parts/fs0307/3mf/SeatRight.3mf differ diff --git a/3d_models/parts/fs0307/3mf/ServoSizingTester.3mf b/3d_models/parts/fs0307/3mf/ServoSizingTester.3mf new file mode 100644 index 0000000..35baa65 Binary files /dev/null and b/3d_models/parts/fs0307/3mf/ServoSizingTester.3mf differ diff --git a/3d_models/parts/fs0307/3mf/UpperArm.3mf b/3d_models/parts/fs0307/3mf/UpperArm.3mf new file mode 100644 index 0000000..c92e094 Binary files /dev/null and b/3d_models/parts/fs0307/3mf/UpperArm.3mf differ diff --git a/3d_models/parts/fs0307/README.md b/3d_models/parts/fs0307/README.md new file mode 100644 index 0000000..f83380e --- /dev/null +++ b/3d_models/parts/fs0307/README.md @@ -0,0 +1,17 @@ +# Feetech FS0307 + +- `servo_id`: fs0307 +- `servo_body_width`: 20 mm +- `servo_body_height`: 13 mm +- `servo_mount_width`: 3.8 mm +- `servo_mount_thickness`: 1 mm +- `servo_gearbox_height`: 3.7 mm +- `servo_gearbox_padding`: 6.5 mm +- `servo_shaft_housing_height`: 4 mm +- `servo_shaft_position`: 4.5 mm +- `servo_shaft_length`: 3.3 mm +- `servo_shaft_radius`: 1.83 mm +- `servo_body_depth`: 8.6 mm +- `servo_mount_hole_radius`: 1 mm +- `servo_mount_hole_depth_offset`: 0 mm +- `servo_mount_hole_width_offset`: 0 mm diff --git a/3d_models/parts/fs0307/stl/AiEmblem.stl b/3d_models/parts/fs0307/stl/AiEmblem.stl new file mode 100644 index 0000000..ac5b73c Binary files /dev/null and b/3d_models/parts/fs0307/stl/AiEmblem.stl differ diff --git a/3d_models/parts/fs0307/stl/Bell.stl b/3d_models/parts/fs0307/stl/Bell.stl new file mode 100644 index 0000000..7f545c5 Binary files /dev/null and b/3d_models/parts/fs0307/stl/Bell.stl differ diff --git a/3d_models/parts/fs0307/stl/Belly.stl b/3d_models/parts/fs0307/stl/Belly.stl new file mode 100644 index 0000000..4c59671 Binary files /dev/null and b/3d_models/parts/fs0307/stl/Belly.stl differ diff --git a/3d_models/parts/fs0307/stl/Chair.stl b/3d_models/parts/fs0307/stl/Chair.stl new file mode 100644 index 0000000..5efb94b Binary files /dev/null and b/3d_models/parts/fs0307/stl/Chair.stl differ diff --git a/3d_models/parts/fs0307/stl/Chest.stl b/3d_models/parts/fs0307/stl/Chest.stl new file mode 100644 index 0000000..8aa139a Binary files /dev/null and b/3d_models/parts/fs0307/stl/Chest.stl differ diff --git a/3d_models/parts/fs0307/stl/Coffee.stl b/3d_models/parts/fs0307/stl/Coffee.stl new file mode 100644 index 0000000..11d21cc Binary files /dev/null and b/3d_models/parts/fs0307/stl/Coffee.stl differ diff --git a/3d_models/parts/fs0307/stl/Desk.stl b/3d_models/parts/fs0307/stl/Desk.stl new file mode 100644 index 0000000..169f69c Binary files /dev/null and b/3d_models/parts/fs0307/stl/Desk.stl differ diff --git a/3d_models/parts/fs0307/stl/DeskPad.stl b/3d_models/parts/fs0307/stl/DeskPad.stl new file mode 100644 index 0000000..c8025a1 Binary files /dev/null and b/3d_models/parts/fs0307/stl/DeskPad.stl differ diff --git a/3d_models/parts/fs0307/stl/DeskTop.stl b/3d_models/parts/fs0307/stl/DeskTop.stl new file mode 100644 index 0000000..9b40281 Binary files /dev/null and b/3d_models/parts/fs0307/stl/DeskTop.stl differ diff --git a/3d_models/parts/fs0307/stl/ForearmLeft.stl b/3d_models/parts/fs0307/stl/ForearmLeft.stl new file mode 100644 index 0000000..307313a Binary files /dev/null and b/3d_models/parts/fs0307/stl/ForearmLeft.stl differ diff --git a/3d_models/parts/fs0307/stl/ForearmRight.stl b/3d_models/parts/fs0307/stl/ForearmRight.stl new file mode 100644 index 0000000..b0040d3 Binary files /dev/null and b/3d_models/parts/fs0307/stl/ForearmRight.stl differ diff --git a/3d_models/parts/fs0307/stl/Hat.stl b/3d_models/parts/fs0307/stl/Hat.stl new file mode 100644 index 0000000..8aff8a9 Binary files /dev/null and b/3d_models/parts/fs0307/stl/Hat.stl differ diff --git a/3d_models/parts/fs0307/stl/Head.stl b/3d_models/parts/fs0307/stl/Head.stl new file mode 100644 index 0000000..32aeabe Binary files /dev/null and b/3d_models/parts/fs0307/stl/Head.stl differ diff --git a/3d_models/parts/fs0307/stl/LampBase.stl b/3d_models/parts/fs0307/stl/LampBase.stl new file mode 100644 index 0000000..7801c44 Binary files /dev/null and b/3d_models/parts/fs0307/stl/LampBase.stl differ diff --git a/3d_models/parts/fs0307/stl/LampButton.stl b/3d_models/parts/fs0307/stl/LampButton.stl new file mode 100644 index 0000000..f8a4e43 Binary files /dev/null and b/3d_models/parts/fs0307/stl/LampButton.stl differ diff --git a/3d_models/parts/fs0307/stl/LampCap.stl b/3d_models/parts/fs0307/stl/LampCap.stl new file mode 100644 index 0000000..d1c0dcc Binary files /dev/null and b/3d_models/parts/fs0307/stl/LampCap.stl differ diff --git a/3d_models/parts/fs0307/stl/LampDiffuser.stl b/3d_models/parts/fs0307/stl/LampDiffuser.stl new file mode 100644 index 0000000..bd3883f Binary files /dev/null and b/3d_models/parts/fs0307/stl/LampDiffuser.stl differ diff --git a/3d_models/parts/fs0307/stl/LaptopCase.stl b/3d_models/parts/fs0307/stl/LaptopCase.stl new file mode 100644 index 0000000..be1be23 Binary files /dev/null and b/3d_models/parts/fs0307/stl/LaptopCase.stl differ diff --git a/3d_models/parts/fs0307/stl/LaptopScreen.stl b/3d_models/parts/fs0307/stl/LaptopScreen.stl new file mode 100644 index 0000000..83307a7 Binary files /dev/null and b/3d_models/parts/fs0307/stl/LaptopScreen.stl differ diff --git a/3d_models/parts/fs0307/stl/Mug.stl b/3d_models/parts/fs0307/stl/Mug.stl new file mode 100644 index 0000000..bee9adb Binary files /dev/null and b/3d_models/parts/fs0307/stl/Mug.stl differ diff --git a/3d_models/parts/fs0307/stl/Neck.stl b/3d_models/parts/fs0307/stl/Neck.stl new file mode 100644 index 0000000..ea9c684 Binary files /dev/null and b/3d_models/parts/fs0307/stl/Neck.stl differ diff --git a/3d_models/parts/fs0307/stl/SeatLeft.stl b/3d_models/parts/fs0307/stl/SeatLeft.stl new file mode 100644 index 0000000..5b16261 Binary files /dev/null and b/3d_models/parts/fs0307/stl/SeatLeft.stl differ diff --git a/3d_models/parts/fs0307/stl/SeatRight.stl b/3d_models/parts/fs0307/stl/SeatRight.stl new file mode 100644 index 0000000..aa816c6 Binary files /dev/null and b/3d_models/parts/fs0307/stl/SeatRight.stl differ diff --git a/3d_models/parts/fs0307/stl/ServoSizingTester.stl b/3d_models/parts/fs0307/stl/ServoSizingTester.stl new file mode 100644 index 0000000..9deaf47 Binary files /dev/null and b/3d_models/parts/fs0307/stl/ServoSizingTester.stl differ diff --git a/3d_models/parts/fs0307/stl/UpperArm.stl b/3d_models/parts/fs0307/stl/UpperArm.stl new file mode 100644 index 0000000..7cb607d Binary files /dev/null and b/3d_models/parts/fs0307/stl/UpperArm.stl differ diff --git a/3d_models/parts/hd1370a/3mf/AiEmblem.3mf b/3d_models/parts/hd1370a/3mf/AiEmblem.3mf new file mode 100644 index 0000000..f8df7a1 Binary files /dev/null and b/3d_models/parts/hd1370a/3mf/AiEmblem.3mf differ diff --git a/3d_models/parts/hd1370a/3mf/Bell.3mf b/3d_models/parts/hd1370a/3mf/Bell.3mf new file mode 100644 index 0000000..f69077c Binary files /dev/null and b/3d_models/parts/hd1370a/3mf/Bell.3mf differ diff --git a/3d_models/parts/hd1370a/3mf/Belly.3mf b/3d_models/parts/hd1370a/3mf/Belly.3mf new file mode 100644 index 0000000..07d426e Binary files /dev/null and b/3d_models/parts/hd1370a/3mf/Belly.3mf differ diff --git a/3d_models/parts/hd1370a/3mf/Chair.3mf b/3d_models/parts/hd1370a/3mf/Chair.3mf new file mode 100644 index 0000000..38e3df2 Binary files /dev/null and b/3d_models/parts/hd1370a/3mf/Chair.3mf differ diff --git a/3d_models/parts/hd1370a/3mf/Chest.3mf b/3d_models/parts/hd1370a/3mf/Chest.3mf new file mode 100644 index 0000000..9ddc816 Binary files /dev/null and b/3d_models/parts/hd1370a/3mf/Chest.3mf differ diff --git a/3d_models/parts/hd1370a/3mf/Coffee.3mf b/3d_models/parts/hd1370a/3mf/Coffee.3mf new file mode 100644 index 0000000..d1e9123 Binary files /dev/null and b/3d_models/parts/hd1370a/3mf/Coffee.3mf differ diff --git a/3d_models/parts/hd1370a/3mf/Desk.3mf b/3d_models/parts/hd1370a/3mf/Desk.3mf new file mode 100644 index 0000000..b7d4ce5 Binary files /dev/null and b/3d_models/parts/hd1370a/3mf/Desk.3mf differ diff --git a/3d_models/parts/hd1370a/3mf/DeskPad.3mf b/3d_models/parts/hd1370a/3mf/DeskPad.3mf new file mode 100644 index 0000000..4452572 Binary files /dev/null and b/3d_models/parts/hd1370a/3mf/DeskPad.3mf differ diff --git a/3d_models/parts/hd1370a/3mf/DeskTop.3mf b/3d_models/parts/hd1370a/3mf/DeskTop.3mf new file mode 100644 index 0000000..563cc9f Binary files /dev/null and b/3d_models/parts/hd1370a/3mf/DeskTop.3mf differ diff --git a/3d_models/parts/hd1370a/3mf/ForearmLeft.3mf b/3d_models/parts/hd1370a/3mf/ForearmLeft.3mf new file mode 100644 index 0000000..1815780 Binary files /dev/null and b/3d_models/parts/hd1370a/3mf/ForearmLeft.3mf differ diff --git a/3d_models/parts/hd1370a/3mf/ForearmRight.3mf b/3d_models/parts/hd1370a/3mf/ForearmRight.3mf new file mode 100644 index 0000000..51707f6 Binary files /dev/null and b/3d_models/parts/hd1370a/3mf/ForearmRight.3mf differ diff --git a/3d_models/parts/hd1370a/3mf/Hat.3mf b/3d_models/parts/hd1370a/3mf/Hat.3mf new file mode 100644 index 0000000..790ec1d Binary files /dev/null and b/3d_models/parts/hd1370a/3mf/Hat.3mf differ diff --git a/3d_models/parts/hd1370a/3mf/Head.3mf b/3d_models/parts/hd1370a/3mf/Head.3mf new file mode 100644 index 0000000..34ecfbf Binary files /dev/null and b/3d_models/parts/hd1370a/3mf/Head.3mf differ diff --git a/3d_models/parts/hd1370a/3mf/LampBase.3mf b/3d_models/parts/hd1370a/3mf/LampBase.3mf new file mode 100644 index 0000000..f0645bd Binary files /dev/null and b/3d_models/parts/hd1370a/3mf/LampBase.3mf differ diff --git a/3d_models/parts/hd1370a/3mf/LampButton.3mf b/3d_models/parts/hd1370a/3mf/LampButton.3mf new file mode 100644 index 0000000..d32edb2 Binary files /dev/null and b/3d_models/parts/hd1370a/3mf/LampButton.3mf differ diff --git a/3d_models/parts/hd1370a/3mf/LampCap.3mf b/3d_models/parts/hd1370a/3mf/LampCap.3mf new file mode 100644 index 0000000..53dc365 Binary files /dev/null and b/3d_models/parts/hd1370a/3mf/LampCap.3mf differ diff --git a/3d_models/parts/hd1370a/3mf/LampDiffuser.3mf b/3d_models/parts/hd1370a/3mf/LampDiffuser.3mf new file mode 100644 index 0000000..9dba9a3 Binary files /dev/null and b/3d_models/parts/hd1370a/3mf/LampDiffuser.3mf differ diff --git a/3d_models/parts/hd1370a/3mf/LaptopCase.3mf b/3d_models/parts/hd1370a/3mf/LaptopCase.3mf new file mode 100644 index 0000000..a20503e Binary files /dev/null and b/3d_models/parts/hd1370a/3mf/LaptopCase.3mf differ diff --git a/3d_models/parts/hd1370a/3mf/LaptopScreen.3mf b/3d_models/parts/hd1370a/3mf/LaptopScreen.3mf new file mode 100644 index 0000000..bbca243 Binary files /dev/null and b/3d_models/parts/hd1370a/3mf/LaptopScreen.3mf differ diff --git a/3d_models/parts/hd1370a/3mf/Mug.3mf b/3d_models/parts/hd1370a/3mf/Mug.3mf new file mode 100644 index 0000000..f0284f1 Binary files /dev/null and b/3d_models/parts/hd1370a/3mf/Mug.3mf differ diff --git a/3d_models/parts/hd1370a/3mf/Neck.3mf b/3d_models/parts/hd1370a/3mf/Neck.3mf new file mode 100644 index 0000000..bd662ac Binary files /dev/null and b/3d_models/parts/hd1370a/3mf/Neck.3mf differ diff --git a/3d_models/parts/hd1370a/3mf/SeatLeft.3mf b/3d_models/parts/hd1370a/3mf/SeatLeft.3mf new file mode 100644 index 0000000..beebca2 Binary files /dev/null and b/3d_models/parts/hd1370a/3mf/SeatLeft.3mf differ diff --git a/3d_models/parts/hd1370a/3mf/SeatRight.3mf b/3d_models/parts/hd1370a/3mf/SeatRight.3mf new file mode 100644 index 0000000..8490d1a Binary files /dev/null and b/3d_models/parts/hd1370a/3mf/SeatRight.3mf differ diff --git a/3d_models/parts/hd1370a/3mf/ServoSizingTester.3mf b/3d_models/parts/hd1370a/3mf/ServoSizingTester.3mf new file mode 100644 index 0000000..7ce217c Binary files /dev/null and b/3d_models/parts/hd1370a/3mf/ServoSizingTester.3mf differ diff --git a/3d_models/parts/hd1370a/3mf/UpperArm.3mf b/3d_models/parts/hd1370a/3mf/UpperArm.3mf new file mode 100644 index 0000000..9fe6229 Binary files /dev/null and b/3d_models/parts/hd1370a/3mf/UpperArm.3mf differ diff --git a/3d_models/parts/hd1370a/README.md b/3d_models/parts/hd1370a/README.md new file mode 100644 index 0000000..41f7fb7 --- /dev/null +++ b/3d_models/parts/hd1370a/README.md @@ -0,0 +1,17 @@ +# PowerHD HD-1370A + +- `servo_id`: hd1370a +- `servo_body_width`: 20 mm +- `servo_body_height`: 13.1 mm +- `servo_mount_width`: 3.7 mm +- `servo_mount_thickness`: 0.9 mm +- `servo_gearbox_height`: 3.9 mm +- `servo_gearbox_padding`: 5.4 mm +- `servo_shaft_housing_height`: 3.1 mm +- `servo_shaft_position`: 4.5 mm +- `servo_shaft_length`: 2.9 mm +- `servo_shaft_radius`: 1.9 mm +- `servo_body_depth`: 8.6 mm +- `servo_mount_hole_radius`: 1 mm +- `servo_mount_hole_depth_offset`: 0 mm +- `servo_mount_hole_width_offset`: 0 mm diff --git a/3d_models/parts/hd1370a/stl/AiEmblem.stl b/3d_models/parts/hd1370a/stl/AiEmblem.stl new file mode 100644 index 0000000..febbe20 Binary files /dev/null and b/3d_models/parts/hd1370a/stl/AiEmblem.stl differ diff --git a/3d_models/parts/hd1370a/stl/Bell.stl b/3d_models/parts/hd1370a/stl/Bell.stl new file mode 100644 index 0000000..6fe8ba3 Binary files /dev/null and b/3d_models/parts/hd1370a/stl/Bell.stl differ diff --git a/3d_models/parts/hd1370a/stl/Belly.stl b/3d_models/parts/hd1370a/stl/Belly.stl new file mode 100644 index 0000000..d391da5 Binary files /dev/null and b/3d_models/parts/hd1370a/stl/Belly.stl differ diff --git a/3d_models/parts/hd1370a/stl/Chair.stl b/3d_models/parts/hd1370a/stl/Chair.stl new file mode 100644 index 0000000..3ebde7a Binary files /dev/null and b/3d_models/parts/hd1370a/stl/Chair.stl differ diff --git a/3d_models/parts/hd1370a/stl/Chest.stl b/3d_models/parts/hd1370a/stl/Chest.stl new file mode 100644 index 0000000..d17c31c Binary files /dev/null and b/3d_models/parts/hd1370a/stl/Chest.stl differ diff --git a/3d_models/parts/hd1370a/stl/Coffee.stl b/3d_models/parts/hd1370a/stl/Coffee.stl new file mode 100644 index 0000000..11d21cc Binary files /dev/null and b/3d_models/parts/hd1370a/stl/Coffee.stl differ diff --git a/3d_models/parts/hd1370a/stl/Desk.stl b/3d_models/parts/hd1370a/stl/Desk.stl new file mode 100644 index 0000000..8452569 Binary files /dev/null and b/3d_models/parts/hd1370a/stl/Desk.stl differ diff --git a/3d_models/parts/hd1370a/stl/DeskPad.stl b/3d_models/parts/hd1370a/stl/DeskPad.stl new file mode 100644 index 0000000..aaa9990 Binary files /dev/null and b/3d_models/parts/hd1370a/stl/DeskPad.stl differ diff --git a/3d_models/parts/hd1370a/stl/DeskTop.stl b/3d_models/parts/hd1370a/stl/DeskTop.stl new file mode 100644 index 0000000..f514a80 Binary files /dev/null and b/3d_models/parts/hd1370a/stl/DeskTop.stl differ diff --git a/3d_models/parts/hd1370a/stl/ForearmLeft.stl b/3d_models/parts/hd1370a/stl/ForearmLeft.stl new file mode 100644 index 0000000..8b6ed5c Binary files /dev/null and b/3d_models/parts/hd1370a/stl/ForearmLeft.stl differ diff --git a/3d_models/parts/hd1370a/stl/ForearmRight.stl b/3d_models/parts/hd1370a/stl/ForearmRight.stl new file mode 100644 index 0000000..89142b7 Binary files /dev/null and b/3d_models/parts/hd1370a/stl/ForearmRight.stl differ diff --git a/3d_models/parts/hd1370a/stl/Hat.stl b/3d_models/parts/hd1370a/stl/Hat.stl new file mode 100644 index 0000000..291a914 Binary files /dev/null and b/3d_models/parts/hd1370a/stl/Hat.stl differ diff --git a/3d_models/parts/hd1370a/stl/Head.stl b/3d_models/parts/hd1370a/stl/Head.stl new file mode 100644 index 0000000..963a6d0 Binary files /dev/null and b/3d_models/parts/hd1370a/stl/Head.stl differ diff --git a/3d_models/parts/hd1370a/stl/LampBase.stl b/3d_models/parts/hd1370a/stl/LampBase.stl new file mode 100644 index 0000000..596c899 Binary files /dev/null and b/3d_models/parts/hd1370a/stl/LampBase.stl differ diff --git a/3d_models/parts/hd1370a/stl/LampButton.stl b/3d_models/parts/hd1370a/stl/LampButton.stl new file mode 100644 index 0000000..d89a1db Binary files /dev/null and b/3d_models/parts/hd1370a/stl/LampButton.stl differ diff --git a/3d_models/parts/hd1370a/stl/LampCap.stl b/3d_models/parts/hd1370a/stl/LampCap.stl new file mode 100644 index 0000000..cc82298 Binary files /dev/null and b/3d_models/parts/hd1370a/stl/LampCap.stl differ diff --git a/3d_models/parts/hd1370a/stl/LampDiffuser.stl b/3d_models/parts/hd1370a/stl/LampDiffuser.stl new file mode 100644 index 0000000..d9ce98a Binary files /dev/null and b/3d_models/parts/hd1370a/stl/LampDiffuser.stl differ diff --git a/3d_models/parts/hd1370a/stl/LaptopCase.stl b/3d_models/parts/hd1370a/stl/LaptopCase.stl new file mode 100644 index 0000000..bd3712c Binary files /dev/null and b/3d_models/parts/hd1370a/stl/LaptopCase.stl differ diff --git a/3d_models/parts/hd1370a/stl/LaptopScreen.stl b/3d_models/parts/hd1370a/stl/LaptopScreen.stl new file mode 100644 index 0000000..30daa54 Binary files /dev/null and b/3d_models/parts/hd1370a/stl/LaptopScreen.stl differ diff --git a/3d_models/parts/hd1370a/stl/Mug.stl b/3d_models/parts/hd1370a/stl/Mug.stl new file mode 100644 index 0000000..af00762 Binary files /dev/null and b/3d_models/parts/hd1370a/stl/Mug.stl differ diff --git a/3d_models/parts/hd1370a/stl/Neck.stl b/3d_models/parts/hd1370a/stl/Neck.stl new file mode 100644 index 0000000..ed5c623 Binary files /dev/null and b/3d_models/parts/hd1370a/stl/Neck.stl differ diff --git a/3d_models/parts/hd1370a/stl/SeatLeft.stl b/3d_models/parts/hd1370a/stl/SeatLeft.stl new file mode 100644 index 0000000..336ae46 Binary files /dev/null and b/3d_models/parts/hd1370a/stl/SeatLeft.stl differ diff --git a/3d_models/parts/hd1370a/stl/SeatRight.stl b/3d_models/parts/hd1370a/stl/SeatRight.stl new file mode 100644 index 0000000..7fbb15f Binary files /dev/null and b/3d_models/parts/hd1370a/stl/SeatRight.stl differ diff --git a/3d_models/parts/hd1370a/stl/ServoSizingTester.stl b/3d_models/parts/hd1370a/stl/ServoSizingTester.stl new file mode 100644 index 0000000..f6bef8c Binary files /dev/null and b/3d_models/parts/hd1370a/stl/ServoSizingTester.stl differ diff --git a/3d_models/parts/hd1370a/stl/UpperArm.stl b/3d_models/parts/hd1370a/stl/UpperArm.stl new file mode 100644 index 0000000..a950b65 Binary files /dev/null and b/3d_models/parts/hd1370a/stl/UpperArm.stl differ diff --git a/3d_models/parts/sg90/3mf/AiEmblem.3mf b/3d_models/parts/sg90/3mf/AiEmblem.3mf new file mode 100644 index 0000000..77650a0 Binary files /dev/null and b/3d_models/parts/sg90/3mf/AiEmblem.3mf differ diff --git a/3d_models/parts/sg90/3mf/Bell.3mf b/3d_models/parts/sg90/3mf/Bell.3mf new file mode 100644 index 0000000..7379710 Binary files /dev/null and b/3d_models/parts/sg90/3mf/Bell.3mf differ diff --git a/3d_models/parts/sg90/3mf/Belly.3mf b/3d_models/parts/sg90/3mf/Belly.3mf new file mode 100644 index 0000000..3da3306 Binary files /dev/null and b/3d_models/parts/sg90/3mf/Belly.3mf differ diff --git a/3d_models/parts/sg90/3mf/Chair.3mf b/3d_models/parts/sg90/3mf/Chair.3mf new file mode 100644 index 0000000..2a27690 Binary files /dev/null and b/3d_models/parts/sg90/3mf/Chair.3mf differ diff --git a/3d_models/parts/sg90/3mf/Chest.3mf b/3d_models/parts/sg90/3mf/Chest.3mf new file mode 100644 index 0000000..388e81d Binary files /dev/null and b/3d_models/parts/sg90/3mf/Chest.3mf differ diff --git a/3d_models/parts/sg90/3mf/Coffee.3mf b/3d_models/parts/sg90/3mf/Coffee.3mf new file mode 100644 index 0000000..ac30395 Binary files /dev/null and b/3d_models/parts/sg90/3mf/Coffee.3mf differ diff --git a/3d_models/parts/sg90/3mf/Desk.3mf b/3d_models/parts/sg90/3mf/Desk.3mf new file mode 100644 index 0000000..ac263c2 Binary files /dev/null and b/3d_models/parts/sg90/3mf/Desk.3mf differ diff --git a/3d_models/parts/sg90/3mf/DeskPad.3mf b/3d_models/parts/sg90/3mf/DeskPad.3mf new file mode 100644 index 0000000..05cb6c7 Binary files /dev/null and b/3d_models/parts/sg90/3mf/DeskPad.3mf differ diff --git a/3d_models/parts/sg90/3mf/DeskTop.3mf b/3d_models/parts/sg90/3mf/DeskTop.3mf new file mode 100644 index 0000000..1e71fa7 Binary files /dev/null and b/3d_models/parts/sg90/3mf/DeskTop.3mf differ diff --git a/3d_models/parts/sg90/3mf/ForearmLeft.3mf b/3d_models/parts/sg90/3mf/ForearmLeft.3mf new file mode 100644 index 0000000..3f691c3 Binary files /dev/null and b/3d_models/parts/sg90/3mf/ForearmLeft.3mf differ diff --git a/3d_models/parts/sg90/3mf/ForearmRight.3mf b/3d_models/parts/sg90/3mf/ForearmRight.3mf new file mode 100644 index 0000000..cf73e83 Binary files /dev/null and b/3d_models/parts/sg90/3mf/ForearmRight.3mf differ diff --git a/3d_models/parts/sg90/3mf/Hat.3mf b/3d_models/parts/sg90/3mf/Hat.3mf new file mode 100644 index 0000000..1ccd9cf Binary files /dev/null and b/3d_models/parts/sg90/3mf/Hat.3mf differ diff --git a/3d_models/parts/sg90/3mf/Head.3mf b/3d_models/parts/sg90/3mf/Head.3mf new file mode 100644 index 0000000..9d880ef Binary files /dev/null and b/3d_models/parts/sg90/3mf/Head.3mf differ diff --git a/3d_models/parts/sg90/3mf/LampBase.3mf b/3d_models/parts/sg90/3mf/LampBase.3mf new file mode 100644 index 0000000..f2d2111 Binary files /dev/null and b/3d_models/parts/sg90/3mf/LampBase.3mf differ diff --git a/3d_models/parts/sg90/3mf/LampButton.3mf b/3d_models/parts/sg90/3mf/LampButton.3mf new file mode 100644 index 0000000..e3d9221 Binary files /dev/null and b/3d_models/parts/sg90/3mf/LampButton.3mf differ diff --git a/3d_models/parts/sg90/3mf/LampCap.3mf b/3d_models/parts/sg90/3mf/LampCap.3mf new file mode 100644 index 0000000..d98b2a7 Binary files /dev/null and b/3d_models/parts/sg90/3mf/LampCap.3mf differ diff --git a/3d_models/parts/sg90/3mf/LampDiffuser.3mf b/3d_models/parts/sg90/3mf/LampDiffuser.3mf new file mode 100644 index 0000000..a1708d6 Binary files /dev/null and b/3d_models/parts/sg90/3mf/LampDiffuser.3mf differ diff --git a/3d_models/parts/sg90/3mf/LaptopCase.3mf b/3d_models/parts/sg90/3mf/LaptopCase.3mf new file mode 100644 index 0000000..16865c3 Binary files /dev/null and b/3d_models/parts/sg90/3mf/LaptopCase.3mf differ diff --git a/3d_models/parts/sg90/3mf/LaptopScreen.3mf b/3d_models/parts/sg90/3mf/LaptopScreen.3mf new file mode 100644 index 0000000..b429881 Binary files /dev/null and b/3d_models/parts/sg90/3mf/LaptopScreen.3mf differ diff --git a/3d_models/parts/sg90/3mf/Mug.3mf b/3d_models/parts/sg90/3mf/Mug.3mf new file mode 100644 index 0000000..dab6ac3 Binary files /dev/null and b/3d_models/parts/sg90/3mf/Mug.3mf differ diff --git a/3d_models/parts/sg90/3mf/Neck.3mf b/3d_models/parts/sg90/3mf/Neck.3mf new file mode 100644 index 0000000..b9bfc95 Binary files /dev/null and b/3d_models/parts/sg90/3mf/Neck.3mf differ diff --git a/3d_models/parts/sg90/3mf/SeatLeft.3mf b/3d_models/parts/sg90/3mf/SeatLeft.3mf new file mode 100644 index 0000000..978ca45 Binary files /dev/null and b/3d_models/parts/sg90/3mf/SeatLeft.3mf differ diff --git a/3d_models/parts/sg90/3mf/SeatRight.3mf b/3d_models/parts/sg90/3mf/SeatRight.3mf new file mode 100644 index 0000000..7749882 Binary files /dev/null and b/3d_models/parts/sg90/3mf/SeatRight.3mf differ diff --git a/3d_models/parts/sg90/3mf/ServoSizingTester.3mf b/3d_models/parts/sg90/3mf/ServoSizingTester.3mf new file mode 100644 index 0000000..95599b7 Binary files /dev/null and b/3d_models/parts/sg90/3mf/ServoSizingTester.3mf differ diff --git a/3d_models/parts/sg90/3mf/UpperArm.3mf b/3d_models/parts/sg90/3mf/UpperArm.3mf new file mode 100644 index 0000000..b924eca Binary files /dev/null and b/3d_models/parts/sg90/3mf/UpperArm.3mf differ diff --git a/3d_models/parts/sg90/README.md b/3d_models/parts/sg90/README.md new file mode 100644 index 0000000..9bfa25a --- /dev/null +++ b/3d_models/parts/sg90/README.md @@ -0,0 +1,17 @@ +# Tower Pro SG90 + +- `servo_id`: sg90 +- `servo_body_width`: 22.7 mm +- `servo_body_height`: 17 mm +- `servo_mount_width`: 4.8 mm +- `servo_mount_thickness`: 2.4 mm +- `servo_gearbox_height`: 4.4 mm +- `servo_gearbox_padding`: 7.8 mm +- `servo_shaft_housing_height`: 3.5 mm +- `servo_shaft_position`: 5.8 mm +- `servo_shaft_length`: 3.3 mm +- `servo_shaft_radius`: 2.45 mm +- `servo_body_depth`: 12.2 mm +- `servo_mount_hole_radius`: 1.4 mm +- `servo_mount_hole_depth_offset`: 0 mm +- `servo_mount_hole_width_offset`: 0.07 mm diff --git a/3d_models/parts/sg90/stl/AiEmblem.stl b/3d_models/parts/sg90/stl/AiEmblem.stl new file mode 100644 index 0000000..167a55a Binary files /dev/null and b/3d_models/parts/sg90/stl/AiEmblem.stl differ diff --git a/3d_models/parts/sg90/stl/Bell.stl b/3d_models/parts/sg90/stl/Bell.stl new file mode 100644 index 0000000..327602d Binary files /dev/null and b/3d_models/parts/sg90/stl/Bell.stl differ diff --git a/3d_models/parts/sg90/stl/Belly.stl b/3d_models/parts/sg90/stl/Belly.stl new file mode 100644 index 0000000..36aa89e Binary files /dev/null and b/3d_models/parts/sg90/stl/Belly.stl differ diff --git a/3d_models/parts/sg90/stl/Chair.stl b/3d_models/parts/sg90/stl/Chair.stl new file mode 100644 index 0000000..73c8b47 Binary files /dev/null and b/3d_models/parts/sg90/stl/Chair.stl differ diff --git a/3d_models/parts/sg90/stl/Chest.stl b/3d_models/parts/sg90/stl/Chest.stl new file mode 100644 index 0000000..869990d Binary files /dev/null and b/3d_models/parts/sg90/stl/Chest.stl differ diff --git a/3d_models/parts/sg90/stl/Coffee.stl b/3d_models/parts/sg90/stl/Coffee.stl new file mode 100644 index 0000000..51837d0 Binary files /dev/null and b/3d_models/parts/sg90/stl/Coffee.stl differ diff --git a/3d_models/parts/sg90/stl/Desk.stl b/3d_models/parts/sg90/stl/Desk.stl new file mode 100644 index 0000000..6dadba4 Binary files /dev/null and b/3d_models/parts/sg90/stl/Desk.stl differ diff --git a/3d_models/parts/sg90/stl/DeskPad.stl b/3d_models/parts/sg90/stl/DeskPad.stl new file mode 100644 index 0000000..32ba67f Binary files /dev/null and b/3d_models/parts/sg90/stl/DeskPad.stl differ diff --git a/3d_models/parts/sg90/stl/DeskTop.stl b/3d_models/parts/sg90/stl/DeskTop.stl new file mode 100644 index 0000000..9bc964c Binary files /dev/null and b/3d_models/parts/sg90/stl/DeskTop.stl differ diff --git a/3d_models/parts/sg90/stl/ForearmLeft.stl b/3d_models/parts/sg90/stl/ForearmLeft.stl new file mode 100644 index 0000000..79b108e Binary files /dev/null and b/3d_models/parts/sg90/stl/ForearmLeft.stl differ diff --git a/3d_models/parts/sg90/stl/ForearmRight.stl b/3d_models/parts/sg90/stl/ForearmRight.stl new file mode 100644 index 0000000..4404827 Binary files /dev/null and b/3d_models/parts/sg90/stl/ForearmRight.stl differ diff --git a/3d_models/parts/sg90/stl/Hat.stl b/3d_models/parts/sg90/stl/Hat.stl new file mode 100644 index 0000000..92dbc87 Binary files /dev/null and b/3d_models/parts/sg90/stl/Hat.stl differ diff --git a/3d_models/parts/sg90/stl/Head.stl b/3d_models/parts/sg90/stl/Head.stl new file mode 100644 index 0000000..6908ecc Binary files /dev/null and b/3d_models/parts/sg90/stl/Head.stl differ diff --git a/3d_models/parts/sg90/stl/LampBase.stl b/3d_models/parts/sg90/stl/LampBase.stl new file mode 100644 index 0000000..88dd440 Binary files /dev/null and b/3d_models/parts/sg90/stl/LampBase.stl differ diff --git a/3d_models/parts/sg90/stl/LampButton.stl b/3d_models/parts/sg90/stl/LampButton.stl new file mode 100644 index 0000000..dfaedd3 Binary files /dev/null and b/3d_models/parts/sg90/stl/LampButton.stl differ diff --git a/3d_models/parts/sg90/stl/LampCap.stl b/3d_models/parts/sg90/stl/LampCap.stl new file mode 100644 index 0000000..e8a07e4 Binary files /dev/null and b/3d_models/parts/sg90/stl/LampCap.stl differ diff --git a/3d_models/parts/sg90/stl/LampDiffuser.stl b/3d_models/parts/sg90/stl/LampDiffuser.stl new file mode 100644 index 0000000..bcd53ec Binary files /dev/null and b/3d_models/parts/sg90/stl/LampDiffuser.stl differ diff --git a/3d_models/parts/sg90/stl/LaptopCase.stl b/3d_models/parts/sg90/stl/LaptopCase.stl new file mode 100644 index 0000000..4a18c43 Binary files /dev/null and b/3d_models/parts/sg90/stl/LaptopCase.stl differ diff --git a/3d_models/parts/sg90/stl/LaptopScreen.stl b/3d_models/parts/sg90/stl/LaptopScreen.stl new file mode 100644 index 0000000..523db55 Binary files /dev/null and b/3d_models/parts/sg90/stl/LaptopScreen.stl differ diff --git a/3d_models/parts/sg90/stl/Mug.stl b/3d_models/parts/sg90/stl/Mug.stl new file mode 100644 index 0000000..1b39836 Binary files /dev/null and b/3d_models/parts/sg90/stl/Mug.stl differ diff --git a/3d_models/parts/sg90/stl/Neck.stl b/3d_models/parts/sg90/stl/Neck.stl new file mode 100644 index 0000000..af2b8e2 Binary files /dev/null and b/3d_models/parts/sg90/stl/Neck.stl differ diff --git a/3d_models/parts/sg90/stl/SeatLeft.stl b/3d_models/parts/sg90/stl/SeatLeft.stl new file mode 100644 index 0000000..a451dfb Binary files /dev/null and b/3d_models/parts/sg90/stl/SeatLeft.stl differ diff --git a/3d_models/parts/sg90/stl/SeatRight.stl b/3d_models/parts/sg90/stl/SeatRight.stl new file mode 100644 index 0000000..af508aa Binary files /dev/null and b/3d_models/parts/sg90/stl/SeatRight.stl differ diff --git a/3d_models/parts/sg90/stl/ServoSizingTester.stl b/3d_models/parts/sg90/stl/ServoSizingTester.stl new file mode 100644 index 0000000..068f651 Binary files /dev/null and b/3d_models/parts/sg90/stl/ServoSizingTester.stl differ diff --git a/3d_models/parts/sg90/stl/UpperArm.stl b/3d_models/parts/sg90/stl/UpperArm.stl new file mode 100644 index 0000000..e30c37b Binary files /dev/null and b/3d_models/parts/sg90/stl/UpperArm.stl differ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 39f0f3b..5e38f72 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,13 +50,13 @@ npm test --prefix packages/tiny-engineer-antigravity **Integrations.** Add or extend tests in the package you change. Raw REST examples belong in [docs/integration.md](docs/integration.md). Prefer short timeouts and ignore network errors so a missing robot does not stall the agent. -**CAD.** Edit [`3d_models/cad/TinyEngineer.f3d`](3d_models/cad/TinyEngineer.f3d) **and** export the affected [`3d_models/parts/*.3mf`](3d_models/parts/). Keep CERN-OHL-S. Do not swap `AiEmblem.3mf` as a branding change. +**CAD.** Edit [`3d_models/cad/TinyEngineer.f3d`](3d_models/cad/TinyEngineer.f3d) **and** export the affected [`3d_models/parts/{servo_id}/3mf/*.3mf`](3d_models/parts/). Keep CERN-OHL-S. Do not swap `AiEmblem.3mf` as a branding change. -**Motion.** Stay inside the ranges in [docs/robot-movement.md](docs/robot-movement.md) / [`include/servos.h`](include/servos.h). Do not widen NVS servo clamps without testing on a real robot. Setup AP `POST /setup/servo` can use 0–180° to find limits; assembled motion must not. +**Motion.** Animations use −1..1 poses mapped to the saved min/max in [docs/robot-movement.md](docs/robot-movement.md). Stock defaults live in [`include/servos.h`](include/servos.h). Do not widen NVS servo clamps without testing on a real robot. Setup AP `POST /setup/servo` can use 0–180° to find limits; assembled motion must not. ## Bench safety -- Supply **5 V / ≥ 2 A**. Five HD-1370A stalls are ~1.3–1.6 A before Wi-Fi and audio ([docs/hardware/power.md](docs/hardware/power.md)). +- Supply **5 V / ≥ 2 A**. Five PowerHD HD-1370A stalls are ~1.3–1.6 A before Wi-Fi and audio; SG90 (recommended) typically stalls harder ([docs/hardware/power.md](docs/hardware/power.md)). - Never power servos from the ESP32 3.3 V LDO. - PCA9685 **VCC** = 3.3 V logic; **V+** = 5 V servo rail. Do not short them. - Leave the C3-Zero ceramic antenna clear of metal and dense plastic. diff --git a/README.md b/README.md index 619f93d..ee4eb93 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Both hit the same Tiny Engineer REST API (`/anim`). The ESP32-C3 runs that API, | Bus | Hardware | Role | | --- | --- | --- | | I2C | PCA9685 → 5× servos | Head / neck, hands / body | -| I2C | SSD1306 OLED | Status, face, info | +| I2C | Waveshare 0.91inch OLED (SSD1306) | Status, face, info | | I2S | MAX98357A → speaker | Audio | Cursor is one, sample, client, not the architecture. Details: [Cursor hooks](docs/hooks.md) · [any integration](docs/integration.md) · [HTTP API](docs/api.md) @@ -43,7 +43,7 @@ End-to-end path (details live in the linked docs): New to hardware? Start with **[From Code to Circuits](docs/hardware-for-software-engineers/README.md)** — hardware and electronics for software engineers; Tiny Engineer is the worked example. 1. **Get the electronics** — [hardware inventory](docs/hardware/components.md) -2. **3D print the parts** — [printables](3d_models/README.md) +2. **3D print the parts** — [printables](3d_models/README.md). Different servo size: [parametric CAD](docs/3d/parametric-design.md) 3. **Assemble the mechanics** — fit the five servos — [3D models](3d_models/README.md), [servo axes](docs/robot-movement.md) 4. **Wire the electronics** — [wiring](docs/hardware/wiring.md), [hardware overview](docs/hardware/README.md) 5. **Flash the ESP32 firmware** — [getting started → Flash](docs/getting-started.md#4-flash) @@ -61,13 +61,13 @@ Major pieces (exact models and counts in the BOM): | --- | --- | --- | | Controller | Waveshare ESP32-C3-Zero | 1 | | Servo PWM | Adafruit PCA9685 | 1 | -| Actuators | PowerHD HD-1370A micro servos | 5 | -| Display | 0.91" 128×32 SSD1306 OLED (I2C) | 1 | +| Actuators | Analog micro servos — **Tower Pro SG90 recommended**; Feetech FS0307 for a compact build; PowerHD HD-1370A still supported | 5 | +| Display | [Waveshare 0.91inch OLED Module](https://www.waveshare.com/0.91inch-oled-module.htm) (SSD1306, 128×32, I2C) | 1 | | Audio | MAX98357A + 8 Ω / 1 W speaker | 1 + 1 | | USB / power | Adafruit 5993 USB-C breakout; **5 V / ≥2 A** supply | 1 | | Structure | 3D-printed parts | — | -Complete inventory and limits: [docs/hardware/components.md](docs/hardware/components.md). +Complete inventory and limits: [docs/hardware/components.md](docs/hardware/components.md). Which servo to buy and which parts to print: [docs/3d/parametric-design.md](docs/3d/parametric-design.md). ## Quick start @@ -81,7 +81,7 @@ pio device monitor # serial (115200) If servos move but audio is silent, run `pio run -t uploadfs` once. -**Wi-Fi (first boot):** join `TinyEngineer-XXXX`, open `http://192.168.4.1/config`. Wizard: servo calibration, then a **2.4 GHz** network. OLED shows the join-AP steps. +**Wi-Fi (first boot):** join `TinyEngineer-XXXX`, open `http://192.168.4.1/config`. Wizard: servos, screen orientation, RGB LED, speaker, then a **2.4 GHz** network. OLED shows the join-AP steps. **Web UI:** `http://tiny-engineer.local/` (or the IP on the OLED) — settings, hardware tests, animations. @@ -113,6 +113,7 @@ Cursor projects can map agent events to poses via hooks — [docs/hooks.md](docs | Parts / BOM | [docs/hardware/components.md](docs/hardware/components.md) | | Wiring / power | [docs/hardware/README.md](docs/hardware/README.md) | | Printable parts | [3d_models/README.md](3d_models/README.md) | +| Resize CAD for another servo | [docs/3d/parametric-design.md](docs/3d/parametric-design.md) | | Servo axes / safe ranges | [docs/robot-movement.md](docs/robot-movement.md) | | HTTP API | [docs/api.md](docs/api.md) | | Settings | [docs/settings.md](docs/settings.md) | diff --git a/docs/3d/parametric-design.md b/docs/3d/parametric-design.md new file mode 100644 index 0000000..530f0b7 --- /dev/null +++ b/docs/3d/parametric-design.md @@ -0,0 +1,133 @@ +# Parametric design (different servo sizes) + +[`3d_models/cad/TinyEngineer.f3d`](../../3d_models/cad/TinyEngineer.f3d) is the parametric Fusion source for the robot. Printed pockets, mounting tabs, and shaft clearance are driven by Fusion **user parameters**. Change those values and the assembly rebuilds for a different micro servo. + +Presets live in [`servos.json`](../../3d_models/fusion/TinyEngineerTools/servos.json). **TinyEngineer Tools** writes them into the `.f3d`, then exports print meshes under `parts/{servo_id}/3mf/` (and matching `stl/`). + +```mermaid +flowchart LR + json["servos.json presets"] + cfg["Servo Configurator"] + f3d["TinyEngineer.f3d user params"] + parts["parts/servo_id meshes"] + json --> cfg --> f3d --> parts +``` + +## Which servo + +Servo size sets the size of the **whole robot**. Desk, chair, body, and pockets all scale with the preset. Pick the servo first, then print the matching `parts/{servo_id}/3mf/` folder. + +Built-in presets in [`servos.json`](../../3d_models/fusion/TinyEngineerTools/servos.json): + +| Model | `servo_id` | When to use | +| --- | --- | --- | +| **Tower Pro SG90** (recommended) | `sg90` | Easiest to buy. Bigger desk — electronics are easier to fit and assemble. | +| **Feetech FS0307** | `fs0307` | More compact. Looks better if you want a smaller robot. | +| **PowerHD HD-1370A** | `hd1370a` | Still supported for backward compatibility. Not the pick for a new build. | + +A non-standard servo is fine: measure it, add a preset, run the configurator ([Add a new servo](#add-a-new-servo)). Print parts that match that `servo_id`. + +Print matching `parts/{servo_id}/3mf/`: [`sg90`](../../3d_models/parts/sg90/3mf/), [`fs0307`](../../3d_models/parts/fs0307/3mf/), [`hd1370a`](../../3d_models/parts/hd1370a/3mf/). + +## Servo parameters + +Each preset in `servos.json` is a map of Fusion user-parameter names to expressions such as `"20 mm"`. The configurator ([`servo.py`](../../3d_models/fusion/TinyEngineerTools/servo.py)) writes those into the open design. It refuses names that are not already user parameters in the `.f3d` — do not invent keys. + +![Servo body dimensions used as Fusion user parameters](servo_params.png) + +### Body and tabs + +| Parameter | Meaning | +| --- | --- | +| `servo_body_width` | Body length between the two mounting tabs (tabs not included). | +| `servo_body_height` | Body height, from the mounting-tab plane to the opposite face. | +| `servo_body_depth` | Body thickness (front to back). | +| `servo_mount_width` | How far each mounting tab sticks out from the body. | +| `servo_mount_thickness` | Tab thickness. | + +### Gearbox and shaft stack + +| Parameter | Meaning | +| --- | --- | +| `servo_gearbox_height` | Gearbox step sitting on the body. | +| `servo_shaft_housing_height` | Boss around the output shaft, above the gearbox. | +| `servo_gearbox_padding` | How far the gearbox / shaft stack is inset from the body edge (the asymmetric “fat” side of typical micro servos). | + +### Shaft + +| Parameter | Meaning | +| --- | --- | +| `servo_shaft_position` | Distance from the body end to the shaft axis. | +| `servo_shaft_length` | How far the spline shaft sticks out. | +| `servo_shaft_radius` | Shaft radius (not diameter). | + +### Mount holes + +`servo_mount_hole_radius` is in `servos.json` but not drawn on the diagram. + +| Parameter | Meaning | +| --- | --- | +| `servo_mount_hole_depth_offset` | Hole shift along body depth from the tab centerline. | +| `servo_mount_hole_width_offset` | Hole shift along the tab (toward or away from the body). | +| `servo_mount_hole_radius` | Screw-hole radius in the tabs. | + +### Not a dimension + +| Parameter | Meaning | +| --- | --- | +| `servo_id` | Short lowercase folder id (`hd1370a`, `fs0307`, `sg90`). The configurator writes it; the exporter uses it. | + +## TinyEngineer Tools add-in + +**TinyEngineer Tools** is a Fusion add-in for [`cad/TinyEngineer.f3d`](../../3d_models/cad/TinyEngineer.f3d). It writes servo dimensions from [`TinyEngineerTools/servos.json`](../../3d_models/fusion/TinyEngineerTools/servos.json) into Fusion user parameters, and exports each `PRINT_LAYOUT` child as a `.3mf` and binary `.stl` mesh (PRINT_LAYOUT with one child visible, so captured print pose stays). + +Fusion must know about the add-in folder. Folder name, `TinyEngineerTools.py`, and `TinyEngineerTools.manifest` must stay the same. + +Official Autodesk steps: [How to install an add-in or script](https://www.autodesk.com/support/technical/article/caas/sfdcarticles/sfdcarticles/How-to-install-an-ADD-IN-and-Script-in-Fusion-360.html). + +### Point Fusion at this repo + +Fusion remembers the path. Code stays in git. Reload after edits (see below). + +1. Open Fusion. +2. Open **Utilities → Add-Ins → Scripts and Add-Ins**. Shortcut: **Shift+S**. +3. Open the **Add-Ins** tab. +4. Click the green **+** next to **Script of add-in from device**. +5. Browse to `3d_models/fusion/TinyEngineerTools/` in this repo and select the folder. +6. Select **TinyEngineerTools** in the list and click **Run**. +7. Optional: enable **Run on Startup** so Fusion starts it every launch. + +### Use the commands + +Open [`cad/TinyEngineer.f3d`](../../3d_models/cad/TinyEngineer.f3d) and stay in the **Design** workspace. Both commands live under **Utilities → Add-ins**. + +#### TinyEngineer Servo Configurator + +Select a servo model and apply dimensions, including `servo_id` (short lowercase id such as `sg90`). + +The command reads [`servos.json`](../../3d_models/fusion/TinyEngineerTools/servos.json), previews the values, and writes them into the design’s user parameters. Apply a servo before exporting. + +#### Tiny Engineer Parts Exporter + +Choose an export folder. + +The add-in finds the `PRINT_LAYOUT` component and exports each of its direct child components separately. For each child, it temporarily hides the others, keeps that child visible in its saved print orientation, and exports the full PRINT_LAYOUT as: + +* `{servo_id}/3mf/{name}.3mf` +* `{servo_id}/stl/{name}.stl` in binary STL format +* `{servo_id}/README.md` with that servo’s parameters from `servos.json` + +`servo_id` comes from the Fusion parameter set by Servo Configurator. Apply a servo before exporting. + +In that way, each component is exported as a separate file, all keeping their captured print orientation. + +After all components are exported, the original visibility settings are restored. A progress dialog stays up during the run so Fusion can paint; Cancel stops after the current part. + +## Add a new servo + +1. Measure a real unit (prefer calipers over datasheet marketing sizes). +2. Copy an existing object in [`servos.json`](../../3d_models/fusion/TinyEngineerTools/servos.json); keep the same keys; set a unique `servo_id`. +3. Reload the add-in, run Servo Configurator, confirm Fusion parameters update. +4. Print `ServoSizingTester` first, then export. + +The configurator refuses unknown Fusion parameter names. Do not invent keys that are not already user parameters in the `.f3d`. diff --git a/docs/3d/servo_params.png b/docs/3d/servo_params.png new file mode 100644 index 0000000..148a079 Binary files /dev/null and b/docs/3d/servo_params.png differ diff --git a/docs/README.md b/docs/README.md index 1b2aecd..37ce376 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,6 +10,7 @@ Pick a path. Depth lives in the linked pages. | **Flash / configure** only | [getting-started.md](getting-started.md)#4-flash | | **Wire / power detail** | [getting-started.md](getting-started.md)#3-wire-and-power → [hardware/README.md](hardware/README.md) | | **Print / parts inventory** | [getting-started.md](getting-started.md)#2-print-and-mechanical → [../3d_models/README.md](../3d_models/README.md) | +| **Adapt CAD to a different servo** | [3d/parametric-design.md](3d/parametric-design.md) | | **HTTP API / settings** | [api.md](api.md); add a setting: [settings.md](settings.md) | | **Any IDE / scripts** | [integration.md](integration.md) | | **Firmware / package tests** | [testing.md](testing.md) | diff --git a/docs/api.md b/docs/api.md index 69aeb8f..4b634af 100644 --- a/docs/api.md +++ b/docs/api.md @@ -6,9 +6,9 @@ Listens on **port 80** after STA Wi-Fi connects, or during setup AP mode at `htt Optional auth: when an `access_token` is configured in settings, all JSON API routes require `Authorization: Bearer `. Empty token (default) means no auth. `GET /auth` is always public and reports whether auth is required. Missing/wrong token → **401** `{"ok":false,"error":"unauthorized"}`. HTML panel routes stay public (the UI prompts for the token). `Content-Type: application/json`. CORS: `Access-Control-Allow-Origin: *`, `Access-Control-Allow-Headers: Authorization`. -When WiFi credentials are not saved yet, control APIs (`/anim`, `/test/*`) return **503** `{"ok":false,"error":"wifi not configured"}`. Setup routes (`/`, `/config`, `/auth`, `/health`, `/settings`, `/setup/servo`, `/setup/led`, `/setup/audio`) stay available on the setup AP. +When WiFi credentials are not saved yet, control APIs (`/anim`, `/test/*`) return **503** `{"ok":false,"error":"wifi not configured"}`. Setup routes (`/`, `/config`, `/auth`, `/health`, `/settings`, `/setup/servo`, `/setup/led`, `/setup/audio`, `/setup/oled`) stay available on the setup AP. -If boot WiFi credentials are missing, the device opens setup AP mode (`TinyEngineer-XXXX`) and serves a four-step setup wizard at `/config` (servo calibration, RGB LED, speaker test, then Wi-Fi). If saved credentials fail, it reopens setup AP mode. Hardware tests: [`docs/hardware/testing.md`](hardware/testing.md). How to wire this API into AI tools: [`integration.md`](integration.md). +If boot WiFi credentials are missing, the device opens setup AP mode (`TinyEngineer-XXXX`) and serves a five-step setup wizard at `/config` (servo calibration, OLED orientation, RGB LED, speaker test, then Wi-Fi). If saved credentials fail, it reopens setup AP mode. Hardware tests: [`docs/hardware/testing.md`](hardware/testing.md). How to wire this API into AI tools: [`integration.md`](integration.md). Firmware answers mDNS A and AAAA (IPv6 link-local) so macOS does not wait 2–3s on a missing AAAA. If a client still pauses on the hostname, force IPv4 (`curl -4`). The IP on the OLED skips DNS entirely. @@ -16,7 +16,7 @@ Firmware answers mDNS A and AAAA (IPv6 link-local) so macOS does not wait 2–3s ### `GET /` -HTML endpoint index. Lists all routes plus supported parameters for `/anim`, `/settings`, `/test/servo`, `/setup/servo`, `/setup/led`, and `/setup/audio`. Safe, no hardware side effects. Always public (no Bearer required). +HTML endpoint index. Lists all routes plus supported parameters for `/anim`, `/settings`, `/test/servo`, `/setup/servo`, `/setup/led`, `/setup/audio`, and `/setup/oled`. Safe, no hardware side effects. Always public (no Bearer required). Open in a browser: @@ -116,7 +116,8 @@ curl http://tiny-engineer.local/settings "wifi_password_set": true, "servo_mins": [60, 40, 45, 35, 40], "servo_maxs": [130, 130, 135, 125, 130], - "rgb_order": "GRB" + "rgb_order": "GRB", + "oled_rotate_180": false } ``` @@ -136,10 +137,11 @@ curl http://tiny-engineer.local/settings | `servo_mins` | Per-joint safe minimum degrees `[head, neck, left, right, body]` (stock defaults **60, 40, 45, 35, 40**) | | `servo_maxs` | Per-joint safe maximum degrees (stock defaults **130, 130, 135, 125, 130**). Each max must be greater than the matching min | | `rgb_order` | Onboard WS2812 wire-byte order (`RGB`, `RBG`, `GRB`, `GBR`, `BRG`, `BGR`). Default **`GRB`**. Survives factory reset | +| `oled_rotate_180` | OLED 180° rotation (`true` → `setRotation(2)`). Default **`false`**. Setup AP only. Survives factory reset | ### `POST /settings` -Update one or more settings. Query params. Values are written to NVS. `sleep_timeout`, `volume`, `welcome`, `serial_log`, `continuous_timeout`, and `access_token` apply immediately; a changed `hostname` or `loading` takes effect on the **next reboot**. WiFi credentials (`wifi_ssid` + `wifi_password`) are accepted **only in setup AP mode**; they are **tested before save**; on success the device connects to the home network and returns `wifi_connect_success`, `wifi_ip`, and `wifi_hostname`. `hostname` may be sent with those WiFi params (same validation as Config); it is saved before the STA connect so mDNS uses the chosen name immediately. Outside setup AP → **400** `wifi setup only in AP mode`. Servo ranges (`servo_mins` + `servo_maxs`) are also **setup AP only**; they apply immediately to motion clamps. Outside setup AP → **400** `servo setup only in AP mode`. RGB LED mapping (`rgb_order`) is **setup AP only** and applies immediately to the onboard LED. Outside setup AP → **400** `rgb setup only in AP mode`. Requires Bearer when auth is enabled. +Update one or more settings. Query params. Values are written to NVS. `sleep_timeout`, `volume`, `welcome`, `serial_log`, `continuous_timeout`, and `access_token` apply immediately; a changed `hostname` or `loading` takes effect on the **next reboot**. WiFi credentials (`wifi_ssid` + `wifi_password`) are accepted **only in setup AP mode**; they are **tested before save**; on success the device connects to the home network and returns `wifi_connect_success`, `wifi_ip`, and `wifi_hostname`. `hostname` may be sent with those WiFi params (same validation as Config); it is saved before the STA connect so mDNS uses the chosen name immediately. Outside setup AP → **400** `wifi setup only in AP mode`. Servo ranges (`servo_mins` + `servo_maxs`) are also **setup AP only**; they apply immediately to motion clamps. Outside setup AP → **400** `servo setup only in AP mode`. RGB LED mapping (`rgb_order`) is **setup AP only** and applies immediately to the onboard LED. Outside setup AP → **400** `rgb setup only in AP mode`. OLED rotation (`oled_rotate_180`) is **setup AP only** and applies immediately to the display. Outside setup AP → **400** `oled setup only in AP mode`. Requires Bearer when auth is enabled. ```bash curl -X POST "http://tiny-engineer.local/settings?sleep_timeout=2" @@ -154,6 +156,7 @@ curl -X POST "http://tiny-engineer.local/settings?access_token=" curl -X POST "http://192.168.4.1/settings?wifi_ssid=MyNetwork&wifi_password=secret&hostname=desk-bot" curl -X POST "http://192.168.4.1/settings?servo_mins=60,40,45,35,40&servo_maxs=130,130,135,125,130" curl -X POST "http://192.168.4.1/settings?rgb_order=GRB" +curl -X POST "http://192.168.4.1/settings?oled_rotate_180=1" curl -X POST "http://tiny-engineer.local/settings?sleep_timeout=10&hostname=tiny-engineer&volume=70&welcome=1&serial_log=0&continuous_timeout=5&loading=progress" ``` @@ -174,6 +177,7 @@ curl -X POST "http://tiny-engineer.local/settings?sleep_timeout=10&hostname=tiny "servo_mins": [60, 40, 45, 35, 40], "servo_maxs": [130, 130, 135, 125, 130], "rgb_order": "GRB", + "oled_rotate_180": false, "wifi_connect_success": true, "wifi_ip": "192.168.1.10", "wifi_hostname": "desk-bot.local", @@ -196,12 +200,13 @@ curl -X POST "http://tiny-engineer.local/settings?sleep_timeout=10&hostname=tiny | `servo_mins` | string | Five comma-separated integers `0`–`180` (`head,neck,left,right,body`); setup AP only; must be sent with `servo_maxs` | | `servo_maxs` | string | Five comma-separated integers `0`–`180`; setup AP only; each value must be greater than the matching min | | `rgb_order` | string | `RGB`, `RBG`, `GRB`, `GBR`, `BRG`, or `BGR`; setup AP only; default `GRB` | +| `oled_rotate_180` | integer | `0` or `1`; setup AP only; default `0` | -At least one param required. Invalid or missing-all → **400**. WiFi params outside setup AP → **400** `wifi setup only in AP mode`. Servo range params outside setup AP → **400** `servo setup only in AP mode`. RGB order outside setup AP → **400** `rgb setup only in AP mode`. WiFi test failure → **400** with error such as `SSID not found`, `Auth failed`, or `Timeout`. `reboot_required` is present and `true` only when the saved hostname differs from the one used at this boot. After successful WiFi save, `wifi_connect_success`, `wifi_ip`, and `wifi_hostname` are included. +At least one param required. Invalid or missing-all → **400**. WiFi params outside setup AP → **400** `wifi setup only in AP mode`. Servo range params outside setup AP → **400** `servo setup only in AP mode`. RGB order outside setup AP → **400** `rgb setup only in AP mode`. OLED rotation outside setup AP → **400** `oled setup only in AP mode`. WiFi test failure → **400** with error such as `SSID not found`, `Auth failed`, or `Timeout`. `reboot_required` is present and `true` only when the saved hostname differs from the one used at this boot. After successful WiFi save, `wifi_connect_success`, `wifi_ip`, and `wifi_hostname` are included. ### `POST /settings/reset` -Factory reset: clears the NVS `te` namespace and restores settings to compile-time defaults, including WiFi credentials. **Servo min/max and RGB LED mapping (`rgb_order`) are kept.** After reset, power-cycle (or press reset) so the device reopens setup AP mode and WiFi can be configured again; the servo and LED steps are pre-filled with the saved values and can be retuned. Until reboot, credentials are cleared in NVS but the radio may still be on the previous STA network. No query params. Requires Bearer when auth is enabled. Same JSON shape as `GET /settings`; includes `reboot_required` when the boot hostname, loading screen, or saved WiFi differed from defaults before reset. +Factory reset: clears the NVS `te` namespace and restores settings to compile-time defaults, including WiFi credentials. **Servo min/max, RGB LED mapping (`rgb_order`), and OLED rotation (`oled_rotate_180`) are kept.** After reset, power-cycle (or press reset) so the device reopens setup AP mode and WiFi can be configured again; the servo, screen, and LED steps are pre-filled with the saved values and can be retuned. Until reboot, credentials are cleared in NVS but the radio may still be on the previous STA network. No query params. Requires Bearer when auth is enabled. Same JSON shape as `GET /settings`; includes `reboot_required` when the boot hostname, loading screen, or saved WiFi differed from defaults before reset. The Config web UI clears the browser-stored Bearer token and shows a one-shot gate: power-cycle the device, then join the robot WiFi shown on the OLED and open the setup wizard. It does not poll for reconnect. @@ -240,11 +245,7 @@ curl -X POST http://tiny-engineer.local/test/audio ### `POST /test/audio/bell` -Plays `assets/bell.wav` (44100 Hz mono PCM) from LittleFS (`playBell()`). First flash or after changing `data/bell.wav`, upload the filesystem: - -```bash -pio run -t uploadfs -``` +Plays `/bell.wav` from LittleFS (`playBell()`). Git source is [`assets/bell.wav`](../assets/bell.wav) (44100 Hz mono PCM); `pio run` copies it into `data/` for the filesystem image. After changing the asset, upload with `pio run -t upload` or `pio run -t uploadfs`. Do not edit `data/` by hand — the next build overwrites it. ```bash curl -X POST http://tiny-engineer.local/test/audio/bell @@ -270,7 +271,7 @@ curl -X POST http://tiny-engineer.local/test/screen ### `POST /test/movement` -All five servos: 90° → 105° → 75° → 90° (`runServoTest()`). Needs a strong 5 V supply — see [power.md](hardware/power.md). +All five servos: per-joint mid → +0.5 → −0.5 → mid of the saved min/max (`runServoTest()`). Needs a strong 5 V supply — see [power.md](hardware/power.md). ```bash curl -X POST http://tiny-engineer.local/test/movement @@ -377,9 +378,37 @@ curl -X POST "http://192.168.4.1/setup/audio" After STA is up, use `POST /test/audio/bell` (or `/test/audio` for tones). +### `POST /setup/oled` + +Setup AP only. Preview OLED 180° rotation without saving. Does **not** require Wi-Fi credentials. Outside provisioning → **400** `setup oled only in AP mode`. + +`rotate_180=1` applies `setRotation(2)` and draws `THIS WAY UP`. `rotate_180=0` applies `setRotation(0)` and draws the same test. Omit `rotate_180` to restore saved rotation and the provisioning join-AP text. + +| Param | Type | Range | +| --- | --- | --- | +| `rotate_180` | integer | `0` or `1` (omit to restore provisioning text) | + +```bash +curl -X POST "http://192.168.4.1/setup/oled?rotate_180=1" +curl -X POST "http://192.168.4.1/setup/oled" +``` + +```json +{ "ok": true, "setup": "oled", "rotate_180": true } +``` + +Wrong params return **400**: + +| `error` | When | +| --- | --- | +| `setup oled only in AP mode` | Not in setup AP | +| `invalid rotate_180` | `rotate_180` present but not `0` or `1` | + +Save with `POST /settings?oled_rotate_180=1` (setup AP only). The saved value applies immediately and on the next boot. + ### `POST /test/servo` -Smoothly move one servo to an angle at **~40°/s** (`SERVO_SPEED_DEG_S`) from its last commanded position. Query params required. Handler blocks until the move finishes. +Smoothly move one servo to an angle at **~140°/s** (`SERVO_MAX_SPEED_DEG_S`) from its last commanded position. Query params required. Handler blocks until the move finishes. | Param | Type | Range | | --- | --- | --- | @@ -475,10 +504,10 @@ Wrong params return **400**: | Status | Body | When | | --- | --- | --- | -| `400` | `{"ok":false,"error":"..."}` | Bad `/test/servo`, `/setup/servo`, `/setup/led`, `/setup/audio`, or `/anim` params (see tables above) | +| `400` | `{"ok":false,"error":"..."}` | Bad `/test/servo`, `/setup/servo`, `/setup/led`, `/setup/audio`, `/setup/oled`, or `/anim` params (see tables above) | | `401` | `{"ok":false,"error":"unauthorized"}` | Access token configured and `Authorization: Bearer` missing or wrong | | `404` | `{"ok":false,"error":"not found"}` | Unknown path | -| `405` | `{"ok":false,"error":"method not allowed"}` | Wrong method on a `/test/*`, `/setup/servo`, `/setup/led`, `/setup/audio`, `/settings`, `/settings/reset`, `/anim`, or `/auth` path | +| `405` | `{"ok":false,"error":"method not allowed"}` | Wrong method on a `/test/*`, `/setup/servo`, `/setup/led`, `/setup/audio`, `/setup/oled`, `/settings`, `/settings/reset`, `/anim`, or `/auth` path | Test routes are **POST**. GET/prefetch would move hardware. `/anim` allows **GET** (read) and **POST** (set). `/auth` is **GET** only and always public. @@ -511,6 +540,6 @@ Transitions take **1 s** with smooth fade in/out (pulse modes start immediately, Switching between animations that share the same color (e.g. `typing` → `reading`) does not restart a fade. -Boot uses dim green `(0, 32, 0)` as a status indicator during init. After `ROBOT READY`, the LED fades to white if `welcome` runs (Wi-Fi OK) or off if idle. Fatal PCA9685 / I2S errors set solid dim red and hang — not animation-driven. +Boot uses dim green `(0, 32, 0)` as a status indicator during init. After `ROBOT READY`, the LED fades to white if `welcome` runs (Wi-Fi OK) or off if idle. Fatal PCA9685 / I2S errors hang and blink the LED red (1 = PCA9685 missing, 2 = I2S failed) — not animation-driven. See [boot-failure blink codes](hardware/testing.md#boot-failure-blink-codes). `POST /test/led` runs a hardware colour cycle and then restores the current animation LED state. diff --git a/docs/getting-started.md b/docs/getting-started.md index b0dbda0..b1d1e8f 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -17,15 +17,15 @@ In PlatformIO, `board = esp32-c3-devkitm-1` is a **build target name**, not a di Gather electronics from [hardware/components.md](hardware/components.md). Minimum set: - Waveshare ESP32-C3-Zero -- Adafruit PCA9685 + 5× PowerHD HD-1370A (or equivalent micro servos) +- Adafruit PCA9685 + 5× analog micro servos (**Tower Pro SG90** recommended; [other presets](3d/parametric-design.md)) - MAX98357A + 8 Ω / 1 W speaker -- 0.91" 128×32 SSD1306 OLED (I2C) +- [Waveshare 0.91inch OLED Module](https://www.waveshare.com/0.91inch-oled-module.htm) (SSD1306, 128×32, I2C) - Adafruit 5993 USB-C breakout (power + data) - **5 V / ≥2 A** USB supply (servos need headroom — [hardware/power.md](hardware/power.md)) ### 2. Print and mechanical -Printables and CAD: [3d_models/README.md](../3d_models/README.md) (`parts/*.3mf` ready to print PLA/PETG, no supports; source `cad/TinyEngineer.f3d` for edits / different hardware). +Printables and CAD: [3d_models/README.md](../3d_models/README.md) (`parts/{servo_id}/3mf/*.3mf` ready to print PLA/PETG, no supports; source `cad/TinyEngineer.f3d` for edits / different hardware). Parametric servo sizes, Fusion add-in, and export: [3d/parametric-design.md](3d/parametric-design.md). Join printed parts with **2 mm diameter screws** (no glue; easy to dismount later). Exact screw lengths/counts and a full assembly SOP are **not documented yet** — use the parts table there as the inventory. After print: @@ -36,7 +36,7 @@ Join printed parts with **2 mm diameter screws** (no glue; easy to dismount late Canonical connections: [hardware/wiring.md](hardware/wiring.md) and the diagram [wiring/Tiny Engineer.drawio.png](wiring/Tiny%20Engineer.drawio.png). Overview: [hardware/README.md](hardware/README.md). -Before first power-up, run the assembly checks in wiring.md (common GND, PCA9685 **VCC** = 3.3 V vs **V+** = 5 V not shorted, OLED clock on **SCK**, speaker on **SPK+/SPK−** only). Prefer bench bring-up with a strong 5 V supply before seating everything in the printed shell. +Before first power-up, run the assembly checks in wiring.md (common GND, PCA9685 **VCC** = 3.3 V vs **V+** = 5 V not shorted, OLED clock on **SCL**, speaker on **SPK+/SPK−** only). Prefer bench bring-up with a strong 5 V supply before seating everything in the printed shell. ### 4. Flash @@ -71,18 +71,18 @@ also lists Bluetooth serial ports, which are not the board. ### 5. Wi‑Fi setup -First boot (or after factory reset + power-cycle): join setup network `TinyEngineer-XXXX`, open `http://192.168.4.1/config`. A four-step wizard: (1) seat printed parts on the servo shafts at 90° then mark each joint’s safe min/max (factory reset keeps prior ranges; you can still change them here), (2) tap Red/Green/Blue on the onboard LED; change mapping only if colors look wrong (default **GRB**; factory reset keeps the mapping), (3) play the welcome clip through the speaker, (4) hostname and home Wi‑Fi (**2.4 GHz** only). OLED shows join-AP steps. Wi‑Fi is not editable from the normal Config page later — factory reset to change it. +First boot (or after factory reset + power-cycle): join setup network `TinyEngineer-XXXX`, open `http://192.168.4.1/config`. A five-step wizard: (1) seat printed parts on the servo shafts at 90° then mark each joint’s safe min/max (factory reset keeps prior ranges; you can still change them here), (2) look at the OLED and tap Rotate 180° if text is upside down (factory reset keeps the rotation), (3) tap Red/Green/Blue on the onboard LED; change mapping only if colors look wrong (default **GRB**; factory reset keeps the mapping), (4) play the welcome clip through the speaker, (5) hostname and home Wi‑Fi (**2.4 GHz** only). OLED shows join-AP steps. Wi‑Fi is not editable from the normal Config page later — factory reset to change it. ### 6. Prove it -Open `http://tiny-engineer.local/` (or the IP on the OLED) for the **web UI**: settings, hardware tests, animations. WiFi credentials, servo ranges, and RGB LED mapping stay setup-AP-only (`/config` on `http://192.168.4.1`). +Open `http://tiny-engineer.local/` (or the IP on the OLED) for the **web UI**: settings, hardware tests, animations. WiFi credentials, servo ranges, RGB LED mapping, and screen rotation stay setup-AP-only (`/config` on `http://192.168.4.1`). ```bash curl http://tiny-engineer.local/health curl -X POST "http://tiny-engineer.local/anim?name=ring" ``` -If `.local` is slow or fails, use the OLED IP or `curl -4`. Prefer web UI for hardware tests before seating servos hard against stops. When WiFi is not configured, control APIs (`/anim`, `/test/*`) return **503**; `POST /setup/servo`, `POST /setup/led`, and `POST /setup/audio` stay available on the setup AP. Optional access token → `Authorization: Bearer …` on JSON APIs (`GET /auth` stays public) — see [api.md](api.md). +If `.local` is slow or fails, use the OLED IP or `curl -4`. Prefer web UI for hardware tests before seating servos hard against stops. When WiFi is not configured, control APIs (`/anim`, `/test/*`) return **503**; `POST /setup/servo`, `POST /setup/led`, `POST /setup/audio`, and `POST /setup/oled` stay available on the setup AP. Optional access token → `Authorization: Bearer …` on JSON APIs (`GET /auth` stays public) — see [api.md](api.md). ### 7. Optional — Cursor hooks diff --git a/docs/hardware-for-software-engineers/01-electricity-and-units.md b/docs/hardware-for-software-engineers/01-electricity-and-units.md index 79a184f..a824719 100644 --- a/docs/hardware-for-software-engineers/01-electricity-and-units.md +++ b/docs/hardware-for-software-engineers/01-electricity-and-units.md @@ -182,7 +182,7 @@ The PCA9685 has **VCC** (3.3 V logic) and **V+** (5 V servo power). They must st ### Never power servos from 3.3 V -Five HD-1370A servos at stall draw on the order of **1.3–1.6 A**. The ESP32's LDO is rated for hundreds of mA of *logic*. Plug servos into 3.3 V and you get brownout, reset, or a dead regulator. Servos get 5 V via PCA9685 **V+**; the signal wire is 3.3 V PWM from the driver — that's fine. +Five PowerHD HD-1370A servos at stall draw on the order of **1.3–1.6 A**. **SG90** (recommended) is larger and typically stalls harder. The ESP32's LDO is rated for hundreds of mA of *logic*. Plug servos into 3.3 V and you get brownout, reset, or a dead regulator. Servos get 5 V via PCA9685 **V+**; the signal wire is 3.3 V PWM from the driver — that's fine. ### Common GND diff --git a/docs/hardware-for-software-engineers/02-microcontrollers-and-esp32.md b/docs/hardware-for-software-engineers/02-microcontrollers-and-esp32.md index be66feb..2971d93 100644 --- a/docs/hardware-for-software-engineers/02-microcontrollers-and-esp32.md +++ b/docs/hardware-for-software-engineers/02-microcontrollers-and-esp32.md @@ -114,7 +114,7 @@ The C3-Zero has a USB-C port on the board, but **when the robot is assembled**, ### The RGB LED is your boot log -Before Wi-Fi works, the LED tells you state: dim green while booting, solid red if init failed (e.g. PCA9685 missing). It's the hardware equivalent of a health-check endpoint you can see without serial. +Before Wi-Fi works, the LED tells you state: dim green while booting; a fatal init failure hangs and blinks red (1 = PCA9685 missing, 2 = I2S failed). It's the hardware equivalent of a health-check endpoint you can see without serial. Blink table: [hardware/testing.md](../hardware/testing.md#boot-failure-blink-codes). ### Ceramic antenna — don't bury it diff --git a/docs/hardware-for-software-engineers/03-wiring-craft-and-schematics.md b/docs/hardware-for-software-engineers/03-wiring-craft-and-schematics.md index aeb00b5..baaff42 100644 --- a/docs/hardware-for-software-engineers/03-wiring-craft-and-schematics.md +++ b/docs/hardware-for-software-engineers/03-wiring-craft-and-schematics.md @@ -93,7 +93,7 @@ Use [Tiny Engineer.drawio.png](../wiring/Tiny%20Engineer.drawio.png) as your map 4. Add audio, OLED, remaining servos 5. Install into printed parts -**OLED naming trap:** the pad labeled **SCK** on many OLED modules is I2C **SCL**, not SPI clock. Easy to misread if you're used to SPI docs. +**OLED naming trap:** this build uses the [Waveshare 0.91inch OLED Module](https://www.waveshare.com/0.91inch-oled-module.htm), which labels the clock pad **SCL**. Many clone 0.91" boards print **SCK** on that same I2C clock pin — not SPI. The wiring PNG may still say **SCK**. --- diff --git a/docs/hardware-for-software-engineers/04-buses-and-protocols.md b/docs/hardware-for-software-engineers/04-buses-and-protocols.md index 7b06e54..031a34a 100644 --- a/docs/hardware-for-software-engineers/04-buses-and-protocols.md +++ b/docs/hardware-for-software-engineers/04-buses-and-protocols.md @@ -49,7 +49,7 @@ PCA9685 is load-bearing. OLED is nice-to-have for bring-up. **Traps:** - SDA/SCL swapped → nothing works -- OLED pad labeled **SCK** is I2C **SCL**, not SPI clock +- OLED pad labeled **SCL** on the [Waveshare 0.91inch module](https://www.waveshare.com/0.91inch-oled-module.htm) is I2C clock. Generic clones often print **SCK** on that same pin — not SPI. - "Device not found" *after* servos start moving → often **power sag**, not a bad bus. See [Ch. 07](07-power-budgets-and-safety.md) --- @@ -99,7 +99,7 @@ flowchart LR FW["Firmware angle clamp"] I2C["I2C to PCA9685 0x40"] PWM["50 Hz PWM ch 0-4"] - SRV["HD-1370A servos"] + SRV["analog micro servos"] HTTP --> FW --> I2C --> PWM --> SRV ``` @@ -122,7 +122,7 @@ Not USB-PD conversion: it does **not** turn 9/12/20 V into 5 V. Feed a normal 5 | | Pattern | This robot | Typical mistake | | --- | --- | --- | --- | -| **I2C** | Request/response + addresses | PCA9685 + OLED (GP0, GP1) | Swapped SDA/SCL; OLED SCK ≠ SPI | +| **I2C** | Request/response + addresses | PCA9685 + OLED (GP0, GP1) | Swapped SDA/SCL; OLED clock is SCL (clones may say SCK) | | **I2S** | Continuous PCM stream | Amp (GP2–GP4) | SPK− tied to GND | | **PWM** | HIGH pulse length = angle | PCA9685 → five servos | Powering servos from 3.3 V | | **USB** | Power + serial | 5993 → 5 V + flash | Charge-only cable; expecting PD | diff --git a/docs/hardware-for-software-engineers/05-servos-and-mechanical-motion.md b/docs/hardware-for-software-engineers/05-servos-and-mechanical-motion.md index b07ab19..e51cde1 100644 --- a/docs/hardware-for-software-engineers/05-servos-and-mechanical-motion.md +++ b/docs/hardware-for-software-engineers/05-servos-and-mechanical-motion.md @@ -6,7 +6,7 @@ ## What a hobby servo is -A **position servo** (specifically the **PowerHD HD-1370A** micro servos in this build) contains: +A **position servo** (hobby analog micro servos in this build — [which servo](../3d/parametric-design.md)) contains: - Motor - Gear train @@ -15,7 +15,7 @@ A **position servo** (specifically the **PowerHD HD-1370A** micro servos in this You command an **angle** (via pulse width). The servo tries to reach it and **holds** there — even against light load. -**Not** continuous rotation — it stops at mechanical limits (~130° usable on these units, marketed sometimes as 180°). +**Not** continuous rotation — it stops at mechanical limits (~130° usable on some units such as PowerHD HD-1370A, marketed sometimes as 180°). **Three wires:** | Wire | Role | @@ -72,9 +72,9 @@ Two layers of limits: | **Mechanical** | Horn hits shell, linkage binds, servo stalls | Your assembly | | **Safe software** | Clamped angles per joint | `SERVO_SPECS` in firmware | -Always respect **safe ranges** for the assembled robot — [robot-movement.md](../robot-movement.md). Bench-only wider sweeps (75–105°) exist for testing; don't slam full electrical range into a built shell. +Always respect **safe ranges** for the assembled robot — [robot-movement.md](../robot-movement.md). `POST /test/movement` sweeps each joint inside its saved min/max; don't slam full electrical range into a built shell. -**Tune after assembly:** if a hand hits the desk at 135°, lower `max` in firmware or reorient the horn — don't "just command less" in one script and forget. +**Tune after assembly:** if a hand hits the desk at 135°, lower that joint’s max in the setup AP wizard (saved to NVS) or reorient the horn — don't "just command less" in one script and forget. Stock defaults live in `SERVO_SPECS`; assembled motion uses the saved min/max. --- @@ -98,7 +98,7 @@ flowchart LR FW["Firmware angle clamp"] I2C["I2C to PCA9685 0x40"] PWM["50 Hz PWM ch 0-4"] - SRV["HD-1370A servos"] + SRV["analog micro servos"] HTTP --> FW --> I2C --> PWM --> SRV ``` diff --git a/docs/hardware-for-software-engineers/06-3d-printing-and-mechanical-build.md b/docs/hardware-for-software-engineers/06-3d-printing-and-mechanical-build.md index b859d44..f2a8110 100644 --- a/docs/hardware-for-software-engineers/06-3d-printing-and-mechanical-build.md +++ b/docs/hardware-for-software-engineers/06-3d-printing-and-mechanical-build.md @@ -66,27 +66,28 @@ The models are **prepared with clearance** so printed parts and hardware (servos | File | What it is | | --- | --- | -| [`cad/TinyEngineer.f3d`](../../cad/TinyEngineer.f3d) | **Parametric source** — full assembly in Autodesk Fusion | -| [`3d_models/parts/*.3mf`](../../3d_models/parts/) | **Exported meshes** — what the printer eats | +| [`cad/TinyEngineer.f3d`](../../3d_models/cad/TinyEngineer.f3d) | **Parametric source** — full assembly in Autodesk Fusion | +| [`3d_models/parts/{servo_id}/3mf/*.3mf`](../../3d_models/parts/) | **Exported meshes** — what the printer eats | > **If you've written backend code…** `.f3d` is the repo. `.3mf` is the release binary. Don't edit `.3mf` as source — edit Fusion, re-export. ### When to open Fusion - Servo pocket too tight/loose after first print -- Different micro servo dimensions (not just HD-1370A) +- Different micro servo dimensions (use a [`servos.json`](../../3d_models/fusion/TinyEngineerTools/servos.json) preset, or add one) - Cable routing channel for your wiring harness - Antenna keep-out adjustment - Desk/chair scale for your desk -Parts are designed around **HD-1370A** dimensions and **2 mm** screws. Swapping servos usually means CAD edits, not just firmware. +Parts use **2 mm** screws. Servo pockets follow the selected Fusion preset, not one fixed body size. Swapping servos means picking a preset (or adding one) and reprinting — not just firmware. Servo user parameters, the Fusion add-in (configurator + exporter), and how to add a preset: [parametric design](../3d/parametric-design.md). ### Export workflow 1. Open `TinyEngineer.f3d` in Fusion (free personal license exists) -2. Edit the body you need -3. Export that body to `.3mf` in `3d_models/parts/` (or your fork) -4. Print and test fit +2. Apply a servo preset, then export with **Tiny Engineer Parts Exporter** into `parts/{servo_id}/3mf/` (see [parametric design](../3d/parametric-design.md)) +3. Print and test fit + +Manual single-body export is fine for a one-off tweak; keep the file under that servo’s `3mf/` folder, not a flat `3d_models/parts/` dump. Alternatives (FreeCAD, Onshape) can work but project source is Fusion. @@ -104,7 +105,7 @@ Full step-by-step SOP is still a doc gap — these principles keep you out of tr ### Range of motion before closing shell -Command each joint through safe range ([robot-movement.md](../robot-movement.md)). Listen for buzzing (stall). Adjust horns or firmware limits before trapping servos in plastic. +Command each joint through safe range ([robot-movement.md](../robot-movement.md)). Listen for buzzing (stall). Adjust horns or setup-AP min/max (NVS) before trapping servos in plastic. ### USB and service access @@ -134,4 +135,4 @@ Electronics can ship slow. Start printing **structural parts** (chest, head, cha **Next:** [Power budgets and safety](07-power-budgets-and-safety.md) -**Reference:** [3d_models/README.md](../../3d_models/README.md) · [cad/TinyEngineer.f3d](../../cad/TinyEngineer.f3d) · [robot-movement.md](../robot-movement.md) +**Reference:** [3d_models/README.md](../../3d_models/README.md) · [cad/TinyEngineer.f3d](../../3d_models/cad/TinyEngineer.f3d) · [parametric design](../3d/parametric-design.md) · [robot-movement.md](../robot-movement.md) diff --git a/docs/hardware-for-software-engineers/07-power-budgets-and-safety.md b/docs/hardware-for-software-engineers/07-power-budgets-and-safety.md index 27e6e79..5f2ebc1 100644 --- a/docs/hardware-for-software-engineers/07-power-budgets-and-safety.md +++ b/docs/hardware-for-software-engineers/07-power-budgets-and-safety.md @@ -13,7 +13,7 @@ Every load draws what it needs (not what the label says). **Current adds in para - ESP32 + Wi-Fi TX — tens to hundreds of mA peaks - OLED + PCA9685 logic — tens of mA - MAX98357A + speaker — peaks on loud WAV -- **Five servos** — **~1.3–1.6 A** if several stall together (HD-1370A datasheet) +- **Five servos** — **~1.3–1.6 A** if several stall together (PowerHD HD-1370A datasheet). **SG90** (recommended) is larger and typically stalls harder — still **5 V / ≥ 2 A**, with extra margin if several move during audio. A **1 A phone charger** is not enough. Recommendation: **5 V / ≥ 2 A**, with margin if servos move during audio. @@ -23,14 +23,14 @@ A **1 A phone charger** is not enough. Recommendation: **5 V / ≥ 2 A**, with m A **stall** is when the motor tries to move but can't — horn against stop, binding linkage, desk collision. -Per HD-1370A datasheet (approximate): +Per PowerHD HD-1370A datasheet (approximate): | Condition | Per servo | × 5 servos | | --- | --- | --- | | Stall @ ~4.8 V | ~260 mA | **~1.3 A** | | Stall @ ~6.0 V | ~320 mA | **~1.6 A** | -Robot rail is ~5 V — expect something in between if multiple servos stall at once. +Robot rail is ~5 V — expect something in between if multiple HD-1370A servos stall at once. **SG90** (recommended) typically stalls harder; still use **5 V / ≥ 2 A**, with extra margin if several move during audio. That's **before** Wi-Fi, display, and audio peaks. diff --git a/docs/hardware-for-software-engineers/08-tools-debugging-and-embedded-workflow.md b/docs/hardware-for-software-engineers/08-tools-debugging-and-embedded-workflow.md index 41eb899..1a677f3 100644 --- a/docs/hardware-for-software-engineers/08-tools-debugging-and-embedded-workflow.md +++ b/docs/hardware-for-software-engineers/08-tools-debugging-and-embedded-workflow.md @@ -117,7 +117,7 @@ When two things changed, revert one. Hardware debugging punishes batch changes l | Mistake | Consequence | | --- | --- | | Swapped SDA/SCL | No I2C devices | -| OLED SCK wired as SPI clock confusion | Same — it's SCL | +| OLED clock labelled SCK on a clone (Waveshare says SCL) | Same pin — I2C SCL, not SPI | | Servos on 3.3 V | Brownout / dead LDO | | VCC/V+ jumpered on PCA9685 | Logic or servo damage | | SPK− to GND | Bad audio, possible amp damage | diff --git a/docs/hardware-for-software-engineers/09-how-it-all-fits-together.md b/docs/hardware-for-software-engineers/09-how-it-all-fits-together.md index 3116865..8b34332 100644 --- a/docs/hardware-for-software-engineers/09-how-it-all-fits-together.md +++ b/docs/hardware-for-software-engineers/09-how-it-all-fits-together.md @@ -64,7 +64,7 @@ flowchart TB V33 --> PCA V33 --> OLED V5 --> AMP - PCA -->|V+ 5V PWM| SRV["5x HD-1370A servos"] + PCA -->|V+ 5V PWM| SRV["5 analog micro servos"] PCA --> SRV @@ -119,7 +119,7 @@ flowchart TB 1. Print structural parts ([3d_models](../../3d_models/README.md)) 2. Test-fit servos in pockets — don't force -3. Adjust CAD if needed ([Fusion source](../../cad/TinyEngineer.f3d)) +3. Adjust CAD if needed ([Fusion source](../../3d_models/cad/TinyEngineer.f3d)) ### Phase C — Mechanical integration @@ -144,7 +144,7 @@ Check these off — order matters: | --- | --- | --- | | 1 | **Continuity sanity** | No short 5V–GND; GND common (meter, power off) | | 2 | **Serial boot** | `pio device monitor` shows boot log, dim green LED | -| 3 | **PCA9685 detected** | Serial OK; missing = red LED hang | +| 3 | **PCA9685 detected** | Serial OK; missing = 1 red blink, hang ([blink codes](../hardware/testing.md#boot-failure-blink-codes)) | | 4 | **One servo** | `/test/servo?index=0` moves head channel | | 5 | **All five** | Each joint in safe range, no stall buzz | | 6 | **Audio** | Sound on animation; if silent → `uploadfs` | diff --git a/docs/hardware/README.md b/docs/hardware/README.md index 82ff3bc..da2a8f5 100644 --- a/docs/hardware/README.md +++ b/docs/hardware/README.md @@ -4,7 +4,7 @@ Canonical hardware reference for the Tiny Engineer robot. Firmware pin constants live in [`include/pins.h`](../../include/pins.h). Wiring topology lives in [`docs/wiring/Tiny Engineer.drawio.png`](../wiring/Tiny%20Engineer.drawio.png). -Conflict order: firmware GPIO numbers → wiring PNG connections → this `docs/hardware/` set. +Source of truth: firmware `include/pins.h`, then this `docs/hardware/` set and `docs/wiring/`. | Audience | Use this set for | | --- | --- | @@ -53,7 +53,7 @@ flowchart TB ESP -->|"3V3"| PCA ESP -->|"3V3"| OLED ESP -->|"GP0 SDA / GP1 SCL"| PCA - ESP -->|"GP0 SDA / GP1 SCK"| OLED + ESP -->|"GP0 SDA / GP1 SCL"| OLED ESP -->|"GP2 BCLK / GP3 LRC / GP4 DIN"| MAXVIN PCA -->|"PWM"| SERVOS ``` @@ -78,10 +78,10 @@ Speaker **SPK+/SPK-** are not on the PNG. See [wiring.md](wiring.md#not-on-the-d | --- | --- | --- | | Controller | Waveshare ESP32-C3-Zero | 1 | | Servo PWM | Adafruit PCA9685 16-channel driver | 1 | -| Actuators | PowerHD HD-1370A analog micro servo | 5 | +| Actuators | Analog micro servos — **Tower Pro SG90 recommended**; Feetech FS0307 compact; PowerHD HD-1370A still supported | 5 | | Audio amp | MAX98357A I2S class-D (mono) | 1 | | Speaker | 8 Ω / 1 W mono | 1 | -| Display | 0.91" 128×32 OLED, SSD1306, I2C | 1 | +| Display | [Waveshare 0.91inch OLED Module](https://www.waveshare.com/0.91inch-oled-module.htm) (SSD1306, 128×32, I2C) | 1 | | Robot USB | Adafruit 5993 USB-C breakout (power + data) | 1 | Full inventory: [components.md](components.md). @@ -90,7 +90,7 @@ Full inventory: [components.md](components.md). | Bus | ESP32 pins | Devices | | --- | --- | --- | -| I2C | GP0/SDA, GP1/SCL (OLED pad **SCK**) | PCA9685 `0x40`, SSD1306 `0x3C` | +| I2C | GP0/SDA, GP1/SCL (Waveshare OLED **SCL**) | PCA9685 `0x40`, SSD1306 `0x3C` | | I2S | GP2/BCLK, GP3/LRC, GP4/DIN | MAX98357A | | Servo PWM | *(none on ESP32)* | PCA9685 channels 0–4 @ 50 Hz | | USB | Adafruit 5993 (VBUS/GND + D+/D− → GP19/GP18) | Power, flash, serial CDC | diff --git a/docs/hardware/components.md b/docs/hardware/components.md index bb6f546..ed1d68f 100644 --- a/docs/hardware/components.md +++ b/docs/hardware/components.md @@ -1,6 +1,6 @@ # Hardware inventory -Selected hardware only. Do not substitute parts from this list without updating this file, [pinout.md](pinout.md), and [wiring.md](wiring.md). +Selected hardware only. Do not substitute electronics from this list without updating this file, [pinout.md](pinout.md), and [wiring.md](wiring.md). Servo *model* may be any preset in [`servos.json`](../../3d_models/fusion/TinyEngineerTools/servos.json) if the printed parts match — [parametric design](../3d/parametric-design.md). Constants cited below match [`include/pins.h`](../../include/pins.h). @@ -10,10 +10,12 @@ Constants cited below match [`include/pins.h`](../../include/pins.h). | --- | --- | | Waveshare ESP32-C3-Zero | https://docs.waveshare.com/ESP32-C3-Zero | | Adafruit PCA9685 16-channel PWM servo driver | https://www.adafruit.com/product/815 | -| PowerHD HD-1370A | https://www.chd.hk/Product_Detail.aspx?id=30 | +| Tower Pro SG90 (or equivalent) | Widely available hobby servo — no single canonical SKU | +| Feetech FS0307 | See vendor listing for FS0307 analog micro | +| PowerHD HD-1370A (backward compatibility) | https://www.chd.hk/Product_Detail.aspx?id=30 | | MAX98357A I2S class-D mono amplifier | https://www.aliexpress.us/item/3256805196806369.html | | 8 Ω / 1 W mono speaker | https://www.aliexpress.us/item/3256807341987395.html | -| 0.91" 128×32 OLED (SSD1306, I2C) | https://www.raystar-optronics.com/oled-graphic-display-module/oled-i2c-ssd1306.html | +| Waveshare 0.91inch OLED Module | https://www.waveshare.com/0.91inch-oled-module.htm | | Adafruit 5993 USB-C breakout | https://www.adafruit.com/product/5993 | ## Waveshare ESP32-C3-Zero @@ -29,7 +31,7 @@ Constants cited below match [`include/pins.h`](../../include/pins.h). | USB | Native USB on GPIO18/19 — reached via Adafruit 5993 D+/D− (onboard USB-C unused when assembled) | | Flash | 4 MB stacked flash on GPIO12–GPIO17 (those GPIOs are **not exposed**) | | Onboard RGB | WS2812 on **GPIO10** | -| Firmware notes | Boot: dim green status. Animations: white (typing/reading/thinking/welcome/ring/wakeup), pulsing red (attention/error/dead), solid red (abort), off (idle/sleep). 1 s fade between non-pulse states. Fatal init errors: solid dim red + hang. See [`docs/api.md`](../../api.md#rgb-led) | +| Firmware notes | Boot: dim green status. Animations: white (typing/reading/thinking/welcome/ring/wakeup), pulsing red (attention/error/dead), solid red (abort), off (idle/sleep). 1 s fade between non-pulse states. Fatal init errors: hang and blink red (1 = PCA9685, 2 = I2S). See [blink codes](testing.md#boot-failure-blink-codes) and [`docs/api.md`](../../api.md#rgb-led) | | BOOT | Button / strapping on **GPIO9** | | RESET | Separate RESET button (CHIP_EN), not a GPIO | | UART0 silkscreen | TX = GPIO21, RX = GPIO20 | @@ -55,29 +57,65 @@ Constants cited below match [`include/pins.h`](../../include/pins.h). | Limits | **V+** and **VCC** must stay separate. Do not jumper servo power onto logic VCC on this robot | | Firmware note | Detection at `0x40` is a **hard fail** in the bring-up test | -## PowerHD HD-1370A +## Analog micro servos (qty 5) + +Five analog micro servos drive the joints (Head / Neck / L hand / R hand / Body — [robot-movement.md](../robot-movement.md)). Buy **five of one model**. Print the matching `parts/{servo_id}/3mf/` folder. Which size to pick: [parametric design](../3d/parametric-design.md). + +Shared for every preset: | Field | Value | | --- | --- | -| Exact model | PowerHD HD-1370A analog micro servo | -| Quantity | 5 | -| Purpose | Robot joints (Head / Neck / L hand / R hand / Body — [robot-movement.md](../robot-movement.md)) | -| Operating voltage | 4.8–6.0 V (robot: **+5V** via PCA9685 **V+**) | +| Quantity | 5 (one model) | +| Operating voltage | Robot: **+5V** via PCA9685 **V+** | | Interface | Analog PWM, ~50 Hz. Signal is 3.3 V from PCA9685 — acceptable for this servo class | -| Pulse | Neutral ~1500 µs. Declared range ~**800–2200 µs** (`SERVO_MIN_US` / `SERVO_MAX_US`) | -| Current (datasheet stall) | ~260 mA @ 4.8 V, ~320 mA @ 6.0 V | -| No-load current | ~110 mA @ 4.8 V, ~120 mA @ 6.0 V | -| Idle current | ~4 mA @ 4.8 V, ~5 mA @ 6.0 V | -| Torque / speed | 0.4 kg·cm / 0.12 s/60° @ 4.8 V; 0.6 kg·cm / 0.10 s/60° @ 6.0 V | -| Size / mass | ~20.2 × 8.5 × 17.6 mm, ~3.7 g | +| Pulse | Neutral ~1500 µs. Firmware window **800–2200 µs** (`SERVO_MIN_US` / `SERVO_MAX_US`) | | Important pins | SIG, +5V, GND (JR-style 3-wire) | -| Limits | Manufacturer sheet also lists **~130°** travel over 800–2200 µs, while many retailers say 0–180°. Treat 0–180° as **nominal**, not a mechanical guarantee. | > [!WARNING] > Never power the servos from the ESP32 3.3 V regulator. Firmware uses **per-servo safe ranges** in [`include/servos.h`](../../include/servos.h) (`SERVO_SPECS`). Tune after assembly if needed. Do not command full 0–180° / 800–2200 µs on the assembled robot. See [servos.md](servos.md). +### Tower Pro SG90 (recommended) + +Easiest to buy. Larger body → larger printed robot; electronics are easier to fit in the desk. + +| Field | Value | +| --- | --- | +| Exact model | Tower Pro SG90 analog micro (or equivalent 9 g class) | +| `servo_id` | `sg90` | +| Size | Larger than FS0307 / HD-1370A — see [`servos.json`](../../3d_models/fusion/TinyEngineerTools/servos.json) | + +Print [`3d_models/parts/sg90/3mf/`](../../3d_models/parts/sg90/3mf/). + +### Feetech FS0307 + +More compact. Use this if you want a smaller robot that looks tighter on the desk. + +| Field | Value | +| --- | --- | +| Exact model | Feetech FS0307 analog micro servo | +| `servo_id` | `fs0307` | + +Print [`3d_models/parts/fs0307/3mf/`](../../3d_models/parts/fs0307/3mf/). + +### PowerHD HD-1370A (backward compatibility) + +Still supported. Not the pick for a new build. + +Print [`3d_models/parts/hd1370a/3mf/`](../../3d_models/parts/hd1370a/3mf/). + +| Field | Value | +| --- | --- | +| Exact model | PowerHD HD-1370A analog micro servo | +| `servo_id` | `hd1370a` | +| Current (datasheet stall) | ~260 mA @ 4.8 V, ~320 mA @ 6.0 V | +| No-load current | ~110 mA @ 4.8 V, ~120 mA @ 6.0 V | +| Idle current | ~4 mA @ 4.8 V, ~5 mA @ 6.0 V | +| Torque / speed | 0.4 kg·cm / 0.12 s/60° @ 4.8 V; 0.6 kg·cm / 0.10 s/60° @ 6.0 V | +| Size / mass | ~20.2 × 8.5 × 17.6 mm, ~3.7 g | +| Limits | Manufacturer sheet also lists **~130°** travel over 800–2200 µs, while many retailers say 0–180°. Treat 0–180° as **nominal**, not a mechanical guarantee. | + ## MAX98357A I2S class-D mono amplifier | Field | Value | @@ -109,20 +147,20 @@ Firmware uses **per-servo safe ranges** in [`include/servos.h`](../../include/se Speaker wiring is **not** drawn on `docs/wiring/Tiny Engineer.drawio`. Electrical rule is still: speaker ↔ SPK+/SPK- only. -## 0.91" 128×32 OLED (SSD1306, I2C) +## Waveshare 0.91inch OLED Module (SSD1306, I2C) | Field | Value | | --- | --- | -| Exact model | Generic 0.91" 128×32 SSD1306 I2C OLED | +| Exact model | [Waveshare 0.91inch OLED Module](https://www.waveshare.com/0.91inch-oled-module.htm) (SKU 14657). Driver SSD1306, 128×32, I2C | | Quantity | 1 | | Purpose | Status / test UI | -| Operating voltage | Module **VCC** from ESP32 **3V3** (confirmed in [`docs/wiring`](../wiring/Tiny%20Engineer.drawio.png)) | -| Interface | I2C on the shared bus | +| Operating voltage | Module accepts **3.3 V / 5 V**. Robot **VCC** from ESP32 **3V3** (confirmed in [`docs/wiring`](../wiring/Tiny%20Engineer.drawio.png)) | +| Interface | I2C on the shared bus. Pads **VCC**, **GND**, **SDA**, **SCL** | | Address | **`0x3C`** (`OLED_ADDRESS` in firmware). Alternate 0x3D exists on some modules if the ADDR jumper is changed — not used here | | Resolution | 128×32 (`OLED_WIDTH` / `OLED_HEIGHT`) | -| Important pins | As drawn: **VCC**, **GND**, **SDA**, **SCK** (that **SCK** pad is I2C SCL) | -| Firmware notes | `SSD1306_SWITCHCAPVCC`; display rotated **180°** (`setRotation(2)`). Missing OLED is **non-fatal** in the bring-up test | -| Limits | 3.3 V logic. Do not hang this module on the servo +5V rail unless a specific 5 V-tolerant module is verified (**current wiring is 3V3**) | +| Important pins | **VCC**, **GND**, **SDA**, **SCL**. Clock pad on this board is **SCL**. The wiring PNG may still label that net **SCK** (generic clone silkscreen) — same I2C clock, ESP32 **GP1** | +| Firmware notes | `SSD1306_SWITCHCAPVCC`; rotation from `oled_rotate_180` (`setRotation(0)` or `2`). Missing OLED is **non-fatal** in the bring-up test | +| Limits | Robot wiring is **3V3**. Do not hang this module on the servo +5V rail | ## Adafruit 5993 USB-C breakout @@ -137,10 +175,10 @@ Speaker wiring is **not** drawn on `docs/wiring/Tiny Engineer.drawio`. Electrica | Important pins | Drawing may show USB **5V** / **GND** only; also wire **D+/D−** to native USB | | Limits | **Not** a USB-PD voltage converter. It does not step 9 V/12 V/20 V down to 5 V. Prefer a **5 V / ≥2 A** host — see [power.md](power.md) | -Prefer a final 5 V supply of **at least ~2 A** with margin if several servos move while audio plays. +Prefer a final 5 V supply of **at least ~2 A** with margin if several servos move while audio plays. SG90 (recommended) typically stalls harder than HD-1370A — see [power.md](power.md). ## Mechanical / RF notes for firmware and CAD - Keep metal and dense plastic **off** the ESP32 ceramic antenna area. -- Servo horns and end stops: start from `SERVO_SPECS` in [`include/servos.h`](../../include/servos.h); tune after installation if needed. Bench-only test band remains 75–105°. +- Servo horns and end stops: start from `SERVO_SPECS` in [`include/servos.h`](../../include/servos.h); tune after installation if needed. Assembled motion stays inside the saved min/max. - GPIO18/19 are native USB via the 5993 data wires. Do not reassign them while using USB CDC for flash/serial. diff --git a/docs/hardware/interfaces.md b/docs/hardware/interfaces.md index 75bd792..3348aed 100644 --- a/docs/hardware/interfaces.md +++ b/docs/hardware/interfaces.md @@ -9,11 +9,11 @@ Shared bus. | Signal | ESP32 GPIO | Diagram pad | | --- | --- | --- | | SDA | GPIO0 | GP0 → PCA9685 SDA, OLED SDA | -| SCL | GPIO1 | GP1 → PCA9685 SCL, OLED **SCK** | +| SCL | GPIO1 | GP1 → PCA9685 SCL, Waveshare OLED **SCL** | | Device | Address | Firmware behaviour if missing | | --- | --- | --- | -| PCA9685 | **`0x40`** | Hard fail: red RGB, hang | +| PCA9685 | **`0x40`** | Hard fail: hang, 1 red blink (see [blink codes](testing.md#boot-failure-blink-codes)) | | SSD1306 OLED | **`0x3C`** | Soft fail: tests continue without display | Bus start: `Wire.begin(I2C_SDA, I2C_SCL)` with no explicit frequency → Arduino-ESP32 default (typically **100 kHz**). Not set in this repo. diff --git a/docs/hardware/pinout.md b/docs/hardware/pinout.md index 1609f23..eafa914 100644 --- a/docs/hardware/pinout.md +++ b/docs/hardware/pinout.md @@ -2,14 +2,12 @@ Source of truth: [`include/pins.h`](../../include/pins.h). This file must match that header. -Verified against firmware (2026-08-21). No discrepancy vs the tested assignment. - ## Assigned ESP32-C3-Zero GPIO | Function | ESP32-C3-Zero GPIO | Constant | Goes to | | --- | --- | --- | --- | | I2C SDA | **GPIO0** (diagram **GP0**) | `I2C_SDA` | PCA9685 SDA, OLED SDA | -| I2C SCL | **GPIO1** (diagram **GP1**) | `I2C_SCL` | PCA9685 SCL, OLED **SCK** | +| I2C SCL | **GPIO1** (diagram **GP1**) | `I2C_SCL` | PCA9685 SCL, Waveshare OLED **SCL** | | I2S BCLK | **GPIO2** (diagram **GP2**) | `I2S_BCLK` | MAX98357A BCLK | | I2S LRCLK / WS | **GPIO3** (diagram **GP3**) | `I2S_LRC` | MAX98357A **LRC** | | I2S DATA OUT | **GPIO4** (diagram **GP4**) | `I2S_DIN` | MAX98357A DIN | @@ -63,7 +61,6 @@ Power pads (not GPIO): **5V**, **GND**, **3V3**. | `SERVO_MIN_US` / `SERVO_MAX_US` | 800 / 2200 | Electrical PWM span | | `SERVO_BOOT_SPEED_DEG_S` | 35 | Boot centering / sleep-pose rate (deg/s) | | `PCA9685_OE_WIRED` | `false` | Set `true` after wiring GP5 → PCA9685 OE | -| `SERVO_LOW` / `SERVO_CENTER` / `SERVO_HIGH` | 75 / 90 / 105 | Hardware-test angles | ## Pin allocation rules diff --git a/docs/hardware/power.md b/docs/hardware/power.md index fb2a5be..a1e1bc6 100644 --- a/docs/hardware/power.md +++ b/docs/hardware/power.md @@ -19,14 +19,16 @@ Waveshare documents the C3-Zero LDO as ME6217C33M5G (hundreds of mA class). That ## Servo current (worst case) -Datasheet stall currents for HD-1370A: +Datasheet stall currents for PowerHD HD-1370A: | Voltage | Per servo stall | × 5 servos | | --- | --- | --- | | 4.8 V | ~260 mA | **≈ 1.3 A** | | 6.0 V | ~320 mA | **≈ 1.6 A** | -Robot rail is nominally **5 V**, so expect something between those two if several servos stall or start together. +Robot rail is nominally **5 V**, so expect something between those two if several HD-1370A servos stall or start together. + +**Tower Pro SG90** (recommended) is larger and typically stalls harder. Same **5 V / ≥ 2 A** floor; give more margin if several SG90s move while audio plays. Do not treat the HD-1370A milliamp table as an SG90 budget. This **excludes**: @@ -40,7 +42,7 @@ This **excludes**: Adafruit 5993 CC resistors ask the upstream port for **5 V / up to ~1.5 A**. Whether 1.5 A actually arrives depends on the charger/port. -Five servos at stall (~1.3 A @ 4.8 V) plus the rest of the robot leaves **little safety margin** on a 1.5 A USB source. +Five HD-1370A servos at stall (~1.3 A @ 4.8 V) plus the rest of the robot leaves **little safety margin** on a 1.5 A USB source. SG90 stall is typically higher still. Recommendation: diff --git a/docs/hardware/servos.md b/docs/hardware/servos.md index bfdacfa..843aee1 100644 --- a/docs/hardware/servos.md +++ b/docs/hardware/servos.md @@ -4,7 +4,7 @@ | Item | Value | | --- | --- | -| Model | PowerHD HD-1370A analog micro | +| Model | Analog micro servo — **Tower Pro SG90 recommended** ([presets](../3d/parametric-design.md)) | | Count | 5 | | Driver | Adafruit PCA9685, I2C `0x40` | | Servo supply | PCA9685 **V+** ← drawing: USB **5V** → PCA9685 **5V** (same servo rail as **V+**; not **VCC**) | @@ -38,12 +38,9 @@ Verified in [`include/pins.h`](../../include/pins.h) and [`include/servos.h`](.. | Constant | Value | Role | | --- | --- | --- | -| `SERVO_LOW` | **75.0** | Test lower angle | -| `SERVO_CENTER` | **90.0** | Neutral / start / end | -| `SERVO_HIGH` | **105.0** | Test upper angle | | `SERVO_STEP_MS` | 10 | Live update / interpolation step (ms) | | `SERVO_ANGLE_DEADBAND_DEG` | **0.32** | Stop threshold (~half PWM count) | -| `SERVO_SPEED_DEG_S` | **140.0** | Smooth rate for `POST /test/servo` (`SERVO_MAX_SPEED_DEG_S`) | +| `SERVO_MAX_SPEED_DEG_S` | **140.0** | Smooth rate for `POST /test/servo` | | `SERVO_BOOT_SPEED_DEG_S` | **35.0** | Boot centering and sleep-pose moves | ## Boot safety @@ -63,21 +60,21 @@ Firmware uses two complementary control paths: | Mode | API | Use | | --- | --- | --- | -| **Choreographed** | `setPosition()` + time easing (`anim::easedLerp`, `anim::EasedMove`) | Welcome raise/wiggle, thinking head/neck — direct PWM each frame, cubic ease-in-out | -| **Discrete** | `setTarget()` + `update()` slew | Typing, reading, ring, transitions — rate-limited chase to a fixed angle | +| **Choreographed** | `setNormPosition()` + time easing (`anim::easedLerp`, `anim::EasedMove`) | Welcome raise/wiggle, thinking head/neck — pose in −1..1, mapped to saved min/max each frame | +| **Discrete** | `setNormTarget()` + `update()` slew | Typing, reading, ring, transitions — rate-limited chase to a mapped pose | -Helpers live in [`src/animation/util.cpp`](../../src/animation/util.cpp). Blocking test moves (`moveTo`, `servoMoveAllSmooth`) also use cubic easing. +Helpers live in [`src/animation/util.cpp`](../../src/animation/util.cpp). Blocking test moves (`moveTo`) also use cubic easing. -`SERVO_MAX_SPEED_DEG_S` (140°/s) is ~28% of HD-1370A unloaded max (~500°/s @ 4.8 V) — smoother under load while staying responsive for hand taps. +`SERVO_MAX_SPEED_DEG_S` (140°/s) is ~28% of PowerHD HD-1370A unloaded max (~500°/s @ 4.8 V) — smoother under load while staying responsive for hand taps. Other presets (SG90, FS0307) still use this firmware cap. -Bring-up motion (`runServoTest`): +Bring-up motion (`runServoTest`): each joint uses its **saved** min/max (`n` in −1..1): -1. All channels → 90° -2. Smooth 90° → 105° (1 s) -3. Smooth 105° → 75° (2 s) -4. Smooth 75° → 90° (1 s) +1. All channels → mid (`n = 0`) +2. Smooth mid → +0.5 (75% of span) +3. Smooth +0.5 → −0.5 (25% of span) +4. Smooth −0.5 → mid -All five channels get the **same** angle. This is a wiring/power test, not a pose library. +This is a wiring/power test, not a pose library. ## Three ranges (do not collapse them) @@ -87,9 +84,9 @@ All five channels get the **same** angle. This is a wiring/power test, not a pos | 2 | Nominal manufacturer angle | Marketing / datasheet travel (0–180° **or** ~130° over full pulse — sources disagree) | Do not trust for installed mechanics | | 3 | Mechanical safe range | Per-joint min/max after horns and linkages | Stock defaults in `SERVO_SPECS` ([`include/servos.h`](../../include/servos.h)); saved to NVS in the setup AP wizard. See [robot-movement.md](../robot-movement.md) | -Animations and poses command **range 3**, clipped inside range 1. Bench bring-up (`runServoTest`, `/test/servo`) may still use the electrical band. +Animations author poses in **−1..1** (min / mid / max of range 3) and map through `servoNormToDeg`. `POST /test/servo` still takes electrical degrees and clamps to range 3. Setup AP `POST /setup/servo` uses range 1 (0–180°) to find limits. -Blind 0–180° on the assembled robot can stall gears, tear horns, or brown out the 5 V rail. Use the **75 / 90 / 105** test band on the bare bench; on the assembled robot stay inside the saved min/max (stock `SERVO_SPECS` until you calibrate in setup AP). +Blind 0–180° on the assembled robot can stall gears, tear horns, or brown out the 5 V rail. Stay inside the saved min/max (stock `SERVO_SPECS` until you calibrate in setup AP). ## Channel-to-mechanism mapping diff --git a/docs/hardware/testing.md b/docs/hardware/testing.md index 0841116..ee35ad4 100644 --- a/docs/hardware/testing.md +++ b/docs/hardware/testing.md @@ -9,7 +9,7 @@ Firmware is [`src/main.cpp`](../../src/main.cpp). Boot **inits** hardware and st - [`src/hardware/pca9685_servos.cpp`](../../src/hardware/pca9685_servos.cpp) - [`src/http/http_server.cpp`](../../src/http/http_server.cpp), [`src/http/test_handlers.cpp`](../../src/http/test_handlers.cpp) -Constants: [`include/pins.h`](../../include/pins.h). Wi-Fi credentials are saved in NVS and configured only in setup AP mode (first boot, or after factory reset + power-cycle). Servo min/max and RGB LED mapping (`rgb_order`) are also setup-AP-only; factory reset keeps them. +Constants: [`include/pins.h`](../../include/pins.h). Wi-Fi credentials are saved in NVS and configured only in setup AP mode (first boot, or after factory reset + power-cycle). Servo min/max, RGB LED mapping (`rgb_order`), and OLED rotation (`oled_rotate_180`) are also setup-AP-only; factory reset keeps them. Build/flash: project root README (`pio run`, `pio run -t upload`, serial 115200). Physical board is **Waveshare ESP32-C3-Zero**; PlatformIO env name is `esp32-c3-devkitm-1`. @@ -93,27 +93,29 @@ curl -X POST http://tiny-engineer.local/test/audio # OLED title / HELLO / X in a box curl -X POST http://tiny-engineer.local/test/screen -# Servos 90 → 105 → 75 → 90 (channels 0–4) +# Servos mid → +0.5 → −0.5 → mid of saved range (channels 0–4) curl -X POST http://tiny-engineer.local/test/movement # Onboard WS2812 R → G → B → white → off, then back to current animation LED curl -X POST http://tiny-engineer.local/test/led -# One servo smooth move to angle (~40°/s; index 0–4, angle 0–180) +# One servo smooth move to angle (~140°/s; index 0–4, angle 0–180) curl -X POST "http://tiny-engineer.local/test/servo?index=0&angle=90" ``` | Method | Path | Body | | --- | --- | --- | | `GET` | `/` | HTML endpoint index | -| `GET` | `/auth` | Auth status (`ok`, `required`) — always public | +| `GET` | `/auth` | Auth status (`ok`, `required`, `wifi_configured`, `provisioning`) — always public | | `GET` | `/health` | Health JSON (`ok`, `uptime_ms`, `free_heap`, `heap_size`, `cpu_temp_c`, `wifi`, `oled`) | -| `GET` | `/settings` | Persistent settings (`sleep_timeout`, `hostname`, `volume`, `welcome`, `serial_log`, `continuous_timeout`, `loading`, `access_token_set`, `wifi_configured`, `wifi_ssid`, `wifi_password_set`, `servo_mins`, `servo_maxs`, `rgb_order`) | +| `GET` | `/settings` | Persistent settings (`sleep_timeout`, `hostname`, `volume`, `welcome`, `serial_log`, `continuous_timeout`, `loading`, `access_token_set`, `wifi_configured`, `wifi_ssid`, `wifi_password_set`, `servo_mins`, `servo_maxs`, `rgb_order`, `oled_rotate_180`) | | `POST` | `/settings?...&wifi_ssid=&wifi_password=` | Update NVS settings; WiFi params setup-AP-only and tested before save; `reboot_required` if hostname changed | | `POST` | `/settings?...&servo_mins=&servo_maxs=` | Servo min/max comma lists; setup-AP-only | | `POST` | `/settings?...&rgb_order=` | WS2812 byte order (`RGB`/`RBG`/`GRB`/`GBR`/`BRG`/`BGR`); setup-AP-only; default `GRB` | -| `POST` | `/settings/reset` | Factory reset settings to defaults (clears WiFi, keeps servo ranges and RGB mapping); power-cycle to reopen setup AP | +| `POST` | `/settings?...&oled_rotate_180=` | OLED 180° rotation (`0`/`1`); setup-AP-only; default `0` | +| `POST` | `/settings/reset` | Factory reset settings to defaults (clears WiFi, keeps servo ranges, RGB mapping, and screen rotation); power-cycle to reopen setup AP | | `POST` | `/test/audio` | `{"ok":true,"test":"audio"}` after `runSoundTest()` | +| `POST` | `/test/audio/bell` | `{"ok":true,"test":"bell"}` after `playBell()` | | `POST` | `/test/screen` | `{"ok":true,"test":"screen"}` after `runOledTest()` | | `POST` | `/test/movement` | `{"ok":true,"test":"movement"}` after `runServoTest()` | | `POST` | `/test/led` | `{"ok":true,"test":"led"}` after `runRgbTest()` | @@ -121,8 +123,9 @@ curl -X POST "http://tiny-engineer.local/test/servo?index=0&angle=90" | `POST` | `/setup/servo?all=90` or `?index=&angle=` | Setup AP only; slow 0–180° move (`SERVO_CALIB_SPEED_DEG_S`) | | `POST` | `/setup/led?color=R` / `G` / `B` or `?byte=0` / `1` / `2` / `off` | Setup AP only; light logical RGB or one WS2812 wire byte, or release hold | | `POST` | `/setup/audio` | Setup AP only; play `welcome.wav` (`playWelcome()`) | +| `POST` | `/setup/oled?rotate_180=0` / `1` | Setup AP only; preview OLED 180° rotation (`THIS WAY UP`); omit param to restore provisioning text | -GET on a test path returns `405`. Bad `/test/servo`, `/setup/servo`, `/setup/led`, `/setup/audio`, or `/settings` params return `400`. Missing/wrong Bearer when auth enabled returns `401`. Control APIs return `503` when WiFi credentials are not saved (`/setup/servo`, `/setup/led`, and `/setup/audio` stay available). Unknown path returns `404`. JSON `Content-Type`. Handlers block until the test finishes; the OLED returns to `ROBOT READY` after. +GET on a test path returns `405`. Bad `/test/servo`, `/setup/servo`, `/setup/led`, `/setup/audio`, `/setup/oled`, or `/settings` params return `400`. Missing/wrong Bearer when auth enabled returns `401`. Control APIs return `503` when WiFi credentials are not saved (`/setup/servo`, `/setup/led`, `/setup/audio`, and `/setup/oled` stay available). Unknown path returns `404`. JSON `Content-Type`. Handlers block until the test finishes; the OLED returns to `ROBOT READY` after. HTTP runs on STA when connected, or on setup AP at `192.168.4.1` during provisioning. @@ -130,9 +133,9 @@ HTTP runs on STA when connected, or on setup AP at `192.168.4.1` during provisio | Serial / OLED | Meaning | Check | | --- | --- | --- | -| OLED `ERROR: OLED not found` then rest of boot runs | Nothing ACK’d at `0x3C` | OLED **VCC=3V3**, GND, SDA=GP0, **SCK**=GP1, common ground, address jumper still 0x3C | +| OLED `ERROR: OLED not found` then rest of boot runs | Nothing ACK’d at `0x3C` | OLED **VCC=3V3**, GND, SDA=GP0, **SCL**=GP1, common ground, address jumper still 0x3C | | OLED found but `ERROR: OLED initialization failed` | ACK then `display.begin` failed | Wiring/power glitch, wrong size module, I2C noise | -| OLED shows `Join this WiFi` / AP name, then `Then open` / `192.168.4.1` | Setup AP mode active | Connect to the shown AP, open `http://192.168.4.1/config`, finish the servo + LED + speaker + WiFi wizard | +| OLED shows `Join this WiFi` / AP name, then `Then open` / `192.168.4.1` | Setup AP mode active | Connect to the shown AP, open `http://192.168.4.1/config`, finish the servo + screen + LED + speaker + WiFi wizard | | OLED `WiFi failed` then setup AP | Saved STA credentials failed | Join setup AP, open `http://192.168.4.1/config`, enter home WiFi again | | `ERROR: PCA9685 not found` + red RGB + **hang** | Nothing ACK’d at `0x40` | PCA9685 **VCC=3V3** (not V+), GND, SDA/SCL, I2C address pads, +5V not required for the ACK but needed later for motion | | `ERROR: I2S initialization failed` + red RGB + **hang** | `I2S.begin` failed | GPIO2/3/4 not shorted to 5V/GND; pin constants; USB CDC still alive so you can read the line | @@ -144,7 +147,7 @@ OLED absence and Wi-Fi failure are **soft** fails. PCA9685 and I2S failures **ha ## Power during the servo test -All five servos move together on `POST /test/movement`. A weak USB port often dies **here**. If the serial port drops exactly when that POST runs: treat as brownout, not a PWM bug. Boot only parks at 90°, which is much lighter. +All five servos move together on `POST /test/movement`. A weak USB port often dies **here**. If the serial port drops exactly when that POST runs: treat as brownout, not a PWM bug. Boot parks at each joint’s calibrated mid, which is much lighter. ## What this test does not prove diff --git a/docs/hardware/wiring.md b/docs/hardware/wiring.md index cfffada..4d984e0 100644 --- a/docs/hardware/wiring.md +++ b/docs/hardware/wiring.md @@ -21,7 +21,7 @@ Diagram colour: **red** = 5 V, **blue** = GND, thin wires = signals. | PCA9685 **5V** | Servo-power *input* from USB 5V | | PCA9685 **V+** | Servo-power *output* to servo 5V | | PCA9685 **VCC** | Logic **3V3** | -| OLED **SCK** | I2C **SCL** | +| OLED **SCL** | I2C **SCL** (Waveshare silkscreen; PNG may say **SCK**) | | MAX98357A **Vin** | Amp **+5V** (**VIN**) | | MAX98357A **LRC** | **WS/LRC** | @@ -45,7 +45,7 @@ On Adafruit PCA9685, header/terminal **V+** is normally the same servo rail as t | ESP32-C3-Zero | GP0 | PCA9685 | SDA | I2C data | | ESP32-C3-Zero | GP0 | OLED | SDA | I2C data (shared) | | ESP32-C3-Zero | GP1 | PCA9685 | SCL | I2C clock | -| ESP32-C3-Zero | GP1 | OLED | SCK | I2C clock (shared; module silkscreen SCK) | +| ESP32-C3-Zero | GP1 | OLED | SCL | I2C clock (shared; Waveshare silkscreen **SCL**; PNG may say **SCK**) | | ESP32-C3-Zero | GP2 | MAX98357A | BCLK | I2S bit clock | | ESP32-C3-Zero | GP3 | MAX98357A | LRC | I2S word select | | ESP32-C3-Zero | GP4 | MAX98357A | DIN | I2S data | @@ -95,7 +95,7 @@ USB block in the drawing is the Adafruit **5993** — robot **power** (VBUS/GND) Shared bus: - ESP32 **GP0** → PCA9685 **SDA** and OLED **SDA** -- ESP32 **GP1** → PCA9685 **SCL** and OLED **SCK** +- ESP32 **GP1** → PCA9685 **SCL** and OLED **SCL** Addresses (firmware, not on the drawing): PCA9685 `0x40`, OLED `0x3C`. See [interfaces.md](interfaces.md). @@ -138,7 +138,7 @@ Still part of the selected hardware; do not invent extra ESP32 GPIO for them. | Item | Rule | | --- | --- | | Speaker | Terminals → MAX98357A **SPK+** and **SPK-** only | -| 4 extra HD-1370A | Same 3-wire pattern as the drawn servo, on PCA9685 PWM 0–4 | +| 4 extra analog micro servos | Same 3-wire pattern as the drawn servo, on PCA9685 PWM 0–4 | | Adafruit 5993 D+/D− | Wire to ESP32 **GP19** / **GP18** for flash and CDC (single-USB design) | | MAX98357A GAIN / SD | Not wired; leave breakout defaults | @@ -158,6 +158,6 @@ See [interfaces.md](interfaces.md), [power.md](power.md). 1. Every **GND** in the PNG is one net (USB, ESP32, PCA9685 both sides, OLED, MAX98357A, servo). 2. PCA9685 **VCC** ≈ 3.3 V, PCA9685 **5V** / **V+** ≈ 5 V, those two nets **not** shorted. -3. OLED clock wire lands on the pad labelled **SCK**. +3. OLED clock wire lands on Waveshare **SCL** (PNG may still say **SCK**). 4. I2S is GP2/GP3/GP4 → BCLK/LRC/DIN, not swapped with I2C. 5. Speaker (if fitted) is only SPK+/SPK-, which are **not** in the PNG. diff --git a/docs/integration.md b/docs/integration.md index 55dd1ee..f71b5b7 100644 --- a/docs/integration.md +++ b/docs/integration.md @@ -23,6 +23,11 @@ flowchart TB CursorHook --> Npx["npx tiny-engineer-cursor"] Npx --> Post end + subgraph antigravityPath [Antigravity] + AgHook[Antigravity hooks.json] + AgHook --> AgCli["tiny-engineer-antigravity"] + AgCli --> Post + end Post --> Robot[Tiny Engineer on Wi-Fi] ``` @@ -145,7 +150,7 @@ For [Google Antigravity](https://github.com/google/antigravity) (`antigravity-cl ### Setup -Inside this firmware repository, [`.agents/hooks.json`](../.agents/hooks.json) is pre-configured. +Inside this firmware repository, [`.agents/hooks.json`](../.agents/hooks.json) is pre-configured. Run the local bin: `node packages/tiny-engineer-antigravity/bin/tiny-engineer-antigravity.js`. Other machines can `npx -y --package=https://github.com/jamro/tiny-engineer/archive/refs/heads/main.tar.gz tiny-engineer-antigravity` (same tarball pattern as Cursor). To run globally across all projects on your machine, configure `~/.gemini/config/hooks.json`: @@ -196,12 +201,13 @@ To run globally across all projects on your machine, configure `~/.gemini/config - **Building for one IDE / custom agent** → REST. One `POST`, zero Node dependency. - **Using Cursor and want zero mapping code** → Cursor CLI + hooks. -- **Both** are fine together: CLI is just a thin client of the same `/anim` API. +- **Using Antigravity CLI** → Antigravity CLI + hooks. +- **All three** are fine together: the CLIs are thin clients of the same `/anim` API. -Prerequisites for either path: flash firmware, join 2.4 GHz Wi-Fi, confirm `http://tiny-engineer.local/health` (or the OLED IP) responds. +Prerequisites for any path: flash firmware, join 2.4 GHz Wi-Fi, confirm `http://tiny-engineer.local/health` (or the OLED IP) responds. --- ## Optional extras -Same `POST /anim` API; not part of the two paths above. On macOS you can run a small host helper that POSTs `sleep` / `wakeup` when the screen locks or unlocks: [`macos-lock-unlock.md`](macos-lock-unlock.md). The robot does not need it. +Same `POST /anim` API; not part of the three paths above. On macOS you can run a small host helper that POSTs `sleep` / `wakeup` when the screen locks or unlocks: [`macos-lock-unlock.md`](macos-lock-unlock.md). The robot does not need it. diff --git a/docs/robot-movement.md b/docs/robot-movement.md index c06e4a5..f6eb811 100644 --- a/docs/robot-movement.md +++ b/docs/robot-movement.md @@ -10,7 +10,9 @@ The robot sits in a chair beside a desk. A miniature laptop and a service bell s | 3 | **Right hand** | Hand up / down (inverted scale) | up | down | 35 – 125 | | 4 | **Body** | Whole torso left / right | right | left | 40 – 130 | -Ranges match stock `SERVO_SPECS` in [`include/servos.h`](../include/servos.h). Those values are **defaults**. After setup AP calibration they are stored in NVS and clamp all later motion. Factory reset keeps the saved ranges. Retune them in the setup wizard when AP mode is open. +Ranges match stock `SERVO_SPECS` in [`include/servos.h`](../include/servos.h). Those values are **defaults**. After setup AP calibration they are stored in NVS. Factory reset keeps the saved ranges. Retune them in the setup wizard when AP mode is open. + +Animations author poses in **−1..1** relative to each joint’s saved min/max (`−1` = min, `0` = mid, `+1` = max). Horn offset after assembly is absorbed by calibration; firmware maps those poses with `servoNormToDeg`. Right-hand rest is still electrical min (`−1`); left-hand rest is electrical max (`+1`). **Head** tilts the face toward or away from the laptop. **Neck** pans the head side to side. **Hands** lift and lower over the keyboard; the forearms have no elbow servo — the arm linkage is fixed, so only the hand joint moves. **Left** and **right** hand servos use opposite scales: on the left, higher angle is up; on the right, higher angle is down (rest pose for typing is left at `max`, right at `min`). **Body** rotates the whole upper body in the chair while the base stays put. diff --git a/docs/settings.md b/docs/settings.md index bb1832f..50b1266 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -2,7 +2,7 @@ Persistent settings live in NVS (namespace `te`) under [`src/settings/`](../src/settings/). They are exposed on `GET`/`POST /settings`, the Config web UI, and must stay in sync with API docs. -Existing keys: `sleep_timeout`, `hostname`, `volume`, `welcome`, `serial_log`, `continuous_timeout`, `loading`, `access_token`, `wifi_ssid`, `wifi_password`, `sranges` (packed servo min/max blob), `rgb_ord` (WS2812 byte-order string). Follow the same pattern for a new one. +Existing keys: `sleep_timeout`, `hostname`, `volume`, `welcome`, `serial_log`, `continuous_timeout`, `loading`, `access_token`, `wifi_ssid`, `wifi_password`, `sranges` (packed servo min/max blob), `rgb_ord` (WS2812 byte-order string), `oled_rot` (OLED 180° rotation bool). Follow the same pattern for a new one. ## Design choices @@ -12,7 +12,7 @@ Existing keys: `sleep_timeout`, `hostname`, `volume`, `welcome`, `serial_log`, ` | RAM | Cached in a module-static after `initSettings()`; callers use getters | | Update API | Nullable pointer args to `saveSettings(...)` — only non-null fields change | | HTTP | Query params on `POST /settings`; at least one param required | -| Apply timing | Prefer immediate apply. If boot-only (like hostname), set `reboot_required` and document it. WiFi credentials are setup-AP-only and tested before save. Servo min/max (`servo_mins` / `servo_maxs`) and RGB LED mapping (`rgb_order`) are setup-AP-only and apply immediately | +| Apply timing | Prefer immediate apply. If boot-only (like hostname), set `reboot_required` and document it. WiFi credentials are setup-AP-only and tested before save. Servo min/max (`servo_mins` / `servo_maxs`), RGB LED mapping (`rgb_order`), and OLED rotation (`oled_rotate_180`) are setup-AP-only and apply immediately | | Types | Prefer small integers / short strings; validate before any NVS write | ## Checklist @@ -30,7 +30,7 @@ Public API: [`settings.h`](../src/settings/settings.h). RAM cache: [`internal.h` 5. Extend `saveSettings(...)` in [`save.cpp`](../src/settings/save.cpp) with a nullable `const T* foo`: - Reject the whole save if validation fails. - Require at least one non-null arg among all settings. - - Write all persisted fields together (current pattern rewrites sleep/host/volume/welcome/continuous_timeout/loading/access_token each save). + - Write all persisted fields together (current pattern rewrites sleep/host/volume/welcome/serial_log/continuous_timeout/loading/access_token each save; wifi/servo/rgb/oled too). 6. Log the new value on load and save via `logSettingsSnapshot` in [`cache.cpp`](../src/settings/cache.cpp) (for secrets like `access_token`, log set/unset only — never the raw value). ### 2. Consumers @@ -85,7 +85,7 @@ Flash only when you want to try it on hardware (`pio run -t upload`). - **Partial writes:** validate first; never write NVS then fail validation mid-way. - **JSON buffer:** `sendSettingsJson` uses a fixed `char` buffer — bump size when adding fields. -- **Factory reset:** `factoryResetSettings()` in [`reset.cpp`](../src/settings/reset.cpp) clears NVS namespace `te` and writes defaults, including WiFi credentials. Servo min/max (`sranges`) and RGB LED mapping (`rgb_ord`) are written back unchanged. Exposed as `POST /settings/reset`. After reset, power-cycle into setup AP mode to configure WiFi again (WiFi is not editable on the normal Config page). Servo ranges and LED mapping can be retuned in that wizard. +- **Factory reset:** `factoryResetSettings()` in [`reset.cpp`](../src/settings/reset.cpp) clears NVS namespace `te` and writes defaults, including WiFi credentials. Servo min/max (`sranges`), RGB LED mapping (`rgb_ord`), and OLED rotation (`oled_rot`) are written back unchanged. Exposed as `POST /settings/reset`. After reset, power-cycle into setup AP mode to configure WiFi again (WiFi is not editable on the normal Config page). Servo ranges, LED mapping, and screen rotation can be retuned in that wizard. - **Hostname-style settings:** freeze the boot value separately if live change cannot apply (see `settingsBootHostname()` / `reboot_required`). - **HTML string size:** the panel is a big string literal in `index_page.cpp`; keep controls compact. - **Doc drift:** HTML param tables must match `api.md` exactly. diff --git a/docs/testing.md b/docs/testing.md index cf7c3fe..ca5a958 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -4,7 +4,7 @@ Host checks. Neither flashes the board. GitHub Actions on `main` and PRs runs th ## Firmware -Unity on the host. Compiles `src/settings/validate.cpp` only. +Unity on the host. Compiles `src/settings/validate.cpp` plus header-only helpers such as [`include/servos.h`](../include/servos.h). ```bash pio test -e native diff --git a/include/servos.h b/include/servos.h index 9f81552..2828056 100644 --- a/include/servos.h +++ b/include/servos.h @@ -31,10 +31,29 @@ constexpr float servoMid(const ServoSpec& spec) { return (spec.min + spec.max) * 0.5f; } -// Shared angles for /test/movement (not per-servo limits) -constexpr float SERVO_LOW = 75.0f; -constexpr float SERVO_CENTER = 90.0f; -constexpr float SERVO_HIGH = 105.0f; +// Pose space: -1 = saved min, 0 = mid, +1 = saved max. +inline float servoSaturateNorm(float n) { + if (n < -1.0f) { + return -1.0f; + } + if (n > 1.0f) { + return 1.0f; + } + return n; +} + +inline float servoNormToDeg(float n, float savedMin, float savedMax) { + n = servoSaturateNorm(n); + return (savedMin + savedMax) * 0.5f + n * (savedMax - savedMin) * 0.5f; +} + +inline float servoDegToNorm(float deg, float savedMin, float savedMax) { + const float span = savedMax - savedMin; + if (span <= 0.0f) { + return 0.0f; + } + return servoSaturateNorm(2.0f * (deg - savedMin) / span - 1.0f); +} // Max commanded slew rate for all smooth moves constexpr float SERVO_MAX_SPEED_DEG_S = 140.0f; diff --git a/platformio.ini b/platformio.ini index d9b6fbd..7f72967 100644 --- a/platformio.ini +++ b/platformio.ini @@ -45,4 +45,4 @@ test_build_src = true build_src_filter = -<**/*.*> + -build_flags = -std=c++17 \ No newline at end of file +build_flags = -std=c++17 -Iinclude \ No newline at end of file diff --git a/src/animation/abort.cpp b/src/animation/abort.cpp index 512eb79..3330a1d 100644 --- a/src/animation/abort.cpp +++ b/src/animation/abort.cpp @@ -45,68 +45,68 @@ bool allAbortServosStopped() { } void commandResignedPose(float speedDegS) { - servoAt(SERVO_BODY).setTarget( + servoAt(SERVO_BODY).setNormTarget( anim::ABORT_BODY_DISMISSIVE, speedDegS ); - servoAt(SERVO_NECK).setTarget( + servoAt(SERVO_NECK).setNormTarget( anim::ABORT_NECK_SIDE, speedDegS ); - servoAt(SERVO_HEAD).setTarget( + servoAt(SERVO_HEAD).setNormTarget( anim::ABORT_HEAD_UP, speedDegS ); - servoAt(SERVO_HAND_LEFT).setTarget( + servoAt(SERVO_HAND_LEFT).setNormTarget( anim::ABORT_HAND_LEFT_UP, speedDegS ); - servoAt(SERVO_HAND_RIGHT).setTarget( + servoAt(SERVO_HAND_RIGHT).setNormTarget( anim::ABORT_HAND_RIGHT_UP, speedDegS ); } void commandDidntWantBeat() { - servoAt(SERVO_HAND_LEFT).setTarget( + servoAt(SERVO_HAND_LEFT).setNormTarget( anim::ABORT_HAND_LEFT_SHRUG, ABORT_BEAT_SPEED_DEG_S ); - servoAt(SERVO_HAND_RIGHT).setTarget( + servoAt(SERVO_HAND_RIGHT).setNormTarget( anim::ABORT_HAND_RIGHT_SHRUG, ABORT_BEAT_SPEED_DEG_S ); - servoAt(SERVO_HEAD).setTarget( - anim::ABORT_HEAD_UP - 4.0f, + servoAt(SERVO_HEAD).setNormTarget( + anim::ABORT_HEAD_DIDNT_WANT, ABORT_BEAT_SPEED_DEG_S ); } void commandFinishBeat() { - servoAt(SERVO_NECK).setTarget( + servoAt(SERVO_NECK).setNormTarget( anim::ABORT_NECK_OTHER_SIDE, ABORT_BEAT_SPEED_DEG_S ); - servoAt(SERVO_HEAD).setTarget( + servoAt(SERVO_HEAD).setNormTarget( anim::ABORT_HEAD_DIP, ABORT_BEAT_SPEED_DEG_S ); } void commandAnywayBeat() { - servoAt(SERVO_NECK).setTarget( + servoAt(SERVO_NECK).setNormTarget( anim::ABORT_NECK_SIDE, ABORT_BEAT_SPEED_DEG_S ); - servoAt(SERVO_HEAD).setTarget( - anim::ABORT_HEAD_UP + 2.0f, + servoAt(SERVO_HEAD).setNormTarget( + anim::ABORT_HEAD_ANYWAY, ABORT_BEAT_SPEED_DEG_S ); - servoAt(SERVO_HAND_LEFT).setTarget( + servoAt(SERVO_HAND_LEFT).setNormTarget( anim::ABORT_HAND_LEFT_UP, ABORT_BEAT_SPEED_DEG_S ); - servoAt(SERVO_HAND_RIGHT).setTarget( + servoAt(SERVO_HAND_RIGHT).setNormTarget( anim::ABORT_HAND_RIGHT_UP, ABORT_BEAT_SPEED_DEG_S ); @@ -151,8 +151,8 @@ void beginReturnPose(uint32_t now) { g_abortReturnStartMs = now; anim::parkForTransition(); - servoAt(SERVO_HEAD).setTarget( - servoMid(SERVO_SPECS[SERVO_HEAD]), + servoAt(SERVO_HEAD).setNormTarget( + 0.0f, ABORT_RETURN_SPEED_DEG_S ); } diff --git a/src/animation/attention.cpp b/src/animation/attention.cpp index 7dfbb7a..0682157 100644 --- a/src/animation/attention.cpp +++ b/src/animation/attention.cpp @@ -37,23 +37,23 @@ bool g_handWaveHigh = false; uint8_t g_handWaveBurstLeft = 0; void commandPrepPose() { - servoAt(SERVO_BODY).setTarget( + servoAt(SERVO_BODY).setNormTarget( anim::ATTENTION_BODY_MID, ATTENTION_PREP_SPEED_DEG_S ); - servoAt(SERVO_NECK).setTarget( + servoAt(SERVO_NECK).setNormTarget( anim::ATTENTION_NECK_MID, ATTENTION_PREP_SPEED_DEG_S ); - servoAt(SERVO_HEAD).setTarget( + servoAt(SERVO_HEAD).setNormTarget( anim::ATTENTION_HEAD_READY, ATTENTION_PREP_SPEED_DEG_S ); - servoAt(SERVO_HAND_LEFT).setTarget( + servoAt(SERVO_HAND_LEFT).setNormTarget( anim::ATTENTION_HAND_LEFT_PARKED, ATTENTION_PREP_SPEED_DEG_S ); - servoAt(SERVO_HAND_RIGHT).setTarget( + servoAt(SERVO_HAND_RIGHT).setNormTarget( anim::ATTENTION_HAND_RIGHT_RAISED, ATTENTION_PREP_SPEED_DEG_S ); @@ -88,17 +88,17 @@ void commandWaitMove(uint32_t now) { } const float headOffset = g_waitNodHigh - ? anim::ATTENTION_WAIT_HEAD_NOD_DEG - : -anim::ATTENTION_WAIT_HEAD_NOD_DEG; + ? anim::ATTENTION_WAIT_HEAD_NOD + : -anim::ATTENTION_WAIT_HEAD_NOD; const float neckOffset = g_waitNeckRight - ? anim::ATTENTION_WAIT_NECK_TILT_DEG - : -anim::ATTENTION_WAIT_NECK_TILT_DEG; + ? anim::ATTENTION_WAIT_NECK_TILT + : -anim::ATTENTION_WAIT_NECK_TILT; - servoAt(SERVO_HEAD).setTarget( + servoAt(SERVO_HEAD).setNormTarget( anim::ATTENTION_HEAD_READY + headOffset, ATTENTION_WAIT_SPEED_DEG_S ); - servoAt(SERVO_NECK).setTarget( + servoAt(SERVO_NECK).setNormTarget( anim::ATTENTION_NECK_MID + neckOffset, ATTENTION_WAIT_SPEED_DEG_S ); @@ -122,10 +122,10 @@ void commandHandWave(uint32_t now) { } const float handOffset = g_handWaveHigh - ? anim::ATTENTION_WAIT_HAND_WAVE_DEG - : -anim::ATTENTION_WAIT_HAND_WAVE_DEG * 0.45f; + ? anim::ATTENTION_WAIT_HAND_WAVE + : -anim::ATTENTION_WAIT_HAND_WAVE * 0.45f; - servoAt(SERVO_HAND_RIGHT).setTarget( + servoAt(SERVO_HAND_RIGHT).setNormTarget( anim::ATTENTION_HAND_RIGHT_RAISED + handOffset, ATTENTION_WAVE_SPEED_DEG_S ); @@ -152,14 +152,14 @@ void applyAttentionAudioPose(uint32_t audioElapsed, uint32_t now) { } else if (audioElapsed < ATTENTION_AUDIO_HUMAN_END_MS) { neckAngle = easedLerp( anim::ATTENTION_NECK_MID, - anim::ATTENTION_NECK_MID + anim::ATTENTION_HUMAN_NECK_GLANCE_DEG, + anim::ATTENTION_NECK_MID + anim::ATTENTION_HUMAN_NECK_GLANCE, g_attentionAudioStartMs + ATTENTION_AUDIO_PST_END_MS, ATTENTION_AUDIO_HUMAN_END_MS - ATTENTION_AUDIO_PST_END_MS, now ); headAngle = easedLerp( anim::ATTENTION_HEAD_READY, - anim::ATTENTION_HEAD_READY + anim::ATTENTION_HUMAN_HEAD_NOD_DEG, + anim::ATTENTION_HEAD_READY + anim::ATTENTION_HUMAN_HEAD_NOD, g_attentionAudioStartMs + ATTENTION_AUDIO_PST_END_MS, ATTENTION_AUDIO_HUMAN_END_MS - ATTENTION_AUDIO_PST_END_MS, now @@ -167,15 +167,15 @@ void applyAttentionAudioPose(uint32_t audioElapsed, uint32_t now) { handAngle = anim::ATTENTION_HAND_RIGHT_RAISED; } else if (audioElapsed < ATTENTION_AUDIO_END_MS) { neckAngle = easedLerp( - anim::ATTENTION_NECK_MID + anim::ATTENTION_HUMAN_NECK_GLANCE_DEG, - anim::ATTENTION_NECK_MID - anim::ATTENTION_LOOK_NECK_GLANCE_DEG, + anim::ATTENTION_NECK_MID + anim::ATTENTION_HUMAN_NECK_GLANCE, + anim::ATTENTION_NECK_MID - anim::ATTENTION_LOOK_NECK_GLANCE, g_attentionAudioStartMs + ATTENTION_AUDIO_HUMAN_END_MS, ATTENTION_AUDIO_END_MS - ATTENTION_AUDIO_HUMAN_END_MS, now ); headAngle = easedLerp( - anim::ATTENTION_HEAD_READY + anim::ATTENTION_HUMAN_HEAD_NOD_DEG, - anim::ATTENTION_HEAD_READY - anim::ATTENTION_LOOK_HEAD_NOD_DEG, + anim::ATTENTION_HEAD_READY + anim::ATTENTION_HUMAN_HEAD_NOD, + anim::ATTENTION_HEAD_READY - anim::ATTENTION_LOOK_HEAD_NOD, g_attentionAudioStartMs + ATTENTION_AUDIO_HUMAN_END_MS, ATTENTION_AUDIO_END_MS - ATTENTION_AUDIO_HUMAN_END_MS, now @@ -188,14 +188,14 @@ void applyAttentionAudioPose(uint32_t audioElapsed, uint32_t now) { now ); } else { - neckAngle = anim::ATTENTION_NECK_MID - anim::ATTENTION_LOOK_NECK_GLANCE_DEG; - headAngle = anim::ATTENTION_HEAD_READY - anim::ATTENTION_LOOK_HEAD_NOD_DEG; + neckAngle = anim::ATTENTION_NECK_MID - anim::ATTENTION_LOOK_NECK_GLANCE; + headAngle = anim::ATTENTION_HEAD_READY - anim::ATTENTION_LOOK_HEAD_NOD; handAngle = anim::ATTENTION_HAND_POINT; } - servoAt(SERVO_NECK).setPosition(neckAngle); - servoAt(SERVO_HEAD).setPosition(headAngle); - servoAt(SERVO_HAND_RIGHT).setPosition(handAngle); + servoAt(SERVO_NECK).setNormPosition(neckAngle); + servoAt(SERVO_HEAD).setNormPosition(headAngle); + servoAt(SERVO_HAND_RIGHT).setNormPosition(handAngle); } void enterAwaitInput(uint32_t now) { @@ -207,7 +207,7 @@ void enterAwaitInput(uint32_t now) { g_handWaveBurstLeft = 0; scheduleNextWaitMove(now); scheduleNextHandWave(now); - servoAt(SERVO_HAND_RIGHT).setTarget( + servoAt(SERVO_HAND_RIGHT).setNormTarget( anim::ATTENTION_HAND_RIGHT_RAISED, ATTENTION_WAVE_SPEED_DEG_S ); diff --git a/src/animation/constants.h b/src/animation/constants.h index b941826..842ba79 100644 --- a/src/animation/constants.h +++ b/src/animation/constants.h @@ -2,138 +2,96 @@ #include -#include "hardware/servo_wrapper.h" -#include "servos.h" - namespace anim { -constexpr float TYPING_HAND_BAND_DEG = 15.0f; -constexpr float TYPING_HEAD_BAND_DEG = 10.0f; -constexpr float TYPING_SWAY_DEG = 5.0f; -constexpr float READING_HEAD_BAND_DEG = 10.0f; -constexpr float READING_NECK_SWAY_DEG = 10.0f; +// Pose units are -1..1 (saved min / mid / max). Speeds stay deg/s. + +constexpr float TYPING_HAND_BAND = 1.0f / 3.0f; +constexpr float TYPING_HEAD_BAND = 2.0f / 7.0f; +constexpr float TYPING_SWAY = 1.0f / 9.0f; +constexpr float READING_HEAD_BAND = 2.0f / 7.0f; +constexpr float READING_NECK_SWAY = 2.0f / 9.0f; constexpr float TRANSITION_TORSO_SPEED_DEG_S = 25.0f; constexpr float TRANSITION_HAND_SPEED_DEG_S = 90.0f; -constexpr float TYPING_RIGHT_LOW = - SERVO_SPECS[SERVO_HAND_RIGHT].min; -constexpr float TYPING_RIGHT_HIGH = - SERVO_SPECS[SERVO_HAND_RIGHT].min + TYPING_HAND_BAND_DEG; -constexpr float TYPING_LEFT_LOW = - SERVO_SPECS[SERVO_HAND_LEFT].max - TYPING_HAND_BAND_DEG; -constexpr float TYPING_LEFT_HIGH = - SERVO_SPECS[SERVO_HAND_LEFT].max; - -constexpr float TYPING_HEAD_LOW = - SERVO_SPECS[SERVO_HEAD].min; -constexpr float TYPING_HEAD_HIGH = - SERVO_SPECS[SERVO_HEAD].min + TYPING_HEAD_BAND_DEG; - -constexpr float TYPING_BODY_MID = - servoMid(SERVO_SPECS[SERVO_BODY]); -constexpr float TYPING_NECK_MID = - servoMid(SERVO_SPECS[SERVO_NECK]); - -constexpr float READING_HEAD_LOW = - SERVO_SPECS[SERVO_HEAD].min; -constexpr float READING_HEAD_HIGH = - SERVO_SPECS[SERVO_HEAD].min + READING_HEAD_BAND_DEG; -constexpr float READING_NECK_MID = - servoMid(SERVO_SPECS[SERVO_NECK]); - -constexpr float RING_HEAD_START = - servoMid(SERVO_SPECS[SERVO_HEAD]) + 10.0f; -constexpr float RING_BODY_START = SERVO_SPECS[SERVO_BODY].min; -constexpr float RING_RIGHT_START = SERVO_SPECS[SERVO_HAND_RIGHT].max; -constexpr float RING_LEFT_START = - SERVO_SPECS[SERVO_HAND_LEFT].max - 10.0f; -constexpr float RING_NECK_START = - servoMid(SERVO_SPECS[SERVO_NECK]); -constexpr float RING_RIGHT_STRIKE = - SERVO_SPECS[SERVO_HAND_RIGHT].min + 5.0f; -constexpr float RING_RIGHT_BOUNCE = - SERVO_SPECS[SERVO_HAND_RIGHT].min + 20.0f; -constexpr float RING_HEAD_STRIKE = SERVO_SPECS[SERVO_HEAD].min; - -constexpr float WELCOME_HEAD_UP = - servoMid(SERVO_SPECS[SERVO_HEAD]) + 10.0f; -constexpr float WELCOME_HEAD_MID = - servoMid(SERVO_SPECS[SERVO_HEAD]); -constexpr float WELCOME_HAND_REST = - SERVO_SPECS[SERVO_HAND_RIGHT].min; -constexpr float WELCOME_HAND_RAISED = - SERVO_SPECS[SERVO_HAND_RIGHT].max; -constexpr float WELCOME_HAND_WIGGLE_DEG = 8.0f; - -constexpr float ATTENTION_BODY_MID = - servoMid(SERVO_SPECS[SERVO_BODY]); -constexpr float ATTENTION_NECK_MID = - servoMid(SERVO_SPECS[SERVO_NECK]); -constexpr float ATTENTION_HEAD_READY = - servoMid(SERVO_SPECS[SERVO_HEAD]) + 6.0f; -constexpr float ATTENTION_HAND_LEFT_PARKED = - SERVO_SPECS[SERVO_HAND_LEFT].max; -constexpr float ATTENTION_HAND_RIGHT_REST = - SERVO_SPECS[SERVO_HAND_RIGHT].min; -constexpr float ATTENTION_HAND_RIGHT_RAISED = - SERVO_SPECS[SERVO_HAND_RIGHT].min + 48.0f; -constexpr float ATTENTION_HAND_POINT = - SERVO_SPECS[SERVO_HAND_RIGHT].min + 60.0f; -constexpr float ATTENTION_HUMAN_NECK_GLANCE_DEG = 4.0f; -constexpr float ATTENTION_HUMAN_HEAD_NOD_DEG = 2.0f; -constexpr float ATTENTION_LOOK_NECK_GLANCE_DEG = 6.0f; -constexpr float ATTENTION_LOOK_HEAD_NOD_DEG = 2.0f; -constexpr float ATTENTION_WAIT_HEAD_NOD_DEG = 3.0f; -constexpr float ATTENTION_WAIT_NECK_TILT_DEG = 4.0f; -constexpr float ATTENTION_WAIT_HAND_WAVE_DEG = 7.0f; +constexpr float TYPING_RIGHT_LOW = -1.0f; +constexpr float TYPING_RIGHT_HIGH = TYPING_RIGHT_LOW + TYPING_HAND_BAND; +constexpr float TYPING_LEFT_HIGH = 1.0f; +constexpr float TYPING_LEFT_LOW = TYPING_LEFT_HIGH - TYPING_HAND_BAND; + +constexpr float TYPING_HEAD_LOW = -1.0f; +constexpr float TYPING_HEAD_HIGH = TYPING_HEAD_LOW + TYPING_HEAD_BAND; + +constexpr float TYPING_BODY_MID = 0.0f; +constexpr float TYPING_NECK_MID = 0.0f; + +constexpr float READING_HEAD_LOW = -1.0f; +constexpr float READING_HEAD_HIGH = READING_HEAD_LOW + READING_HEAD_BAND; +constexpr float READING_NECK_MID = 0.0f; + +constexpr float RING_HEAD_START = 2.0f / 7.0f; +constexpr float RING_BODY_START = -1.0f; +constexpr float RING_RIGHT_START = 1.0f; +constexpr float RING_LEFT_START = 7.0f / 9.0f; +constexpr float RING_NECK_START = 0.0f; +constexpr float RING_RIGHT_STRIKE = -8.0f / 9.0f; +constexpr float RING_RIGHT_BOUNCE = -5.0f / 9.0f; +constexpr float RING_HEAD_STRIKE = -1.0f; + +constexpr float WELCOME_HEAD_UP = 2.0f / 7.0f; +constexpr float WELCOME_HEAD_MID = 0.0f; +constexpr float WELCOME_HAND_REST = -1.0f; +constexpr float WELCOME_HAND_RAISED = 1.0f; +constexpr float WELCOME_HAND_WIGGLE = 8.0f / 45.0f; +constexpr float WELCOME_HEAD_NOD = 3.0f / 35.0f; + +constexpr float ATTENTION_BODY_MID = 0.0f; +constexpr float ATTENTION_NECK_MID = 0.0f; +constexpr float ATTENTION_HEAD_READY = 6.0f / 35.0f; +constexpr float ATTENTION_HAND_LEFT_PARKED = 1.0f; +constexpr float ATTENTION_HAND_RIGHT_REST = -1.0f; +constexpr float ATTENTION_HAND_RIGHT_RAISED = 1.0f / 15.0f; +constexpr float ATTENTION_HAND_POINT = 1.0f / 3.0f; +constexpr float ATTENTION_HUMAN_NECK_GLANCE = 4.0f / 45.0f; +constexpr float ATTENTION_HUMAN_HEAD_NOD = 2.0f / 35.0f; +constexpr float ATTENTION_LOOK_NECK_GLANCE = 2.0f / 15.0f; +constexpr float ATTENTION_LOOK_HEAD_NOD = 2.0f / 35.0f; +constexpr float ATTENTION_WAIT_HEAD_NOD = 3.0f / 35.0f; +constexpr float ATTENTION_WAIT_NECK_TILT = 4.0f / 45.0f; +constexpr float ATTENTION_WAIT_HAND_WAVE = 7.0f / 45.0f; // Attention/error wait after audio before auto-finish to none. constexpr uint32_t NON_CONTINUOUS_HOLD_MS = 60UL * 1000UL; -constexpr float ERROR_BODY_TASK_SIDE = - servoMid(SERVO_SPECS[SERVO_BODY]) - 8.0f; -constexpr float ERROR_NECK_TASK_SIDE = - servoMid(SERVO_SPECS[SERVO_NECK]) - 10.0f; -constexpr float ERROR_HEAD_CONCERNED = - servoMid(SERVO_SPECS[SERVO_HEAD]) - 12.0f; -constexpr float ERROR_HAND_RIGHT_PRESENT = - SERVO_SPECS[SERVO_HAND_RIGHT].min + 58.0f; -constexpr float ERROR_HAND_LEFT_TASK_POINT = - SERVO_SPECS[SERVO_HAND_LEFT].max - 22.0f; -constexpr float ERROR_HELP_NECK_GLANCE_DEG = 10.0f; -constexpr float ERROR_AWAY_NECK_GLANCE_DEG = 4.0f; -constexpr float ERROR_HELP_HEAD_GLANCE_DEG = 3.0f; -constexpr float ERROR_WORRY_HEAD_NOD_DEG = 2.0f; - -constexpr float ABORT_BODY_DISMISSIVE = - servoMid(SERVO_SPECS[SERVO_BODY]) - 5.0f; -constexpr float ABORT_NECK_SIDE = - servoMid(SERVO_SPECS[SERVO_NECK]) + 14.0f; -constexpr float ABORT_NECK_OTHER_SIDE = - servoMid(SERVO_SPECS[SERVO_NECK]) - 6.0f; -constexpr float ABORT_HEAD_UP = - servoMid(SERVO_SPECS[SERVO_HEAD]) + 16.0f; -constexpr float ABORT_HEAD_DIP = - servoMid(SERVO_SPECS[SERVO_HEAD]) + 5.0f; +constexpr float ERROR_BODY_TASK_SIDE = -8.0f / 45.0f; +constexpr float ERROR_NECK_TASK_SIDE = -2.0f / 9.0f; +constexpr float ERROR_HEAD_CONCERNED = -12.0f / 35.0f; +constexpr float ERROR_HAND_RIGHT_PRESENT = 13.0f / 45.0f; +constexpr float ERROR_HAND_LEFT_TASK_POINT = 23.0f / 45.0f; +constexpr float ERROR_HELP_NECK_GLANCE = 2.0f / 9.0f; +constexpr float ERROR_AWAY_NECK_GLANCE = 4.0f / 45.0f; +constexpr float ERROR_HELP_HEAD_GLANCE = 3.0f / 35.0f; +constexpr float ERROR_WORRY_HEAD_NOD = 2.0f / 35.0f; + +constexpr float ABORT_BODY_DISMISSIVE = -1.0f / 9.0f; +constexpr float ABORT_NECK_SIDE = 14.0f / 45.0f; +constexpr float ABORT_NECK_OTHER_SIDE = -2.0f / 15.0f; +constexpr float ABORT_HEAD_UP = 16.0f / 35.0f; +constexpr float ABORT_HEAD_DIP = 1.0f / 7.0f; +constexpr float ABORT_HEAD_DIDNT_WANT = 12.0f / 35.0f; +constexpr float ABORT_HEAD_ANYWAY = 18.0f / 35.0f; // Idle sleep / boot sleep-inertia: chin down, then rise to mid on wake. -constexpr float SLEEP_HEAD_DOWN_OFFSET_DEG = -20.0f; -constexpr float SLEEP_HEAD_DOWN = - servoMid(SERVO_SPECS[SERVO_HEAD]) + SLEEP_HEAD_DOWN_OFFSET_DEG; -constexpr float SLEEP_HEAD_AWAKE = - servoMid(SERVO_SPECS[SERVO_HEAD]); -constexpr float ABORT_HAND_RIGHT_UP = - SERVO_SPECS[SERVO_HAND_RIGHT].max; -constexpr float ABORT_HAND_RIGHT_SHRUG = - SERVO_SPECS[SERVO_HAND_RIGHT].max - 10.0f; -constexpr float ABORT_HAND_LEFT_UP = - SERVO_SPECS[SERVO_HAND_LEFT].max - 55.0f; -constexpr float ABORT_HAND_LEFT_SHRUG = - SERVO_SPECS[SERVO_HAND_LEFT].max - 43.0f; +constexpr float SLEEP_HEAD_DOWN = -4.0f / 7.0f; +constexpr float SLEEP_HEAD_AWAKE = 0.0f; +constexpr float ABORT_HAND_RIGHT_UP = 1.0f; +constexpr float ABORT_HAND_RIGHT_SHRUG = 7.0f / 9.0f; +constexpr float ABORT_HAND_LEFT_UP = -2.0f / 9.0f; +constexpr float ABORT_HAND_LEFT_SHRUG = 2.0f / 45.0f; // Power-loss collapse: chin fully down, hands parked. -constexpr float DEAD_HEAD_DOWN = SERVO_SPECS[SERVO_HEAD].min; +constexpr float DEAD_HEAD_DOWN = -1.0f; constexpr float DEAD_COLLAPSE_SPEED_DEG_S = 48.0f; } // namespace anim diff --git a/src/animation/dead.cpp b/src/animation/dead.cpp index a601cdc..ed61f3d 100644 --- a/src/animation/dead.cpp +++ b/src/animation/dead.cpp @@ -34,7 +34,7 @@ bool allDeadServosStopped() { void commandCollapsePose() { anim::parkTorso(anim::DEAD_COLLAPSE_SPEED_DEG_S); anim::parkHands(anim::DEAD_COLLAPSE_SPEED_DEG_S); - servoAt(SERVO_HEAD).setTarget( + servoAt(SERVO_HEAD).setNormTarget( anim::DEAD_HEAD_DOWN, anim::DEAD_COLLAPSE_SPEED_DEG_S ); diff --git a/src/animation/error.cpp b/src/animation/error.cpp index 8dc2ebe..41d652c 100644 --- a/src/animation/error.cpp +++ b/src/animation/error.cpp @@ -38,23 +38,23 @@ bool g_holdNodLow = false; ErrorLook g_currentLook = ErrorLook::Task; void commandObstaclePose() { - servoAt(SERVO_BODY).setTarget( + servoAt(SERVO_BODY).setNormTarget( anim::ERROR_BODY_TASK_SIDE, ERROR_PREP_SPEED_DEG_S ); - servoAt(SERVO_NECK).setTarget( + servoAt(SERVO_NECK).setNormTarget( anim::ERROR_NECK_TASK_SIDE, ERROR_PREP_SPEED_DEG_S ); - servoAt(SERVO_HEAD).setTarget( + servoAt(SERVO_HEAD).setNormTarget( anim::ERROR_HEAD_CONCERNED, ERROR_PREP_SPEED_DEG_S ); - servoAt(SERVO_HAND_LEFT).setTarget( + servoAt(SERVO_HAND_LEFT).setNormTarget( anim::ERROR_HAND_LEFT_TASK_POINT, ERROR_PREP_SPEED_DEG_S ); - servoAt(SERVO_HAND_RIGHT).setTarget( + servoAt(SERVO_HAND_RIGHT).setNormTarget( anim::ERROR_HAND_RIGHT_PRESENT, ERROR_PREP_SPEED_DEG_S ); @@ -94,28 +94,28 @@ void commandNervousLook(uint32_t now) { float neckOffset = 0.0f; float headOffset = g_holdNodLow - ? -anim::ERROR_WORRY_HEAD_NOD_DEG - : anim::ERROR_WORRY_HEAD_NOD_DEG; + ? -anim::ERROR_WORRY_HEAD_NOD + : anim::ERROR_WORRY_HEAD_NOD; switch (g_currentLook) { case ErrorLook::Task: neckOffset = 0.0f; - headOffset -= anim::ERROR_HELP_HEAD_GLANCE_DEG; + headOffset -= anim::ERROR_HELP_HEAD_GLANCE; break; case ErrorLook::Human: - neckOffset = anim::ERROR_HELP_NECK_GLANCE_DEG; - headOffset += anim::ERROR_HELP_HEAD_GLANCE_DEG; + neckOffset = anim::ERROR_HELP_NECK_GLANCE; + headOffset += anim::ERROR_HELP_HEAD_GLANCE; break; case ErrorLook::Away: - neckOffset = -anim::ERROR_AWAY_NECK_GLANCE_DEG; + neckOffset = -anim::ERROR_AWAY_NECK_GLANCE; break; } - servoAt(SERVO_HEAD).setTarget( + servoAt(SERVO_HEAD).setNormTarget( anim::ERROR_HEAD_CONCERNED + headOffset, ERROR_GLANCE_SPEED_DEG_S ); - servoAt(SERVO_NECK).setTarget( + servoAt(SERVO_NECK).setNormTarget( anim::ERROR_NECK_TASK_SIDE + neckOffset, ERROR_GLANCE_SPEED_DEG_S ); diff --git a/src/animation/reading.cpp b/src/animation/reading.cpp index 1058c7b..900f392 100644 --- a/src/animation/reading.cpp +++ b/src/animation/reading.cpp @@ -28,23 +28,23 @@ uint32_t g_scrollIdleUntilMs = 0; void commandReadingHead() { if (g_headHigh) { const float upSpeed = 12.0f + 10.0f * randUnit(); - servoAt(SERVO_HEAD).setTarget(anim::READING_HEAD_HIGH, upSpeed); + servoAt(SERVO_HEAD).setNormTarget(anim::READING_HEAD_HIGH, upSpeed); } else { const float downSpeed = 2.5f + 2.5f * randUnit(); - servoAt(SERVO_HEAD).setTarget(anim::READING_HEAD_LOW, downSpeed); + servoAt(SERVO_HEAD).setNormTarget(anim::READING_HEAD_LOW, downSpeed); } } void commandReadingNeck() { const float speed = 8.0f + 8.0f * randUnit(); if (g_neckAngleHigh) { - servoAt(SERVO_NECK).setTarget( - anim::READING_NECK_MID + anim::READING_NECK_SWAY_DEG, + servoAt(SERVO_NECK).setNormTarget( + anim::READING_NECK_MID + anim::READING_NECK_SWAY, speed ); } else { - servoAt(SERVO_NECK).setTarget( - anim::READING_NECK_MID - anim::READING_NECK_SWAY_DEG, + servoAt(SERVO_NECK).setNormTarget( + anim::READING_NECK_MID - anim::READING_NECK_SWAY, speed ); } @@ -54,8 +54,8 @@ void commandScrollPress() { const float lift = 0.55f + 0.45f * randUnit(); const float speedDegS = SERVO_MAX_SPEED_DEG_S * (0.85f + 0.15f * randUnit()); - servoAt(SERVO_HAND_RIGHT).setTarget( - anim::TYPING_RIGHT_LOW + lift * anim::TYPING_HAND_BAND_DEG, + servoAt(SERVO_HAND_RIGHT).setNormTarget( + anim::TYPING_RIGHT_LOW + lift * anim::TYPING_HAND_BAND, speedDegS ); } @@ -63,7 +63,7 @@ void commandScrollPress() { void commandScrollRelease() { const float speedDegS = SERVO_MAX_SPEED_DEG_S * (0.55f + 0.20f * randUnit()); - servoAt(SERVO_HAND_RIGHT).setTarget(anim::TYPING_RIGHT_LOW, speedDegS); + servoAt(SERVO_HAND_RIGHT).setNormTarget(anim::TYPING_RIGHT_LOW, speedDegS); } void scheduleNextScrollBurst(uint32_t now) { diff --git a/src/animation/ring.cpp b/src/animation/ring.cpp index e1d0057..9baa114 100644 --- a/src/animation/ring.cpp +++ b/src/animation/ring.cpp @@ -31,68 +31,68 @@ enum class RingPhase { RingPhase g_ringPhase = RingPhase::ReachStart; void commandStartPose() { - servoAt(SERVO_BODY).setTarget( + servoAt(SERVO_BODY).setNormTarget( anim::RING_BODY_START, RING_START_SPEED_DEG_S ); - servoAt(SERVO_NECK).setTarget( + servoAt(SERVO_NECK).setNormTarget( anim::RING_NECK_START, RING_START_SPEED_DEG_S ); - servoAt(SERVO_HEAD).setTarget( + servoAt(SERVO_HEAD).setNormTarget( anim::RING_HEAD_START, RING_START_SPEED_DEG_S ); - servoAt(SERVO_HAND_LEFT).setTarget( + servoAt(SERVO_HAND_LEFT).setNormTarget( anim::RING_LEFT_START, RING_START_SPEED_DEG_S ); - servoAt(SERVO_HAND_RIGHT).setTarget( + servoAt(SERVO_HAND_RIGHT).setNormTarget( anim::RING_RIGHT_START, RING_START_SPEED_DEG_S ); } void commandStrike() { - servoAt(SERVO_HAND_RIGHT).setTarget( + servoAt(SERVO_HAND_RIGHT).setNormTarget( anim::RING_RIGHT_STRIKE, RING_STRIKE_SPEED_DEG_S ); - servoAt(SERVO_HEAD).setTarget( + servoAt(SERVO_HEAD).setNormTarget( anim::RING_HEAD_STRIKE, RING_HEAD_STRIKE_SPEED_DEG_S ); } void commandBounce() { - servoAt(SERVO_HAND_RIGHT).setTarget( + servoAt(SERVO_HAND_RIGHT).setNormTarget( anim::RING_RIGHT_BOUNCE, RING_BOUNCE_SPEED_DEG_S ); } void commandReturnCenter() { - servoAt(SERVO_BODY).setTarget( - servoMid(SERVO_SPECS[SERVO_BODY]), + servoAt(SERVO_BODY).setNormTarget( + 0.0f, RING_RETURN_SPEED_DEG_S ); - servoAt(SERVO_HAND_RIGHT).setTarget( - SERVO_SPECS[SERVO_HAND_RIGHT].min, + servoAt(SERVO_HAND_RIGHT).setNormTarget( + -1.0f, RING_RETURN_SPEED_DEG_S ); - servoAt(SERVO_HEAD).setTarget( - servoMid(SERVO_SPECS[SERVO_HEAD]), + servoAt(SERVO_HEAD).setNormTarget( + 0.0f, RING_RETURN_SPEED_DEG_S ); - servoAt(SERVO_NECK).setTarget( - servoMid(SERVO_SPECS[SERVO_NECK]), + servoAt(SERVO_NECK).setNormTarget( + 0.0f, RING_RETURN_SPEED_DEG_S ); } void commandReturnLeftHand() { - servoAt(SERVO_HAND_LEFT).setTarget( - SERVO_SPECS[SERVO_HAND_LEFT].max, + servoAt(SERVO_HAND_LEFT).setNormTarget( + 1.0f, RING_RETURN_SPEED_DEG_S ); } diff --git a/src/animation/sleep_anim.cpp b/src/animation/sleep_anim.cpp index 50815b2..89945d2 100644 --- a/src/animation/sleep_anim.cpp +++ b/src/animation/sleep_anim.cpp @@ -16,7 +16,8 @@ bool headAtSleepPose() { } const float err = - servoAt(SERVO_HEAD).angle() - anim::SLEEP_HEAD_DOWN; + servoAt(SERVO_HEAD).angle() - + servoNormToDeg(SERVO_HEAD, anim::SLEEP_HEAD_DOWN); return err > -1.5f && err < 1.5f; } diff --git a/src/animation/thinking.cpp b/src/animation/thinking.cpp index 12d92a5..95de618 100644 --- a/src/animation/thinking.cpp +++ b/src/animation/thinking.cpp @@ -21,9 +21,12 @@ using anim::stopAnimServos; namespace { -constexpr float THINK_NECK_MID = - servoMid(SERVO_SPECS[SERVO_NECK]); -constexpr float THINK_JITTER_DEG = 3.5f; +constexpr float THINK_HEAD_JITTER = 3.5f / 35.0f; +constexpr float THINK_NECK_JITTER = 3.5f / 45.0f; +constexpr float THINK_HEAD_MICRO = 1.5f / 35.0f; +constexpr float THINK_HEAD_MICRO_SPAN = 3.5f / 35.0f; +constexpr float THINK_NECK_MICRO = 1.5f / 45.0f; +constexpr float THINK_NECK_MICRO_SPAN = 3.5f / 45.0f; constexpr uint32_t THINK_MIN_POSE_CHANGE_MS = 2200; enum class ThinkPhase { @@ -34,8 +37,8 @@ enum class ThinkPhase { }; struct ThinkPose { - float headDeg; - float neckDeg; + float headNorm; + float neckNorm; }; ThinkPhase g_thinkPhase = ThinkPhase::TransitionPark; @@ -49,35 +52,27 @@ bool g_thinkDidMicro = false; uint8_t g_thinkMicroChain = 0; constexpr ThinkPose THINK_POSES[] = { - {125.0f, THINK_NECK_MID}, - {122.0f, THINK_NECK_MID - 8.0f}, - {120.0f, THINK_NECK_MID + 8.0f}, - {128.0f, THINK_NECK_MID - 5.0f}, - {127.0f, THINK_NECK_MID + 6.0f}, - {123.0f, THINK_NECK_MID - 10.0f}, - {121.0f, THINK_NECK_MID + 10.0f}, + {6.0f / 7.0f, 0.0f}, + {27.0f / 35.0f, -8.0f / 45.0f}, + {5.0f / 7.0f, 8.0f / 45.0f}, + {33.0f / 35.0f, -1.0f / 9.0f}, + {32.0f / 35.0f, 2.0f / 15.0f}, + {4.0f / 7.0f, -2.0f / 9.0f}, + {26.0f / 35.0f, 2.0f / 9.0f}, }; constexpr uint8_t THINK_POSE_COUNT = sizeof(THINK_POSES) / sizeof(THINK_POSES[0]); -float clampThinkHead(float deg) { - return constrain( - deg, - SERVO_SPECS[SERVO_HEAD].min, - SERVO_SPECS[SERVO_HEAD].max - ); +float clampThinkNorm(float n) { + return servoSaturateNorm(n); } -float clampThinkNeck(float deg) { - return constrain( - deg, - SERVO_SPECS[SERVO_NECK].min, - SERVO_SPECS[SERVO_NECK].max - ); +float jitterHead() { + return (randUnit() * 2.0f - 1.0f) * THINK_HEAD_JITTER; } -float jitterDeg() { - return (randUnit() * 2.0f - 1.0f) * THINK_JITTER_DEG; +float jitterNeck() { + return (randUnit() * 2.0f - 1.0f) * THINK_NECK_JITTER; } uint32_t jitterPauseMs(uint32_t baseMs) { @@ -88,8 +83,8 @@ uint32_t jitterPauseMs(uint32_t baseMs) { ThinkPose perturbedPose(uint8_t index) { const ThinkPose& base = THINK_POSES[index % THINK_POSE_COUNT]; ThinkPose pose; - pose.headDeg = clampThinkHead(base.headDeg + jitterDeg()); - pose.neckDeg = clampThinkNeck(base.neckDeg + jitterDeg()); + pose.headNorm = clampThinkNorm(base.headNorm + jitterHead()); + pose.neckNorm = clampThinkNorm(base.neckNorm + jitterNeck()); return pose; } @@ -99,8 +94,8 @@ uint8_t pickNextThinkPoseIndex() { } if (randChance(58)) { - const float curHead = servoAt(SERVO_HEAD).angle(); - const float curNeck = servoAt(SERVO_NECK).angle(); + const float curHead = servoDegToNorm(SERVO_HEAD, servoAt(SERVO_HEAD).angle()); + const float curNeck = servoDegToNorm(SERVO_NECK, servoAt(SERVO_NECK).angle()); uint8_t bestIdx = g_thinkPoseIndex; float bestDist = 999.0f; uint8_t secondIdx = g_thinkPoseIndex; @@ -112,8 +107,8 @@ uint8_t pickNextThinkPoseIndex() { } const ThinkPose& pose = THINK_POSES[i]; const float dist = hypotf( - pose.headDeg - curHead, - pose.neckDeg - curNeck + pose.headNorm - curHead, + pose.neckNorm - curNeck ); if (dist < bestDist) { secondDist = bestDist; @@ -151,7 +146,7 @@ void beginThinkAxisMove( void tickThinkAxisMoves(uint32_t now) { if (g_thinkHeadMove.active) { - servoAt(SERVO_HEAD).setPosition( + servoAt(SERVO_HEAD).setNormPosition( easedMoveValue(g_thinkHeadMove, now) ); if (easedMoveDone(g_thinkHeadMove, now)) { @@ -159,7 +154,7 @@ void tickThinkAxisMoves(uint32_t now) { } } if (g_thinkNeckMove.active) { - servoAt(SERVO_NECK).setPosition( + servoAt(SERVO_NECK).setNormPosition( easedMoveValue(g_thinkNeckMove, now) ); if (easedMoveDone(g_thinkNeckMove, now)) { @@ -168,9 +163,9 @@ void tickThinkAxisMoves(uint32_t now) { } } -uint32_t primaryDurationMs(float deltaDeg) { +uint32_t primaryDurationMs(float deltaNorm) { const uint32_t base = randRangeMs(480, 950); - const float scale = 0.65f + 0.55f * (deltaDeg / 20.0f); + const float scale = 0.65f + 0.55f * (deltaNorm / 0.57f); return (uint32_t)(base * constrain(scale, 0.65f, 1.2f)); } @@ -179,10 +174,10 @@ uint32_t microDurationMs() { } void startThinkPrimaryMove(uint32_t now, const ThinkPose& pose) { - const float headFrom = servoAt(SERVO_HEAD).angle(); - const float neckFrom = servoAt(SERVO_NECK).angle(); - const float headTo = pose.headDeg; - const float neckTo = pose.neckDeg; + const float headFrom = servoDegToNorm(SERVO_HEAD, servoAt(SERVO_HEAD).angle()); + const float neckFrom = servoDegToNorm(SERVO_NECK, servoAt(SERVO_NECK).angle()); + const float headTo = pose.headNorm; + const float neckTo = pose.neckNorm; const uint32_t headDur = primaryDurationMs(fabsf(headTo - headFrom)); @@ -217,15 +212,15 @@ void startThinkPrimaryMove(uint32_t now, const ThinkPose& pose) { g_thinkDidMicro = false; serialLogPrint("[anim] think primary head "); - serialLogPrint(headFrom, 1); + serialLogPrint(servoNormToDeg(SERVO_HEAD, headFrom), 1); serialLogPrint("->"); - serialLogPrint(headTo, 1); + serialLogPrint(servoNormToDeg(SERVO_HEAD, headTo), 1); serialLogPrint(" ("); serialLogPrint(headDur); serialLogPrint("ms) neck "); - serialLogPrint(neckFrom, 1); + serialLogPrint(servoNormToDeg(SERVO_NECK, neckFrom), 1); serialLogPrint("->"); - serialLogPrint(neckTo, 1); + serialLogPrint(servoNormToDeg(SERVO_NECK, neckTo), 1); serialLogPrint(" ("); serialLogPrint(neckDur); serialLogPrint("ms) headFirst="); @@ -250,9 +245,8 @@ void enterThinkPause(uint32_t now, bool afterMicro) { } void beginThinkMicroMove(uint32_t now) { - const float headFrom = servoAt(SERVO_HEAD).angle(); - const float neckFrom = servoAt(SERVO_NECK).angle(); - const float microDeg = 1.5f + 3.5f * randUnit(); + const float headFrom = servoDegToNorm(SERVO_HEAD, servoAt(SERVO_HEAD).angle()); + const float neckFrom = servoDegToNorm(SERVO_NECK, servoAt(SERVO_NECK).angle()); const uint32_t dur = microDurationMs(); g_thinkHeadMove.active = false; @@ -261,20 +255,22 @@ void beginThinkMicroMove(uint32_t now) { const uint8_t mode = (uint8_t)(esp_random() % 3u); if (mode == 0 || mode == 2) { const float sign = randChance(50) ? 1.0f : -1.0f; + const float micro = THINK_HEAD_MICRO + THINK_HEAD_MICRO_SPAN * randUnit(); beginThinkAxisMove( g_thinkHeadMove, headFrom, - clampThinkHead(headFrom + sign * microDeg), + clampThinkNorm(headFrom + sign * micro), now, dur ); } if (mode == 1 || mode == 2) { const float sign = randChance(50) ? 1.0f : -1.0f; + const float micro = THINK_NECK_MICRO + THINK_NECK_MICRO_SPAN * randUnit(); beginThinkAxisMove( g_thinkNeckMove, neckFrom, - clampThinkNeck(neckFrom + sign * microDeg), + clampThinkNorm(neckFrom + sign * micro), now + (mode == 2 ? randRangeMs(50, 150) : 0), dur ); diff --git a/src/animation/typing.cpp b/src/animation/typing.cpp index 9278ab1..24e43c3 100644 --- a/src/animation/typing.cpp +++ b/src/animation/typing.cpp @@ -31,23 +31,23 @@ uint32_t g_swayPauseUntilMs = 0; void commandBodyNeckSway() { const float offset = - g_bodySwayPositive ? anim::TYPING_SWAY_DEG : -anim::TYPING_SWAY_DEG; + g_bodySwayPositive ? anim::TYPING_SWAY : -anim::TYPING_SWAY; const float speedDegS = 12.0f + 18.0f * randUnit(); - servoAt(SERVO_BODY).setTarget( + servoAt(SERVO_BODY).setNormTarget( anim::TYPING_BODY_MID + offset, speedDegS ); - servoAt(SERVO_NECK).setTarget( + servoAt(SERVO_NECK).setNormTarget( anim::TYPING_NECK_MID - offset, speedDegS ); serialLogPrint("[anim] typing sway body->"); - serialLogPrint(anim::TYPING_BODY_MID + offset, 1); + serialLogPrint(servoNormToDeg(SERVO_BODY, anim::TYPING_BODY_MID + offset), 1); serialLogPrint(" neck->"); - serialLogPrint(anim::TYPING_NECK_MID - offset, 1); + serialLogPrint(servoNormToDeg(SERVO_NECK, anim::TYPING_NECK_MID - offset), 1); serialLogPrint(" speed="); serialLogPrint(speedDegS, 1); serialLogPrint(" dir="); @@ -76,13 +76,13 @@ void commandHandStroke() { if (g_typingMoveRight) { const float target = isPress ? anim::TYPING_RIGHT_LOW - : anim::TYPING_RIGHT_LOW + lift * anim::TYPING_HAND_BAND_DEG; - servoAt(SERVO_HAND_RIGHT).setTarget(target, speedDegS); + : anim::TYPING_RIGHT_LOW + lift * anim::TYPING_HAND_BAND; + servoAt(SERVO_HAND_RIGHT).setNormTarget(target, speedDegS); } else { const float target = isPress ? anim::TYPING_LEFT_HIGH - : anim::TYPING_LEFT_HIGH - lift * anim::TYPING_HAND_BAND_DEG; - servoAt(SERVO_HAND_LEFT).setTarget(target, speedDegS); + : anim::TYPING_LEFT_HIGH - lift * anim::TYPING_HAND_BAND; + servoAt(SERVO_HAND_LEFT).setNormTarget(target, speedDegS); } } @@ -90,14 +90,14 @@ void commandHead() { if (g_headHigh) { const float upSpeed = 35.0f + 25.0f * randUnit(); - servoAt(SERVO_HEAD).setTarget( + servoAt(SERVO_HEAD).setNormTarget( anim::TYPING_HEAD_HIGH, upSpeed ); } else { const float downSpeed = 6.0f + 6.0f * randUnit(); - servoAt(SERVO_HEAD).setTarget( + servoAt(SERVO_HEAD).setNormTarget( anim::TYPING_HEAD_LOW, downSpeed ); diff --git a/src/animation/util.cpp b/src/animation/util.cpp index 063a830..679dd41 100644 --- a/src/animation/util.cpp +++ b/src/animation/util.cpp @@ -130,30 +130,24 @@ void stopAnimServos() { } void parkTorso(float speedDegS) { - const float bodyMid = servoMid(SERVO_SPECS[SERVO_BODY]); - const float neckMid = servoMid(SERVO_SPECS[SERVO_NECK]); - serialLogPrint("[anim] parkTorso body "); serialLogPrint(servoAt(SERVO_BODY).angle(), 1); serialLogPrint(" -> "); - serialLogPrint(bodyMid, 1); + serialLogPrint(servoNormToDeg(SERVO_BODY, 0.0f), 1); serialLogPrint(" neck "); serialLogPrint(servoAt(SERVO_NECK).angle(), 1); serialLogPrint(" -> "); - serialLogPrint(neckMid, 1); + serialLogPrint(servoNormToDeg(SERVO_NECK, 0.0f), 1); serialLogPrint(" speed="); serialLogPrintln(speedDegS, 1); - servoAt(SERVO_BODY).setTarget(bodyMid, speedDegS); - servoAt(SERVO_NECK).setTarget(neckMid, speedDegS); + servoAt(SERVO_BODY).setNormTarget(0.0f, speedDegS); + servoAt(SERVO_NECK).setNormTarget(0.0f, speedDegS); } void parkHands(float speedDegS) { - const float handRightRest = SERVO_SPECS[SERVO_HAND_RIGHT].min; - const float handLeftRest = SERVO_SPECS[SERVO_HAND_LEFT].max; - - servoAt(SERVO_HAND_RIGHT).setTarget(handRightRest, speedDegS); - servoAt(SERVO_HAND_LEFT).setTarget(handLeftRest, speedDegS); + servoAt(SERVO_HAND_RIGHT).setNormTarget(-1.0f, speedDegS); + servoAt(SERVO_HAND_LEFT).setNormTarget(1.0f, speedDegS); } void parkForTransition() { @@ -174,23 +168,20 @@ void parkHandsAndBody() { void parkNonePose() { parkForTransition(); - servoAt(SERVO_HEAD).setTarget( - servoMid(SERVO_SPECS[SERVO_HEAD]), - TRANSITION_TORSO_SPEED_DEG_S - ); + servoAt(SERVO_HEAD).setNormTarget(0.0f, TRANSITION_TORSO_SPEED_DEG_S); } void parkSleepPose() { parkForTransition(); - servoAt(SERVO_HEAD).setTarget( + servoAt(SERVO_HEAD).setNormTarget( SLEEP_HEAD_DOWN, SERVO_BOOT_SPEED_DEG_S ); } -void snapHeadToRangeHigh(float highDeg) { - servoAt(SERVO_HEAD).setTarget( - highDeg, +void snapHeadToRangeHigh(float highNorm) { + servoAt(SERVO_HEAD).setNormTarget( + highNorm, SERVO_MAX_SPEED_DEG_S * 0.85f ); } diff --git a/src/animation/util.h b/src/animation/util.h index ef2a3fa..1cedcdb 100644 --- a/src/animation/util.h +++ b/src/animation/util.h @@ -43,7 +43,7 @@ bool isTransitionParkComplete(); void parkHandsAndBody(); void parkNonePose(); void parkSleepPose(); -void snapHeadToRangeHigh(float highDeg); +void snapHeadToRangeHigh(float highNorm); void logServoSnapshot(const char* tag); diff --git a/src/animation/wakeup.cpp b/src/animation/wakeup.cpp index 654fee0..c778f62 100644 --- a/src/animation/wakeup.cpp +++ b/src/animation/wakeup.cpp @@ -17,7 +17,7 @@ namespace { constexpr uint32_t kOpenDurationMs = 2000; constexpr uint32_t kTotalDurationMs = 5500; constexpr uint32_t kSettleMs = 400; -constexpr float kNeckWaveAmpDeg = 4.0f; +constexpr float kNeckWaveAmp = 4.0f / 45.0f; constexpr float kWavePeriodMs = 2800.0f; constexpr float kServoSpeedDegS = SERVO_BOOT_SPEED_DEG_S; constexpr float kTwoPi = 6.28318530718f; @@ -33,24 +33,20 @@ float headMid() { } float neckMid() { - return servoMid(SERVO_SPECS[SERVO_NECK]); + return 0.0f; } -void applyHeadNeck(float headDeg, float neckDeg) { - servoAt(SERVO_HEAD).setTarget(headDeg, kServoSpeedDegS); - servoAt(SERVO_NECK).setTarget(neckDeg, kServoSpeedDegS); +void applyHeadNeck(float headNorm, float neckNorm) { + servoAt(SERVO_HEAD).setNormTarget(headNorm, kServoSpeedDegS); + servoAt(SERVO_NECK).setNormTarget(neckNorm, kServoSpeedDegS); } void snapSleepPose() { - const float mid = neckMid(); - const float handRightDown = SERVO_SPECS[SERVO_HAND_RIGHT].min; - const float handLeftDown = SERVO_SPECS[SERVO_HAND_LEFT].max; - - servoAt(SERVO_HEAD).setPosition(anim::SLEEP_HEAD_DOWN); - servoAt(SERVO_NECK).setPosition(mid); - servoAt(SERVO_HAND_RIGHT).setPosition(handRightDown); - servoAt(SERVO_HAND_LEFT).setPosition(handLeftDown); - servoAt(SERVO_BODY).setPosition(servoMid(SERVO_SPECS[SERVO_BODY])); + servoAt(SERVO_HEAD).setNormPosition(anim::SLEEP_HEAD_DOWN); + servoAt(SERVO_NECK).setNormPosition(0.0f); + servoAt(SERVO_HAND_RIGHT).setNormPosition(-1.0f); + servoAt(SERVO_HAND_LEFT).setNormPosition(1.0f); + servoAt(SERVO_BODY).setNormPosition(0.0f); } void markFinished(uint32_t now) { @@ -123,7 +119,7 @@ void updateWakeup(uint32_t nowMs) { static_cast(elapsed) / static_cast(kTotalDurationMs) ); - const float headDeg = anim::lerp( + const float headNorm = anim::lerp( anim::SLEEP_HEAD_DOWN, headMid(), riseT @@ -132,10 +128,10 @@ void updateWakeup(uint32_t nowMs) { static_cast(elapsed) / kWavePeriodMs; const float wave = sinf(waveT * kTwoPi); const float waveFade = 1.0f - riseT; - const float neckDeg = - neckMid() + wave * kNeckWaveAmpDeg * waveFade; + const float neckNorm = + neckMid() + wave * kNeckWaveAmp * waveFade; - applyHeadNeck(headDeg, neckDeg); + applyHeadNeck(headNorm, neckNorm); updateAllServos(); } } diff --git a/src/animation/welcome.cpp b/src/animation/welcome.cpp index fc84e75..906b177 100644 --- a/src/animation/welcome.cpp +++ b/src/animation/welcome.cpp @@ -26,7 +26,7 @@ bool handRaiseComplete(uint32_t elapsed) { void parkWelcomeIdlePose() { anim::parkForTransition(); - servoAt(SERVO_HEAD).setTarget( + servoAt(SERVO_HEAD).setNormTarget( anim::WELCOME_HEAD_MID, anim::TRANSITION_TORSO_SPEED_DEG_S ); @@ -48,8 +48,8 @@ void applyRaisePose(uint32_t now) { now ); - servoAt(SERVO_HAND_RIGHT).setPosition(handAngle); - servoAt(SERVO_HEAD).setPosition(headAngle); + servoAt(SERVO_HAND_RIGHT).setNormPosition(handAngle); + servoAt(SERVO_HEAD).setNormPosition(headAngle); } void applyAudioPose(uint32_t audioElapsed, uint32_t now) { @@ -88,18 +88,18 @@ void applyAudioPose(uint32_t audioElapsed, uint32_t now) { const float wiggle = sinf( questionElapsed / questionDuration * 2.0f * PI * 2.0f - ) * anim::WELCOME_HAND_WIGGLE_DEG; + ) * anim::WELCOME_HAND_WIGGLE; handAngle = anim::WELCOME_HAND_RAISED + wiggle; const float nod = sinf( questionElapsed / questionDuration * 2.0f * PI - ) * 3.0f; + ) * anim::WELCOME_HEAD_NOD; headAngle = anim::WELCOME_HEAD_UP + nod; } - servoAt(SERVO_HAND_RIGHT).setPosition(handAngle); - servoAt(SERVO_HEAD).setPosition(headAngle); + servoAt(SERVO_HAND_RIGHT).setNormPosition(handAngle); + servoAt(SERVO_HEAD).setNormPosition(headAngle); } void updateWelcomeParkServos() { diff --git a/src/boot/boot_loading.cpp b/src/boot/boot_loading.cpp index 6b47574..be011f4 100644 --- a/src/boot/boot_loading.cpp +++ b/src/boot/boot_loading.cpp @@ -19,10 +19,6 @@ namespace { constexpr uint32_t kFrameMs = 16; -float neckMid() { - return servoMid(SERVO_SPECS[SERVO_NECK]); -} - } // namespace bool bootLoadingIsProgress() { @@ -55,20 +51,16 @@ bool bootSleepInertiaUsesServos() { } void bootSnapSleepPose() { - const float mid = neckMid(); - const float handRightDown = SERVO_SPECS[SERVO_HAND_RIGHT].min; - const float handLeftDown = SERVO_SPECS[SERVO_HAND_LEFT].max; - float targets[SERVO_COUNT]; for (int servo = 0; servo < SERVO_COUNT; servo++) { - targets[servo] = servoMid(SERVO_SPECS[servo]); + targets[servo] = servoNormToDeg(servo, 0.0f); } - targets[SERVO_HEAD] = anim::SLEEP_HEAD_DOWN; - targets[SERVO_NECK] = mid; - targets[SERVO_HAND_RIGHT] = handRightDown; - targets[SERVO_HAND_LEFT] = handLeftDown; + targets[SERVO_HEAD] = servoNormToDeg(SERVO_HEAD, anim::SLEEP_HEAD_DOWN); + targets[SERVO_NECK] = servoNormToDeg(SERVO_NECK, 0.0f); + targets[SERVO_HAND_RIGHT] = servoNormToDeg(SERVO_HAND_RIGHT, -1.0f); + targets[SERVO_HAND_LEFT] = servoNormToDeg(SERVO_HAND_LEFT, 1.0f); servoMoveAllSmoothTo(targets); } diff --git a/src/display/eyes/modes/reading.cpp b/src/display/eyes/modes/reading.cpp index 5282b8a..5db3c42 100644 --- a/src/display/eyes/modes/reading.cpp +++ b/src/display/eyes/modes/reading.cpp @@ -31,15 +31,15 @@ float clampUnit(float value) { } int16_t gazeXFromNeck() { - const float neck = servoAt(SERVO_NECK).angle(); + const float neck = servoDegToNorm(SERVO_NECK, servoAt(SERVO_NECK).angle()); const float norm = clampUnit( - (neck - anim::READING_NECK_MID) / anim::READING_NECK_SWAY_DEG + (neck - anim::READING_NECK_MID) / anim::READING_NECK_SWAY ); return (int16_t)(norm * READ_NECK_GAZE_PX); } int16_t gazeYFromHead() { - const float head = servoAt(SERVO_HEAD).angle(); + const float head = servoDegToNorm(SERVO_HEAD, servoAt(SERVO_HEAD).angle()); const float span = anim::READING_HEAD_HIGH - anim::READING_HEAD_LOW; if (span <= 0.0f) { return 2; diff --git a/src/display/oled.cpp b/src/display/oled.cpp index f91b144..d113e51 100644 --- a/src/display/oled.cpp +++ b/src/display/oled.cpp @@ -8,6 +8,7 @@ #include "display/oled.h" #include "display/oled_internal.h" #include "network/wifi_connect.h" +#include "settings/settings.h" Adafruit_SSD1306 display( OLED_WIDTH, @@ -111,3 +112,31 @@ void wakeOled() { display.ssd1306_command(SSD1306_DISPLAYON); } + +void applyOledRotation() { + if (!oledAvailable) { + return; + } + + display.setRotation(settingsOledRotate180() ? 2 : 0); +} + +void showOledOrientationTest(bool rotate180) { + if (!oledAvailable) { + return; + } + + stopEyes(); + wakeOled(); + display.setRotation(rotate180 ? 2 : 0); + showOledText("THIS WAY UP"); +} + +void restoreProvisioningOled() { + if (!wifiProvisioningMode()) { + return; + } + + applyOledRotation(); + showProvisioningOled("Join this WiFi", wifiApSsid()); +} diff --git a/src/display/oled.h b/src/display/oled.h index 2975dcf..628170a 100644 --- a/src/display/oled.h +++ b/src/display/oled.h @@ -42,3 +42,6 @@ void showServoTestFinished(); void blankOled(); void sleepOled(); void wakeOled(); +void applyOledRotation(); +void showOledOrientationTest(bool rotate180); +void restoreProvisioningOled(); diff --git a/src/display/oled_init.cpp b/src/display/oled_init.cpp index 05bae8f..3901a4b 100644 --- a/src/display/oled_init.cpp +++ b/src/display/oled_init.cpp @@ -6,6 +6,7 @@ #include "display/oled.h" #include "display/oled_internal.h" #include "serial_log.h" +#include "settings/settings.h" bool i2cDeviceConnected(uint8_t address) { Wire.beginTransmission(address); @@ -33,8 +34,7 @@ void initOled() { return; } - // Rotate OLED by 180 degrees - display.setRotation(2); + display.setRotation(settingsOledRotate180() ? 2 : 0); display.clearDisplay(); display.display(); diff --git a/src/hardware/pca9685_servos.cpp b/src/hardware/pca9685_servos.cpp index 147e9e8..fdcaaf2 100644 --- a/src/hardware/pca9685_servos.cpp +++ b/src/hardware/pca9685_servos.cpp @@ -65,13 +65,22 @@ void centerAllServos() { float targets[SERVO_COUNT]; for (int servo = 0; servo < SERVO_COUNT; servo++) { - targets[servo] = - (settingsServoMin(servo) + settingsServoMax(servo)) * 0.5f; + targets[servo] = servoNormToDeg(servo, 0.0f); } servoMoveAllSmoothTo(targets); } +void servoMoveAllNorm(float n) { + float targets[SERVO_COUNT]; + + for (int i = 0; i < SERVO_COUNT; i++) { + targets[i] = servoNormToDeg(i, n); + } + + servoMoveAllSmoothTo(targets, SERVO_MAX_SPEED_DEG_S); +} + void runServoTest() { constexpr int TOTAL_STEPS = 4; @@ -80,63 +89,47 @@ void runServoTest() { serialLogPrintln("SERVO TEST - HEAD/NECK/HANDS/BODY"); serialLogPrintln("=========================="); - serialLogPrintln( - "1. All servos -> 90 deg" - ); + serialLogPrintln("1. All servos -> mid"); showServoProgress( 1, TOTAL_STEPS, - "ALL -> 90" + "ALL -> mid" ); - servoMoveAllSmooth( - SERVO_CENTER - ); + servoMoveAllNorm(0.0f); delay(1000); - serialLogPrintln( - "2. All servos: 90 -> 105 deg" - ); + serialLogPrintln("2. All servos: mid -> +0.5"); showServoProgress( 2, TOTAL_STEPS, - "90 -> 105" + "mid -> +0.5" ); - servoMoveAllSmooth( - SERVO_HIGH - ); + servoMoveAllNorm(0.5f); - serialLogPrintln( - "3. All servos: 105 -> 75 deg" - ); + serialLogPrintln("3. All servos: +0.5 -> -0.5"); showServoProgress( 3, TOTAL_STEPS, - "105 -> 75" + "+0.5 -> -0.5" ); - servoMoveAllSmooth( - SERVO_LOW - ); + servoMoveAllNorm(-0.5f); - serialLogPrintln( - "4. All servos: 75 -> 90 deg" - ); + serialLogPrintln("4. All servos: -0.5 -> mid"); showServoProgress( 4, TOTAL_STEPS, - "75 -> 90" + "-0.5 -> mid" ); - servoMoveAllSmooth( - SERVO_CENTER - ); + servoMoveAllNorm(0.0f); showServoTestFinished(); diff --git a/src/hardware/servo_wrapper.cpp b/src/hardware/servo_wrapper.cpp index bc06c40..8941eb2 100644 --- a/src/hardware/servo_wrapper.cpp +++ b/src/hardware/servo_wrapper.cpp @@ -57,7 +57,31 @@ float clampAngle(int index, float angle, bool electrical) { } float servoRuntimeMid(int index) { - return (settingsServoMin(index) + settingsServoMax(index)) * 0.5f; + return servoNormToDeg(index, 0.0f); +} + +float servoNormToDeg(int index, float n) { + if (index < 0 || index >= SERVO_COUNT) { + return servoNormToDeg(n, 0.0f, 180.0f); + } + + return servoNormToDeg( + n, + settingsServoMin(index), + settingsServoMax(index) + ); +} + +float servoDegToNorm(int index, float deg) { + if (index < 0 || index >= SERVO_COUNT) { + return servoDegToNorm(deg, 0.0f, 180.0f); + } + + return servoDegToNorm( + deg, + settingsServoMin(index), + settingsServoMax(index) + ); } void initServoOutputPin() { @@ -132,6 +156,14 @@ void ServoWrapper::setPosition(float angle) { lastUpdateMs_ = 0; } +void ServoWrapper::setNormTarget(float n, float speedDegS) { + setTarget(servoNormToDeg(index_, n), speedDegS); +} + +void ServoWrapper::setNormPosition(float n) { + setPosition(servoNormToDeg(index_, n)); +} + void ServoWrapper::stop() { target_ = angle_; } @@ -462,13 +494,3 @@ void servoMoveAllToElectrical( g_servos[i].lastUpdateMs_ = 0; } } - -void servoMoveAllSmooth(float toAngle) { - float targets[SERVO_COUNT]; - - for (int i = 0; i < SERVO_COUNT; i++) { - targets[i] = toAngle; - } - - servoMoveAllSmoothTo(targets, SERVO_MAX_SPEED_DEG_S); -} diff --git a/src/hardware/servo_wrapper.h b/src/hardware/servo_wrapper.h index 52afb28..52200fb 100644 --- a/src/hardware/servo_wrapper.h +++ b/src/hardware/servo_wrapper.h @@ -19,11 +19,15 @@ class ServoWrapper { float speedDegS = SERVO_MAX_SPEED_DEG_S ); void setPosition(float angle); + void setNormTarget( + float n, + float speedDegS = SERVO_MAX_SPEED_DEG_S + ); + void setNormPosition(float n); void stop(); void update(); bool isMoving() const; - friend void servoMoveAllSmooth(float toAngle); friend void servoMoveAllSmoothTo( const float targets[SERVO_COUNT], float speedDegS @@ -49,7 +53,6 @@ void initServoOutputPin(); void disableServoOutputs(); void enableServoOutputs(); void initServoPwmDriver(); -void servoMoveAllSmooth(float toAngle); void servoMoveAllSmoothTo( const float targets[SERVO_COUNT], float speedDegS = SERVO_BOOT_SPEED_DEG_S @@ -59,3 +62,8 @@ void servoMoveAllToElectrical( float speedDegS = SERVO_CALIB_SPEED_DEG_S ); void updateAllServos(); +float clampElectricalAngle(float angle); +float clampServoAngle(int index, float angle); +float servoRuntimeMid(int index); +float servoNormToDeg(int index, float n); +float servoDegToNorm(int index, float deg); diff --git a/src/http/index_page.cpp b/src/http/index_page.cpp index 988b7d9..680e628 100644 --- a/src/http/index_page.cpp +++ b/src/http/index_page.cpp @@ -188,8 +188,8 @@ body:not(.setup-mode) #setup-wizard{display:none!important}

Factory reset done

-

Settings and WiFi credentials are cleared. Servo ranges and RGB LED mapping stay. Power-cycle or press the device reset button.

-

Then join the robot WiFi shown on the OLED and open the setup page to configure home WiFi. You can retune servo ranges and LED mapping there.

+

Settings and WiFi credentials are cleared. Servo ranges, RGB LED mapping, and screen rotation stay. Power-cycle or press the device reset button.

+

Then join the robot WiFi shown on the OLED and open the setup page to configure home WiFi. You can retune servo ranges, screen rotation, and LED mapping there.