element14 Community
element14 Community
    Register Log In
  • Site
  • Search
  • Log In Register
  • About Us
  • Community Hub
    Community Hub
    • What's New on element14
    • Feedback and Support
    • Benefits of Membership
    • Personal Blogs
    • Members Area
    • Achievement Levels
  • Learn
    Learn
    • Ask an Expert
    • eBooks
    • element14 presents
    • Learning Center
    • Tech Spotlight
    • STEM Academy
    • Webinars, Training and Events
    • Learning Groups
  • Technologies
    Technologies
    • 3D Printing
    • FPGA
    • Industrial Automation
    • Internet of Things
    • Power & Energy
    • Sensors
    • Technology Groups
  • Challenges & Projects
    Challenges & Projects
    • Design Challenges
    • element14 presents Projects
    • Project14
    • Arduino Projects
    • Raspberry Pi Projects
    • Project Groups
  • Products
    Products
    • Arduino
    • Avnet Boards Community
    • Dev Tools
    • Manufacturers
    • Multicomp Pro
    • Product Groups
    • Raspberry Pi
    • RoadTests & Reviews
  • Store
    Store
    • Visit Your Store
    • Choose another store...
      • Europe
      •  Austria (German)
      •  Belgium (Dutch, French)
      •  Bulgaria (Bulgarian)
      •  Czech Republic (Czech)
      •  Denmark (Danish)
      •  Estonia (Estonian)
      •  Finland (Finnish)
      •  France (French)
      •  Germany (German)
      •  Hungary (Hungarian)
      •  Ireland
      •  Israel
      •  Italy (Italian)
      •  Latvia (Latvian)
      •  
      •  Lithuania (Lithuanian)
      •  Netherlands (Dutch)
      •  Norway (Norwegian)
      •  Poland (Polish)
      •  Portugal (Portuguese)
      •  Romania (Romanian)
      •  Russia (Russian)
      •  Slovakia (Slovak)
      •  Slovenia (Slovenian)
      •  Spain (Spanish)
      •  Sweden (Swedish)
      •  Switzerland(German, French)
      •  Turkey (Turkish)
      •  United Kingdom
      • Asia Pacific
      •  Australia
      •  China
      •  Hong Kong
      •  India
      •  Korea (Korean)
      •  Malaysia
      •  New Zealand
      •  Philippines
      •  Singapore
      •  Taiwan
      •  Thailand (Thai)
      • Americas
      •  Brazil (Portuguese)
      •  Canada
      •  Mexico (Spanish)
      •  United States
      Can't find the country/region you're looking for? Visit our export site or find a local distributor.
  • Translate
  • Profile
  • Settings
Music Time
  • Challenges & Projects
  • Project14
  • Music Time
  • More
  • Cancel
Music Time
P14 Music Time Blog 100 year old player piano gets an update! Music Time via Python and Modbus TCP
  • Blog
  • Forum
  • Documents
  • Events
  • Polls
  • Files
  • Members
  • Mentions
  • Sub-Groups
  • Tags
  • More
  • Cancel
  • New
Join Music Time to participate - click to join for free!
  • Share
  • More
  • Cancel
Group Actions
  • Group RSS
  • More
  • Cancel
Engagement
  • Author Author: aspork42
  • Date Created: 14 Apr 2023 5:05 AM Date Created
  • Views 2073 views
  • Likes 14 likes
  • Comments 5 comments
  • Player Piano
  • python
  • musictimech
  • piano
  • festo
  • music time
  • modbus
  • tcp
  • pneumatic
Related
Recommended

100 year old player piano gets an update! Music Time via Python and Modbus TCP

aspork42
aspork42
14 Apr 2023

Ummmm what?

piano with Festo air manifold adapted to itAs with many of us, I've always got all sorts of wacky ideas floating around inside my melon. When Music Time contest came up, I thought it would be the perfect time to make this one happen! I've got a player piano from the early 1900's just sitting around making a great storage shelf and I've often considered a way to control it using modern techniques. This was the perfect time to try out using a Festo pneumatic air manifold left over from a project at work to see if I can tie them together and run via a script.

