First commit

This commit is contained in:
2025-08-20 17:53:34 +02:00
commit cd6daae17e
14 changed files with 6289 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
# Gitignore settings for ESPHome
/.esphome/
/secrets.yaml

57
README.md Normal file
View File

@@ -0,0 +1,57 @@
# Luna Hexagon
Luna hexagon is an electronic game I built for my daughter.
It is based on [ESPHome](https://esphome.io/).
## How it works (user's perspective)
### Baby interaction
She's 8 months old, so the interaction should be very simple. She mostly grabs and pulls objects, so there's a cord knotted to a limit switch's lever. When she pulls the cord, one of the sectons in the hexagon lights up and the corresponding tune is played.
### Parents interaction
It doubles as a night light: in this case the hexagon sections are not individually controllable, they all light up the same, but it's still RGB and dimmable.
It can be controlled via Home Assistant (it appears as a standard dimmable RGB light) or via a web browser (it exposes a simple webpage with the light controls).
It works on batteries, they last from 4 to 8 hours, depending on the usage.
## Worklog
See the complete worklog here: (TODO: Link to the Fediverse)
## How to build one
### BOM
- ESP8266
- An 8 Ohm speaker
- Neopixel compatible 5v led strip
- Limit switch
- Battery holder
- Power switch
- A nice case (mine was sold as a tea box)
### Flash the firmware
The file containing the firmware definition is `hexagon.yaml`. That's the file you'll flash to the ESP8266.
First of all, you need to adapt the configuration for your wifi connection. I've placed a couple of example in the yaml file, one with DHCP (for use with Home Assistant) and one with static IP (for standalone use, needed to access the web interface in case you don't own an home assistant instance). Change them to match your configuration. The Wifi SSID and password are stored in a separate file called `secrets.yaml`. I provided an example file, `secrets.yaml.example`, just rename it and replace the SSID and password.
To flash the firmware you have 3 options. From simplest to hardest:
- If you can use Chrome/Chromium web browser, you can use [ESPHome Web flasher](https://web.esphome.io/)
- If you have Home Assistant, you can use the [ESPHome Device Builder](https://esphome.io/guides/getting_started_hassio.html)
- You can also use the [ESPHome CLI](https://esphome.io/guides/getting_started_command_line)
In any case, the file you have to use is `hexagon.yaml`.
### Build the game
Find a suitable enclosure, install the switches, speaker, battery holder and ESP8266. Cut the neopixel-compatible led strip in six chunks of two leds, stick it to the 6 sections of the box and solder it according to the electric scheme. Test it all and decorate as you prefer.
You will find useful:
- the [Electric scheme](hexagon-schematics.pdf) (image, or if you prefer you can open the KiCad project)
- the worklog (TODO: Link to the Fediverse) to see how I built mine
## Contributing
Want to contribute? Ask me an account writing an email to daniele.verducci@ichibi.eu

BIN
hexagon-schematics.pdf Normal file

Binary file not shown.

View File

@@ -0,0 +1,2 @@
(kicad_pcb (version 20241229) (generator "pcbnew") (generator_version "9.0")
)

View File

@@ -0,0 +1,97 @@
{
"board": {
"active_layer": 0,
"active_layer_preset": "",
"auto_track_width": true,
"hidden_netclasses": [],
"hidden_nets": [],
"high_contrast_mode": 0,
"net_color_mode": 1,
"opacity": {
"images": 0.6,
"pads": 1.0,
"shapes": 1.0,
"tracks": 1.0,
"vias": 1.0,
"zones": 0.6
},
"selection_filter": {
"dimensions": true,
"footprints": true,
"graphics": true,
"keepouts": true,
"lockedItems": false,
"otherItems": true,
"pads": true,
"text": true,
"tracks": true,
"vias": true,
"zones": true
},
"visible_items": [
"vias",
"footprint_text",
"footprint_anchors",
"ratsnest",
"grid",
"footprints_front",
"footprints_back",
"footprint_values",
"footprint_references",
"tracks",
"drc_errors",
"drawing_sheet",
"bitmaps",
"pads",
"zones",
"drc_warnings",
"locked_item_shadows",
"conflict_shadows",
"shapes"
],
"visible_layers": "ffffffff_ffffffff_ffffffff_ffffffff",
"zone_display_mode": 0
},
"git": {
"repo_type": "",
"repo_username": "",
"ssh_key": ""
},
"meta": {
"filename": "hexagon-schematics.kicad_prl",
"version": 5
},
"net_inspector_panel": {
"col_hidden": [],
"col_order": [],
"col_widths": [],
"custom_group_rules": [],
"expanded_rows": [],
"filter_by_net_name": true,
"filter_by_netclass": true,
"filter_text": "",
"group_by_constraint": false,
"group_by_netclass": false,
"show_unconnected_nets": false,
"show_zero_pad_nets": false,
"sort_ascending": true,
"sorting_column": -1
},
"open_jobsets": [],
"project": {
"files": []
},
"schematic": {
"selection_filter": {
"graphics": true,
"images": true,
"labels": true,
"lockedItems": false,
"otherItems": true,
"pins": true,
"symbols": true,
"text": true,
"wires": true
}
}
}

View File

@@ -0,0 +1,417 @@
{
"board": {
"3dviewports": [],
"design_settings": {
"defaults": {},
"diff_pair_dimensions": [],
"drc_exclusions": [],
"rules": {},
"track_widths": [],
"via_dimensions": []
},
"ipc2581": {
"dist": "",
"distpn": "",
"internal_id": "",
"mfg": "",
"mpn": ""
},
"layer_pairs": [],
"layer_presets": [],
"viewports": []
},
"boards": [],
"cvpcb": {
"equivalence_files": []
},
"erc": {
"erc_exclusions": [],
"meta": {
"version": 0
},
"pin_map": [
[
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
2,
0,
1,
0,
0,
1,
0,
2,
2,
2,
2
],
[
0,
0,
0,
0,
0,
0,
1,
0,
1,
0,
1,
2
],
[
0,
1,
0,
0,
0,
0,
1,
1,
2,
1,
1,
2
],
[
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
2
],
[
1,
1,
1,
1,
1,
0,
1,
1,
1,
1,
1,
2
],
[
0,
0,
0,
1,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
2,
1,
2,
0,
0,
1,
0,
2,
2,
2,
2
],
[
0,
2,
0,
1,
0,
0,
1,
0,
2,
0,
0,
2
],
[
0,
2,
1,
1,
0,
0,
1,
0,
2,
0,
0,
2
],
[
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2
]
],
"rule_severities": {
"bus_definition_conflict": "error",
"bus_entry_needed": "error",
"bus_to_bus_conflict": "error",
"bus_to_net_conflict": "error",
"different_unit_footprint": "error",
"different_unit_net": "error",
"duplicate_reference": "error",
"duplicate_sheet_names": "error",
"endpoint_off_grid": "warning",
"extra_units": "error",
"footprint_filter": "ignore",
"footprint_link_issues": "warning",
"four_way_junction": "ignore",
"global_label_dangling": "warning",
"hier_label_mismatch": "error",
"label_dangling": "error",
"label_multiple_wires": "warning",
"lib_symbol_issues": "warning",
"lib_symbol_mismatch": "warning",
"missing_bidi_pin": "warning",
"missing_input_pin": "warning",
"missing_power_pin": "error",
"missing_unit": "warning",
"multiple_net_names": "warning",
"net_not_bus_member": "warning",
"no_connect_connected": "warning",
"no_connect_dangling": "warning",
"pin_not_connected": "error",
"pin_not_driven": "error",
"pin_to_pin": "warning",
"power_pin_not_driven": "error",
"same_local_global_label": "warning",
"similar_label_and_power": "warning",
"similar_labels": "warning",
"similar_power": "warning",
"simulation_model_issue": "ignore",
"single_global_label": "ignore",
"unannotated": "error",
"unconnected_wire_endpoint": "warning",
"unit_value_mismatch": "error",
"unresolved_variable": "error",
"wire_dangling": "error"
}
},
"libraries": {
"pinned_footprint_libs": [],
"pinned_symbol_libs": []
},
"meta": {
"filename": "hexagon-schematics.kicad_pro",
"version": 3
},
"net_settings": {
"classes": [
{
"bus_width": 12,
"clearance": 0.2,
"diff_pair_gap": 0.25,
"diff_pair_via_gap": 0.25,
"diff_pair_width": 0.2,
"line_style": 0,
"microvia_diameter": 0.3,
"microvia_drill": 0.1,
"name": "Default",
"pcb_color": "rgba(0, 0, 0, 0.000)",
"priority": 2147483647,
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 0.2,
"via_diameter": 0.6,
"via_drill": 0.3,
"wire_width": 6
}
],
"meta": {
"version": 4
},
"net_colors": null,
"netclass_assignments": null,
"netclass_patterns": []
},
"pcbnew": {
"last_paths": {
"gencad": "",
"idf": "",
"netlist": "",
"plot": "",
"pos_files": "",
"specctra_dsn": "",
"step": "",
"svg": "",
"vrml": ""
},
"page_layout_descr_file": ""
},
"schematic": {
"annotate_start_num": 0,
"bom_export_filename": "${PROJECTNAME}.csv",
"bom_fmt_presets": [],
"bom_fmt_settings": {
"field_delimiter": ",",
"keep_line_breaks": false,
"keep_tabs": false,
"name": "CSV",
"ref_delimiter": ",",
"ref_range_delimiter": "",
"string_delimiter": "\""
},
"bom_presets": [],
"bom_settings": {
"exclude_dnp": false,
"fields_ordered": [
{
"group_by": false,
"label": "Reference",
"name": "Reference",
"show": true
},
{
"group_by": false,
"label": "Qty",
"name": "${QUANTITY}",
"show": true
},
{
"group_by": true,
"label": "Value",
"name": "Value",
"show": true
},
{
"group_by": true,
"label": "DNP",
"name": "${DNP}",
"show": true
},
{
"group_by": true,
"label": "Exclude from BOM",
"name": "${EXCLUDE_FROM_BOM}",
"show": true
},
{
"group_by": true,
"label": "Exclude from Board",
"name": "${EXCLUDE_FROM_BOARD}",
"show": true
},
{
"group_by": true,
"label": "Footprint",
"name": "Footprint",
"show": true
},
{
"group_by": false,
"label": "Datasheet",
"name": "Datasheet",
"show": true
}
],
"filter_string": "",
"group_symbols": true,
"include_excluded_from_bom": true,
"name": "Default Editing",
"sort_asc": true,
"sort_field": "Riferimento"
},
"connection_grid_size": 50.0,
"drawing": {
"dashed_lines_dash_length_ratio": 12.0,
"dashed_lines_gap_length_ratio": 3.0,
"default_line_thickness": 6.0,
"default_text_size": 50.0,
"field_names": [],
"intersheets_ref_own_page": false,
"intersheets_ref_prefix": "",
"intersheets_ref_short": false,
"intersheets_ref_show": false,
"intersheets_ref_suffix": "",
"junction_size_choice": 3,
"label_size_ratio": 0.375,
"operating_point_overlay_i_precision": 3,
"operating_point_overlay_i_range": "~A",
"operating_point_overlay_v_precision": 3,
"operating_point_overlay_v_range": "~V",
"overbar_offset_ratio": 1.23,
"pin_symbol_size": 25.0,
"text_offset_ratio": 0.15
},
"legacy_lib_dir": "",
"legacy_lib_list": [],
"meta": {
"version": 1
},
"net_format_name": "",
"page_layout_descr_file": "",
"plot_directory": "",
"space_save_all_events": true,
"spice_current_sheet_as_root": false,
"spice_external_command": "spice \"%I\"",
"spice_model_current_sheet_as_root": true,
"spice_save_all_currents": false,
"spice_save_all_dissipations": false,
"spice_save_all_voltages": false,
"subpart_first_id": 65,
"subpart_id_separator": 0
},
"sheets": [
[
"06b6e953-64d5-4585-a47a-1bc53a5729bc",
"Root"
]
],
"text_variables": {}
}

File diff suppressed because it is too large Load Diff

246
hexagon.yaml Normal file
View File

@@ -0,0 +1,246 @@
# ESP8266 connections:
# D8 -> speaker +
# RX -> neopixel leds data pin
# D0 -> switch
# A0 -> voltage divider between VCC and ground (VCC->10kΩ->A0->10kΩ->GND)
# Please see the complete elecric scheme
esphome:
name: luna-hexagon
on_boot:
- priority: 600
# Initial status initialization
then:
- light.turn_on: hexagon_sections
- priority: -100
# When all is set up. Start the show.
then:
- script.execute: game_ready_animation
# Variables
globals:
- id: game_selected_section
type: int
restore_value: no
initial_value: '0'
web_server:
port: 80
version: 3
log: False
ota: False
esp8266:
board: d1_mini_lite
# Enable logging (for development)
# logger:
api:
reboot_timeout: 0s
ota:
- platform: esphome
password: ""
wifi:
reboot_timeout: 0s
networks:
# Normal connection, for use with home assistant
- ssid: !secret wifi_ssid_1
password: !secret wifi_password_1
# Static IP connection, to use with browser:
# in the following example, head to http://192.168.0.42 to see the control interface.
# Modify according to your home wifi configuration.
- ssid: !secret wifi_ssid_2
password: !secret wifi_password_2
manual_ip:
static_ip: 192.168.0.42
gateway: 192.168.0.1
subnet: 255.255.255.0
button:
- platform: template
name: Color wheel
id: color_wheel_button
icon: "mdi:hexagon-slice-6"
on_press:
- script.execute: game_ready_animation
binary_sensor:
# Buttons (connect to a button that shorts to 3v3)
- platform: gpio
pin:
number: D0
mode:
input: true
pulldown: true
id: "pull_cord"
internal: True
# Debouncing
filters:
- settle: 10ms
- autorepeat:
time_off: 100ms
time_on: 100ms
on_press:
then:
- script.execute: game_advance_section
on_release:
then:
- script.execute: game_play_section
sensor:
# Battery level sensor: 560 is 4.0v (battery flat), 740 is 5,2v (battery full)
- platform: adc
pin: A0
name: "Battery level"
icon: 'mdi:battery'
update_interval: 1min
unit_of_measurement: "%"
raw: true
filters:
- clamp:
min_value: 560.0
max_value: 740.0
- calibrate_linear:
- 560.0 -> 0.0
- 740.0 -> 100.0
- min:
send_first_at: 3
output:
- platform: esp8266_pwm
pin: D8
id: speaker
light:
- platform: neopixelbus
variant: WS2812
method:
type: esp8266_dma
pin: GPIO3 # "RX" pin: see https://github.com/Makuna/NeoPixelBus/wiki/ESP8266-NeoMethods#dma-i2s on why this does not break serial logging
num_leds: 12
name: "Hexagon Sections"
id: hexagon_sections
# Component to play Ring Tone Text Transfer Language
# (Nokia ringtone format) on a digital pin
rtttl:
output: speaker
on_finished_playback:
# - script.execute: game_ready_animation
- script.execute:
id: set_section_color
section: !lambda return id(game_selected_section);
r: 1.0
g: 1.0
b: 1.0
# ---- Scripts ----
script:
# Set section color. Section is 0-5
- id: set_section_color
parameters:
section: int
r: float
g: float
b: float
then:
- light.addressable_set:
id: hexagon_sections
range_from: !lambda return section * 2;
range_to: !lambda return section * 2 + 1;
red: !lambda return r;
green: !lambda return g;
blue: !lambda return b;
#transition_length: 1s;
- id: game_ready_animation
then:
- script.execute:
id: set_section_color
section: 0
r: 1.0
g: 0.0
b: 0.0
- delay: 200ms
- script.execute:
id: set_section_color
section: 1
r: 0.0
g: 1.0
b: 0.0
- delay: 200ms
- script.execute:
id: set_section_color
section: 2
r: 0.0
g: 0.0
b: 1.0
- delay: 200ms
- script.execute:
id: set_section_color
section: 3
r: 1.0
g: 1.0
b: 0.0
- delay: 200ms
- script.execute:
id: set_section_color
section: 4
r: 1.0
g: 0.0
b: 1.0
- delay: 200ms
- script.execute:
id: set_section_color
section: 5
r: 0.0
g: 1.0
b: 1.0
- delay: 200ms
- id: game_advance_section
then:
# Stop playback
- rtttl.stop
# Turn off all sections
- repeat:
count: 6
then:
lambda: |-
id(set_section_color).execute(iteration, 0, 0, 0);
# Increment section counter
- lambda: if (id(game_selected_section) >= 5) id(game_selected_section) = 0; else id(game_selected_section) += 1;
# Turn on corresponding section
- script.execute:
id: set_section_color
section: !lambda return id(game_selected_section);
r: !lambda if (id(game_selected_section) < 3) return 0.0; else return 1.0;
g: !lambda if (id(game_selected_section) == 0 || id(game_selected_section) == 3 || id(game_selected_section) == 4) return 0.0; else return 1.0;
b: !lambda if (id(game_selected_section) % 2 == 0) return 1.0; else return 0.0;
- logger.log:
format: "Selected section %d"
args: [ 'id(game_selected_section)' ]
- id: game_play_section
then:
- rtttl.play:
rtttl: !lambda |-
switch(id(game_selected_section)) {
case 0:
return "BabyElep:d=8,o=6,b=125:f.,16a5,c,f,a,g,f,d,b5,c,2p.,f.,16a5,c,f,a,g,f,d,2c.,d,1g#5,d,g#5,f5";
case 1:
return "Moonligt:d=4,o=6,b=160:f.,16p,8f,8f,8g,8f,8e,d.,8p,d.,8p,8e,8e,8f,g.,32p,8f,8g,a,a,8g,8c.,16p,f.,16p,8f,8f,8g,8f,8e,d.,8p,8p,8e,8e,8f,g.,32p,8f,8g,a,a,8g,8c.";
case 2:
return "Smurfs:d=32,o=5,b=200:4c#6,16p,4f#6,p,16c#6,p,8d#6,p,8b,p,4g#,16p,4c#6,p,16a#,p,8f#,p,8a#,p,4g#,4p,g#,p,a#,p,b,p,c6,p,4c#6,16p,4f#6,p,16c#6,p,8d#6,p,8b,p,4g#,16p,4c#6,p,16a#,p,8b,p,8f,p,4f#";
case 3:
return "SecretOf:d=4,o=5,b=180:8e6,8p,8e6,8g6,8f#6,8e6,d6,2e6,8p,8d6,8p,8d6,8c6,8b,8d6,8c6,8p,8c6,8p,b,p,8e6,8p,e.6,8g6,8f#6,8e6,d6,2e6,8p,8f#6,8g6,8p,8g6,8p,a6,p,f#6,8p,8g6,8f#6,8e6,8d6,8f#6,8g6,8p,8g6,8p,f#6";
case 4:
return "PinkPanther:d=4,o=5,b=160:8d#,8e,2p,8f#,8g,2p,8d#,8e,16p,8f#,8g,16p,8c6,8b,16p,8d#,8e,16p,8b,2a#,2p,16a,16g,16e,16d,2e";
case 5:
default:
return "mario:d=4,o=5,b=100:16e6,16e6,32p,8e6,16c6,8e6,8g6,8p,8g,8p,8c6,16p,8g,16p,8e,16p,8a,8b,16a#,8a,16g.,16e6,16g6,8a6,16f6,8g6,8e6,16c6,16d6,8b,16p,8c6,16p,8g,16p,8e,16p,8a,8b,16a#,8a,16g.,16e6,16g6,8a6,16f6,8g6,8e6,16c6,16d6,8b,8p,16g6,16f#6,16f6,16d#6,16p,16e6,16p,16g#,16a,16c6,16p,16a,16c6,16d6,8p,16g6,16f#6,16f6,16d#6,16p,16e6,16p,16c7,16p,16c7,16c7,p,16g6,16f#6,16f6,16d#6,16p,16e6,16p,16g#,16a,16c6,16p,16a,16c6,16d6,8p,16d#6,8p,16d6,8p,16c6";
}

4
secrets.yaml.example Normal file
View File

@@ -0,0 +1,4 @@
wifi_ssid_1: "My Wifi Name"
wifi_password_1: "MyWifiPassword!"
wifi_ssid_2: "My Wifi Name"
wifi_password_2: "MyWifiPassword!"