The Qt Scribble demo app rewritten in Python, which receives OSC messages that change the pen size, both absolute via `/penwidth` and relative via `/penwidthby`, both of which take integer parameters. Naturally other parameters such as colour could be controlled in the same way, and other events, as illustrated by `/clear`. That is the general idea, and all the clever workflow design can be factored out of the app -- the core of the app **needs only to do as it is told**. Part of the idea of Hedgehog is that the core of an app is controlled by receiving messages, **both from the App's own UI, and also from external sources, so that a user can control an app with anything he/she wishes**. Thus *Our Hedgehogs can talk to our applications*. The point to make is how easy it can be to Hedgehog-enable an application. (Here OSC is used as the messaging protocol, and it probably will suffice, Hedgehog is the concept of controlling our apps with possibly-custom-designed peripherals with a varied array of knobs and buttons.) In this instance we are using PySide6, and Qt nicely provides us with a UdpSocket class that neatly integrates with Qt's signals and slots and its runloop. Basically an app needs to start a thread which listens on the socket and, when they are received, store them somewhere and notify the main runloop that there is a message to be processed. Once that is done, you can control your apps happily using whatever control surface you wish to fashion, be it made of Midi controllers, or something cooked up with an Arduino, or some commercial knobs-and-buttons peripherals like the TourBox. ## scribble.py ```python from PySide6.QtCore import * from PySide6.QtGui import * from PySide6.QtWidgets import * from PySide6.QtNetwork import * import sys from pythonosc.osc_message import OscMessage app = QApplication(sys.argv) class HHReceiver(QObject): # this only works with datagrams containing single osc messages def handleUdp(self): while self.udpSocket.hasPendingDatagrams(): datagram = self.udpSocket.receiveDatagram(4096) data = datagram.data().data() print(f"Recv ({len(data)}): {data}") try: message = OscMessage(data) except Exception as e: print(f"#Fail to parse datagram ({e}) : {data}") self.processMessage(message) def processMessage(self,message): addr = message.address args = message.params if addr == "/penwidth": try: newWidth = int(args[0]) self.scribble.setPenWidth(newWidth) except IndexError: print(f"/penwidth message must have single integer param") elif addr == "/penwidthby": try: dx = int(args[0]) self.scribble.adjustPenWidthBy(dx) except IndexError: print(f"/penwidthby message must have single integer param") elif addr == "/clear": self.scribble.clearImage() def __init__(self,scribble,port=2800): self.scribble = scribble self.udpSocket = QUdpSocket() self.udpSocket.readyRead.connect(self.handleUdp) self.udpSocket.bind(QHostAddress.Any,port) class ScribbleArea(QWidget): def __init__(self,*xs,**kw): super().__init__(*xs,**kw) self.setAttribute(Qt.WA_StaticContents) self._modified = False self._scribbling = False self._penWidth = 1 self._penColor = Qt.blue self.image = QImage() self.lastPoint = QPoint() def adjustPenWidthBy(self,dx): self.setPenWidth(self._penWidth+dx) def setPenColor(self, newColor : QColor): self._penColor = newColor def setPenWidth(self, newWidth : int): self._penWidth = min(max(1,newWidth),100) def isModified(self) -> bool: return self._modified def penColor(self) -> QColor: return self._penColor def penwidth(self) -> int: return self._penWidth def clearImage(self): self.image.fill(qRgb(255,255,255)) self.modified = True self.update() def mousePressEvent(self,event): if event.button() == Qt.LeftButton: self.lastPoint = event.position().toPoint() self.scribbling = True def mouseMoveEvent(self,event): if (event.buttons() & Qt.LeftButton) and self.scribbling: self.drawLineTo(event.position().toPoint()) def mouseReleaseEvent(self,event): if event.button() == Qt.LeftButton and self.scribbling: self.drawLineTo(event.position().toPoint()) self.scribbling = False def paintEvent(self,event): with QPainter(self) as p: dirtyRect = event.rect() p.drawImage(dirtyRect,self.image,dirtyRect) def resizeEvent(self,event): w = self.width() h = self.height() iw = self.image.width() ih = self.image.height() if w > iw or h > ih: nw = max(w+128,iw) nh = max(h+128,ih) self.resizeImage(self.image,QSize(nw,nh)) super().resizeEvent(event) def drawLineTo(self,endPoint : QPoint): with QPainter(self.image) as p: p.setPen(QPen(self._penColor,self._penWidth,Qt.SolidLine,Qt.RoundCap,Qt.RoundJoin)) p.drawLine(self.lastPoint,endPoint) self.modified = True rad = int((self._penWidth // 3)*2) self.update(QRect(self.lastPoint, endPoint).normalized().adjusted(-rad,-rad,+rad,+rad)) self.lastPoint = endPoint def resizeImage(self,image : QImage, newSize: QSize): if image.size() == newSize: return newImage = QImage(newSize, QImage.Format_RGB32) newImage.fill(qRgb(255,255,255)) with QPainter(newImage) as p: p.drawImage(QPoint(0,0),self.image) self.image = newImage scr = ScribbleArea() scr.resize(600,500) scr.show() h = HHReceiver(scr) exit(app.exec()) ``` ## a trivial sender for interactive use ```python description = ''' load in interactive mode (i.e. python -i trivial_send.py) then use s("/osc/message",[1,2,3,4]) to send ''' import socket from pythonosc import udp_client client = udp_client.SimpleUDPClient("localhost",2800,family=socket.AF_INET) def s(path,*args): print(f"Sending {path=} {args=}") client.send_message(path,args) ``` usage ```bash python -i sender.py ``` example transcript of interactive session ```plaintext >>> s("/penwidth",15) Sending path='/penwidth' args=(15,) >>> s("/penwidthby",15) Sending path='/penwidthby' args=(15,) >>> s("/clear") Sending path='/clear' args=() ```