Soooo what did I do?

I modified a player piano so that I can run it via a Python script. This involved developing a 3D printed bracket that could bolt on to the player piano and route the vacuum lines out such that they could be routed into a Festo air manifold. This also required using Modbus (TCP) within a Python script to be able to send commands. I don't remember the exact age of the piano, but I believe it was from the "19-teens" or so. It's been upgraded to an electric vacuum pump but still has the foot pedals to 

One original goal was to be able to send RTTTL ringtones to this via MQTT. Perhaps I might roll this in later, but for now I ended up developing a somewhat similar; but more capable protocol for encoding the music and ended up just hard coding to save time. As you'll read below, there were multiple avenues of delays in this project and I was happy to get it to the current state; right at the end of the contest period. I'm not too far away from having this implemented in my script but ended up just coding the few piano tunes I know. 

What were my challenges?

Well - lots! As with a lot of fun projects, we're all always pushing and expanding and trying new things. Of course, I've never created a part for a player piano before, so that was a thing. Using Modbus TCP (or any version of Modbus really) was also something I've never done. Another significant challenge was setting up a /very/ unusual pneumatic system. Each of these has a section below for more details.

Also - I travel fairly often for work and also am married with two kids WHO GO TO BED EARLY NO YOU CAN'T STAY UP LATE PLEASE GO TO BED DADDY NEEDS TO PRACTICE PROGRAMMING PIANOS! ... ... ... which was also something to keep all balanced out Slight smile I leave in Wisconsin and have had about 3-4 trips to Illinois, plus trips to Louisville and St. Louis to balance out during this build. 

First some background on player pianos

A player piano is based on a normal piano, but uses a fantastic add-on system of pneumatics to allow music to be played back via specialized version of punched tape - aka a music scroll. A song consists of a scroll of paper with a series of holes punched into it. As the song plays, the paper is pulled across the "air bar" (note- no idea if that's the actual name of the part; thats just what I call it). Where there is a hole punched in the paper, then the vacuum for that particular key drops off and causes the key to strike the strings. 

The idea with this project is to replace the paper scroll with a pneumatic manifold that I can control electronically and cause the same effect. Old-school players required someone to sit there and pump two foot pedals attached to bellows to build up vacuum and allow the system to run completely mechanically. Player pianos have since been 'modernized' with a vacuum pump which now works at the flip of a switch to create the vacuum. 

So for an 88-key piano, to play a note, the paper music scroll will have only a single small hole for the one key that needs to be played; allowing air into the system in one specific location. So to upgrade to pneumatics, I would need to seal off all unused keys, and allow venting to ambient air for the active key that we want to 'press'. 

Also I wanted a 100% non-invasive method to do this. I needed something that was easily reversible as to not damage or alter the original piano. 

Lemmie see that 3D Printed bracket!

series of failed designs for a bracketOne major component that caused much consternation was the 3D printed adapter bracket. I had about 7 renditions (it felt like 100) of it to get the spacing dialed in for the holes, the hole positions, the pneumatic tube size (to make a vacuum-tight seal via push fit). I had to measure up the 'air bar' part of the player piano, then use Fusion360 to design the bracket. I started with a snap-on design that would Image showing internal design of Piano Air bar adapterhold itself to the piano; but found that it this severely inadequate in terms of a good vacuum seal. In the end, I used a design with one flat-faced manifold and two 'pinch pieces' that secure with M3 bolts and can be snugged down to the air bar of the piano to make a decent seal. I found that the top surface of the 3D printed bracket wasn't smooth enough right off the printer, so had to take to the garage and sand down with 80 and then 200-grit sand paper. This seemed to make it smooth enough that I could get a good seal. 

Each rendition took a couple of days to design, print, install, test, iterate. I also had to counter-sink the holes for the pneumatic tubes and do some additional design work on the air channels to make sure they weren't anemic to airflow. The pitch of the holes was about 1.5mm so there wasn't much room for error. I also had issues with my super-cheap 3D printer not being able to make airtight prints. I had to use 5 shells to allow enough plastic to hold back the vacuum without seeping through the plastic piece. This increased printing time to about 4 hours per test; so I couldn't iterate more than once per day; allowing for additional interruptions like work travel. 

I'll take the modBUS to work

A second challenge was using Modbus to control this system. I've never personally used modbus before so I had to learn about how it works. In a nutshell, Modbus protocol is sending (or getting) a set of 'coil' commands out to a remote [terminal] unit (RTU). This was traditionally done via the various serial protocols, but now is done via TCP. A coil would represent an individual output relay coil. In the end, it works pretty easily; but with using the Festo manifold I spent more than a few days trying to figure out why my programs weren't working.

I originally started using an Arduino MKR 1010 Wifi for this project and had it using a Modbus TCP library but wasn't getting solid communication to the Festo manifold. I then switched to running Python from my PC using a different Modbus library instead and still had the same issues. I had hoped that I could do better debugging from Pycharm than Arduino IDE but it turned out that didn't help. 

The Modbus TCP library mostly just requires the IP address of the destination device; plus the command. My pneumatic manifold has 12 physical positions for pneumatic valves; each position having 2 unique pneumatic solenoid positions (12x2= 24). So to send a command, I had to use a 24-bit command. I broke this out into a low-word (16 bits) and a high word (8 bits anyways) to total 24 bits which could activate the 24 individual coils.

Excel calculator for finding hex values of chordsAs noted above, using the Arduino library from a MKR 1010 Wifi, I got what appeared to be extremely poor reliability on the Festo manifold side. 75% of commands appeared to be dropped and the other 25% would be activated but drop out immediately. I found a DIP switch on the manifold that would "retain outputs on communications fault" and that seemed to help quite a bit. I could finally send a command to open/close a valve and the manifold would react predictably. But this still meant there was some underlying problems - ONES THAT I NEVER SOLVED Slight smile.

To create the 24-bit command which I needed to send, My process was to photograph my hand on the piano Slight smile then consult an Excel spreadsheet I created which I could input the keys I pressed and result with a hex-coded value representing the chord. The selected cell in the screenshot above shows the Hex value that I need to send via Modbus to achieve the cord indicated.

I swear this is better than RTTTL!

Although I'm already using RTTTL in my home automation system, one thing I noted about it is a limitation of not being polyphonic. An RTTTL ringtone is monophonic meaning it can only play a single tone at one given time. So any notion of a chord is "out the window". Imagine playing piano with one single finger instead of two hands. Once I got this system operational, I realized this artificial limitation and started down a different path. I also had to really scrounge to find enough of the correct valve types to get two octaves (24 keys) worth of notes. A lot of the RTTTL ringtones use 3+ octaves of notes so I wouldn't be able to play them properly to start with. 

 

Pneumatic - wha??

Image shows my first (unsuable) 2x 3/2 valve and a 5/3 valveSo one section of this build relates a lot to the specific pneumatic valves along with the manifold base. Pneumatics are used a lot in industrial automation and in concept; they're very very simple - open a valve and air pressure comes through and can 'do work'. Aren't pneumatics basically the same as realys just for air instead of electricity? That's mostly true; but there's a lot of nuance behind the scenes. Does the valve have a natural resting state? How does it get to the resting state? How does it get to the 'activated' state? Pneumatic valves have a concept of 'pilot' pressure which can help shuttle the physical valve between its multiple possible states. This means that the physical valve uses 24V to activate, but won't actually move unless a certain amount of air pressure is present in the pilot side of the valve. I spent a couple of days working with the manifold before I realized that they weren't doing what I expected because I didn't have any incoming air pressure. They will work starting at about 1.5 bar of pilot pressure; but the problem was that the manifold  by default takes pilot pressure directly from an incoming air supply port (aka 80 PSI). The problem I have is that I can't use that same physical channel since I needed to use ambient air on that channel to vent each valve to atmosphere for the Player Piano to work. 

Eventually I found that the Festo ecosystem has a device which can separate the pilot air pressure from the primary air channel which vents to ambient. I was able to get one of these and install in the manifold. This really bummed me out since requiring a pilot pressure means that the system requires and external pressurized air source. So I had to run a compressed air line out to my air compressor in the garage to make this work. I was expecting that I could run without any external air lines; but that wasn't the case. 

The image shown here shows the first valve (top left) I wanted to use which is internally two individual valves (2x @ 3/2); but used a pneumatic return. The valve on the bottom/right uses a spring return but is a 5/3 valve introducing a new limitation on which keys I can hit at the same time. 

This is only referring to getting the valve from 'resting' to 'activated' state. To go from 'activated' back to 'resting' is a different case. Some pneumatic valves have physical spring returns by itself; but some require (just like above) an external pilot pressure to shuttle the valve back over to the 'resting' state. The original 12(!!!) valves I had were all air-return and thus complete garbage for the application Slight smile. I was able to eventually scrounge together enough spring-return valves to make this work.

image

Another downside of the physical valves that I ended up using was they are 5/3 valves. This means that they have 5 physical ports and 3 positions. So this introduced another limitation versus the valves I had before which were 2x individual 3/2 valves in a single body. That meant I can't activate a pair of keys next to each other at the same time. This was a minor inconvenience but still a weird limitation. If you haven't worked with pneumatics; they are a weird beast. In practice, there's a lot of consideration into the desired failure states and non-powered states and this doesn't even begin to consider how flow controls work or mass going over-center on a mechanical system. 

So I had to designate the manifold port 1 as the 'ambient' channel; allocating for a port blocker for the pilot pressure. Ports 3 and 5 were dead-headed to allow for no airflow. 

Sooooooo it works, right?

Why yes, it works!

Have a peek at this video to see a demo reel Slight smile 

You don't have permission to edit metadata of this video.
Edit media
x
image
Upload Preview
image

Note that some keys don't always work. I believe that this is due to the 3D Printed adapter bracket possibly not aligning with the holes of the piano's air bar. I think a few more renditions and this could be resolved. 

And here is another peek at the background on how it works:

You don't have permission to edit metadata of this video.
Edit media
x
image
Upload Preview
image

Y U No Code Post?

Okay fine - Here's the Python code.

I essentially use two arrays to play the code. One gives the chord/note to play, and one gives the intonation or time for each chord. This is passed to a method which can send the command via ModbusTCB (library). As noted; there are some underlying issues and I was able to get around them via a DIP switch on the Festo manifold. But other than that it works Slight smile

import time
from pymodbus.client import ModbusTcpClient


def __init__(self):
    print("Starting...")


def playSong(msgtime, msgValue):
    print("Playing song!")
    client = ModbusTcpClient('192.168.1.24')
    timestart = time.time()

    n = 0
    while n < len(msgValue):

        if time.time() >= (timestart + msgtime[n]):
            client.connect()
            client.write_register(0, msgValue[n] & 0xFFFF)  # send low word
            client.write_register(1, msgValue[n] >> 16)  # send high word
            client.close()
            # client.write_register(0, 0x6c)
            n += 1

            print(time.time())
        # client.write_register(0, 0x6c)

# play song format using a note duration instead of overall time
def playSongND(msgtime, msgValue):
    print("Playing song!")
    client = ModbusTcpClient('192.168.1.24')
    lastTime = time.time()

    n = 0
    while n < len(msgValue):

        if time.time() >= (lastTime + msgtime[n]):
            client.connect()
            client.write_register(0, msgValue[n] & 0xFFFF)  # send low word
            client.write_register(1, msgValue[n] >> 16)  # send high word
            client.close()
            lastTime = time.time()
            # client.write_register(0, 0x6c)
            n += 1

            print(time.time())
        # client.write_register(0, 0x6c)

# play song format using a note duration instead of overall time
def playSongND1(msgtime, msgValue, transpose):
    client = ModbusTcpClient('192.168.1.24')
    lastTime = time.time()

    n = 0
    while n < len(msgValue):

        client.connect()

        msgValue[n] = msgValue[n] << transpose
        client.write_register(0, msgValue[n] & 0xFFFF)  # send low word
        client.write_register(1, msgValue[n] >> 16)  # send high word
        client.close()
        # print(msgValue[n])
        lastTime = time.time()
        # client.write_register(0, 0x6c)

        while time.time() < lastTime + msgtime[n]:
            pass
        n += 1
        # print(time.time())
        # client.write_register(0, 0x6c)

# if __name__ == '__main__':


def main():
    NextMessage = [.25, .25, .25, .25, .25, .25, .25, .25,
                   .25, .25, .25, .25, .25, .25, .25, .25,
                   .25, .25, .25, .25, .25, .25, .25, .25]
    NextCommand = [0x000001, 0x000002, 0x000004, 0x000008, 0x000010, 0x000020, 0x000040, 0x000080,
                   0x000100, 0x000200, 0x000400, 0x000800, 0x001000, 0x002000, 0x004000, 0x008000,
                   0x010000, 0x020000, 0x040000, 0x080000, 0x100000, 0x200000, 0x400000, 0x800000]
    # currentTime = time.time()

    # timestart = time.time()

    playSongND1(NextMessage, NextCommand, 0)
    n = 0
    client = ModbusTcpClient('192.168.1.24')

    # play batman song
    NextMessage = [.1, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25,
                   0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.7]
    NextCommand = [0x000000, 0x008000, 0x004000, 0x002000, 0x004000, 0x008000, 0x004000, 0x002000, 0x004000,
                   0x008000, 0x004000, 0x002000, 0x004000, 0x008000, 0x000000, 0x008000]
    playSongND1(NextMessage, NextCommand, 0)
    #play it again!
    playSongND1(NextMessage, NextCommand, 0)


    # play other song
    NextMessage = [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
                   0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
                   0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
                   0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,

                   0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
                   0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
                   0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
                   0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]

    NextCommand = [0x000000, 0x000412, 0x000000, 0x000412, 0x000000, 0x000412, 0x000000, 0x000412, 0x000000,
                   0x000412, 0x000000, 0x000412, 0x000000, 0x000412, 0x000000, 0x000412, 0x000000,
                   0x000422, 0x000000, 0x000422, 0x000000, 0x000422, 0x000000, 0x000422, 0x000000,
                   0x000422, 0x000000, 0x000422, 0x000000, 0x000422, 0x000000, 0x000422, 0x000000,

                   0x000412, 0x000000, 0x000412, 0x000000, 0x000412, 0x000000, 0x000412, 0x000000,
                   0x000412, 0x000000, 0x000412, 0x000000, 0x000412, 0x000000, 0x000412, 0x000000,
                   0x000422, 0x000000, 0x000422, 0x000000, 0x000422, 0x000000, 0x000422, 0x000000,
                   0x000422, 0x000000, 0x000422, 0x000000, 0x000422, 0x000000, 0x000422, 0x000000]


    playSongND1(NextMessage, NextCommand, 6)

    NextMessage = [.5, .2, .2, .5, 0.2, 0.2, 0.5, 0.3,
                   0.2, 0.3, 0.5, 0.15, 0.2
                   ]

    NextCommand = [0x000400, 0x010000, 0x008000, 0x000400, 0x010000, 0x008000, 0x000400,
                   0x010000, 0x008000, 0x000400, 0x010000,  0x008000, 0x000400
                   ]


    playSongND1(NextMessage, NextCommand, 6)

    # play other song
    NextMessage = [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
                   0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
                   0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
                   0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,

                   0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
                   0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
                   0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
                   0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]

    NextCommand = [0x000000, 0x000412, 0x000000, 0x000412, 0x000000, 0x000412, 0x000000, 0x000412, 0x000000,
                   0x000412, 0x000000, 0x000412, 0x000000, 0x000412, 0x000000, 0x000412, 0x000000,
                   0x000422, 0x000000, 0x000422, 0x000000, 0x000422, 0x000000, 0x000422, 0x000000,
                   0x000422, 0x000000, 0x000422, 0x000000, 0x000422, 0x000000, 0x000422, 0x000000,

                   0x000412, 0x000000, 0x000412, 0x000000, 0x000412, 0x000000, 0x000412, 0x000000,
                   0x000412, 0x000000, 0x000412, 0x000000, 0x000412, 0x000000, 0x000412, 0x000000,
                   0x000422, 0x000000, 0x000422, 0x000000, 0x000422, 0x000000, 0x000422, 0x000000,
                   0x000422, 0x000000, 0x000422, 0x000000, 0x000422, 0x000000, 0x000422, 0x000000]

    playSongND1(NextMessage, NextCommand, 6)

    # play other song
    NextMessage = [1, 2, .1, 3, .1]

    NextCommand = [0x000000, 0x910000, 0x000000, 0xA10000, 0x000000]

    playSongND1(NextMessage, NextCommand, 0)


main()

This protocol that I ended up developing also takes a parameter for transposing into different keys. In the demo reel video, some keys don't play. I think that this is a physical issue in the adapter piece; but ran out of time to diagnose. RTTTL gives a specific key/note to play; but my system isn't tied to a definitive note anywhere on the piano. The transpose parameter let me push 'up or down' any number of keys easily. 

Pretty cool buh?

Okay - calm down! But welcome to the spaghetti mess inside my head! I was pretty pumped as with many projects to see them go from a ridiculous idea into something that actually works in the physical world. This project raised a lot of challenges to overcome and lots of fun things to figure out. Adding MQTT would be actually pretty awesome and only a few lines of code - I have actually use that in Python before and it isn't too crazy.  Then I could play the piano whenever someone rings our doorbell or the cat uses the litter box. tariq.ahmad  this project will likely /not/ get left set up and running for very long since it's got a pretty low WAF; and weirdly a pretty low HAF as well. But it was super fun to play with!

  • Sign in to reply
  • aspork42
    aspork42 over 2 years ago in reply to dougw

    I laughed when I read this - yes; I’ve automated an automated piano Slight smile

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
  • dougw
    dougw over 2 years ago

     Not many people "just happen to have a player piano sitting around" Relaxed That is as remarkable as the nice job you did to automate an automated piano..

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
  • javagoza
    javagoza over 2 years ago

    Of course, very cool! My 3D printer doesn't come up with such ingenious ideas. It is always a pleasure to learn about your inventions.

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
  • aspork42
    aspork42 over 2 years ago in reply to genebren

    Thanks! This was a fun project. The end result is that it plays piano /at least/ as good as I do. Possibly better Slight smile

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
  • genebren
    genebren over 2 years ago

    What a fun project and a very playful blog. I really enjoyed reading through this.

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
element14 Community

element14 is the first online community specifically for engineers. Connect with your peers and get expert answers to your questions.

  • Members
  • Learn
  • Technologies
  • Challenges & Projects
  • Products
  • Store
  • About Us
  • Feedback & Support
  • FAQs
  • Terms of Use
  • Privacy Policy
  • Legal and Copyright Notices
  • Sitemap
  • Cookies

An Avnet Company © 2025 Premier Farnell Limited. All Rights Reserved.

Premier Farnell Ltd, registered in England and Wales (no 00876412), registered office: Farnell House, Forge Lane, Leeds LS12 2NE.

ICP 备案号 10220084.

Follow element14

  • X
  • Facebook
  • linkedin
  • YouTube