Changeset - c5974a6bbc28
[Not reviewed]
default
0 3 0
drewp@bigasterisk.com - 20 months ago 2023-05-18 17:13:55
drewp@bigasterisk.com
reformat
3 files changed with 26 insertions and 32 deletions:
0 comments (0 inline, 0 general)
light9/collector/output.py
Show inline comments
 
from rdflib import URIRef
 
import socket
 
import struct
 
import time
 
import usb.core
 
import logging
 
from twisted.internet import threads, reactor, task
 
from light9.metrics import metrics
 

	
 
log = logging.getLogger('output')
 
logAllDmx = logging.getLogger('output.allDmx')
 

	
 

	
 
class Output(object):
 
    """
 
    send a binary buffer of values to some output device. Call update
 
    as often as you want- the result will be sent as soon as possible,
 
    and with repeats as needed to outlast hardware timeouts.
 

	
 
    This base class doesn't ever call _write. Subclasses below have
 
    strategies for that.
 
@@ -33,71 +34,69 @@ class Output(object):
 
    def reconnect(self):
 
        pass
 

	
 
    def shortId(self) -> str:
 
        """short string to distinguish outputs"""
 
        return self.uri.rstrip('/').rsplit('/')[-1]
 

	
 
    def update(self, buf: bytes) -> None:
 
        """caller asks for the output to be this buffer"""
 
        self._currentBuffer = buf
 

	
 
    def _periodicLog(self):
 
        msg = '%s: %s' % (self.shortId(), ' '.join(map(str,
 
                                                       self._currentBuffer)))
 
        msg = '%s: %s' % (self.shortId(), ' '.join(map(str, self._currentBuffer)))
 
        if msg != self._lastLoggedMsg:
 
            log.debug(msg)
 
            self._lastLoggedMsg = msg
 

	
 
    def _write(self, buf: bytes) -> None:
 
        """
 
        write buffer to output hardware (may be throttled if updates are
 
        too fast, or repeated if they are too slow)
 
        """
 
        pass
 

	
 
    def crash(self):
 
        log.error('unrecoverable- exiting')
 
        reactor.crash()
 

	
 

	
 
class DummyOutput(Output):
 

	
 
    def __init__(self, uri, **kw):
 
        super().__init__(uri)
 

	
 
    def update(self, buf:bytes):
 
    def update(self, buf: bytes):
 
        log.info(f'dummy update {list(map(int,buf[:80]))}')
 

	
 

	
 
class BackgroundLoopOutput(Output):
 
    """Call _write forever at 20hz in background threads"""
 

	
 
    rate: float
 

	
 
    def __init__(self, uri, rate=22):
 
        super().__init__(uri)
 
        self.rate = rate
 
        self._currentBuffer = b''
 

	
 
        self._loop()
 

	
 
    def _loop(self):
 
        start = time.time()
 
        sendingBuffer = self._currentBuffer
 

	
 
        def done(worked):
 
            metrics('write_success', output=self.shortId()).incr()
 
            reactor.callLater(max(0, start + 1 / self.rate - time.time()),
 
                              self._loop)
 
            reactor.callLater(max(0, start + 1 / self.rate - time.time()), self._loop)
 

	
 
        def err(e):
 
            metrics('write_fail', output=self.shortId()).incr()
 
            log.error(e)
 
            reactor.callLater(.2, self._loop)
 

	
 
        d = threads.deferToThread(self._write, sendingBuffer)
 
        d.addCallbacks(done, err)
 

	
 

	
 
class FtdiDmx(BackgroundLoopOutput):
 

	
 
@@ -111,76 +110,73 @@ class FtdiDmx(BackgroundLoopOutput):
 
        with metrics('write', output=self.shortId()).time():
 
            if not buf:
 
                logAllDmx.debug('%s: empty buf- no output', self.shortId())
 
                return
 

	
 
            # ok to truncate the last channels if they just went
 
            # to 0? No it is not. DMX receivers don't add implicit
 
            # zeros there.
 
            buf = bytes([0]) + buf[:self.lastDmxChannel]
 

	
 
            if logAllDmx.isEnabledFor(logging.DEBUG):
 
                # for testing fps, smooth fades, etc
 
                logAllDmx.debug('%s: %s...' %
 
                                (self.shortId(), ' '.join(map(str, buf[:32]))))
 
                logAllDmx.debug('%s: %s...' % (self.shortId(), ' '.join(map(str, buf[:32]))))
 

	
 
            self.dmx.send_dmx(buf)
 

	
 

	
 
class ArtnetDmx(BackgroundLoopOutput):
 
    # adapted from https://github.com/spacemanspiff2007/PyArtNet/blob/master/pyartnet/artnet_node.py (gpl3)
 
    def __init__(self, uri, host, port, rate):
 
        """sends UDP messages to the given host/port"""
 
        super().__init__(uri, rate)
 
        packet = bytearray()
 
        packet.extend(map(ord, "Art-Net"))
 
        packet.append(0x00)  # Null terminate Art-Net
 
        packet.extend([0x00, 0x50])  # Opcode ArtDMX 0x5000 (Little endian)
 
        packet.extend([0x00, 0x0e])  # Protocol version 14
 
        self.base_packet = packet
 
        self.sequence_counter = 255
 
        self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
 

	
 
    def _write(self, buf):
 
       with metrics('write', output=self.shortId()).time():
 
        with metrics('write', output=self.shortId()).time():
 
            if not buf:
 
                logAllDmx.debug('%s: empty buf- no output', self.shortId())
 
                return
 

	
 
            if logAllDmx.isEnabledFor(logging.DEBUG):
 
                # for testing fps, smooth fades, etc
 
                logAllDmx.debug('%s: %s...' %
 
                                (self.shortId(), ' '.join(map(str, buf[:32]))))
 
                logAllDmx.debug('%s: %s...' % (self.shortId(), ' '.join(map(str, buf[:32]))))
 

	
 
            if self.sequence_counter:
 
                self.sequence_counter += 1
 
                if self.sequence_counter > 255:
 
                    self.sequence_counter = 1
 
            packet = self.base_packet[:]
 
            packet.append(self.sequence_counter)  # Sequence,
 
            packet.append(0x00)  # Physical
 
            universe_nr = 0
 
            packet.append(universe_nr & 0xFF)  # Universe LowByte
 
            packet.append(universe_nr >> 8 & 0xFF)  # Universe HighByte
 

	
 
            packet.extend(struct.pack(
 
                '>h', len(buf)))  # Pack the number of channels Big endian
 
            packet.extend(struct.pack('>h', len(buf)))  # Pack the number of channels Big endian
 
            packet.extend(buf)
 

	
 
            self._socket.sendto(packet, ('127.0.0.1', 6454))
 

	
 

	
 
class Udmx(BackgroundLoopOutput):
 

	
 
    def __init__(self, uri, bus, address, lastDmxChannel, rate=22):
 
    def __init__(self, uri: URIRef, bus: int, address: int, lastDmxChannel: int, rate=22):
 
        self.bus = bus
 
        self.address = address
 
        self.lastDmxChannel = lastDmxChannel
 
        self.dev = None
 
        super().__init__(uri, rate=rate)
 

	
 
        self.reconnect()
 

	
 
    def shortId(self) -> str:
 
        return super().shortId() + f'_bus={self.bus}'
 

	
 
    def reconnect(self):
 
@@ -207,50 +203,47 @@ class Udmx(BackgroundLoopOutput):
 
            try:
 
                if not buf:
 
                    logAllDmx.debug('%s: empty buf- no output', self.shortId())
 
                    return
 

	
 
                # ok to truncate the last channels if they just went
 
                # to 0? No it is not. DMX receivers don't add implicit
 
                # zeros there.
 
                buf = buf[:self.lastDmxChannel]
 

	
 
                if logAllDmx.isEnabledFor(logging.DEBUG):
 
                    # for testing fps, smooth fades, etc
 
                    logAllDmx.debug(
 
                        '%s: %s...' %
 
                        (self.shortId(), ' '.join(map(str, buf[:32]))))
 
                    logAllDmx.debug('%s: %s...' % (self.shortId(), ' '.join(map(str, buf[:32]))))
 

	
 
                sent = self.dev.send_multi_value(1, buf)
 
                if sent != len(buf):
 
                    raise ValueError("incomplete send")
 
            except ValueError:
 
                self.reconnect()
 
                raise
 
            except usb.core.USBError as e:
 
                # not in main thread
 
                if e.errno == 75:
 
                    metrics('write_overflow', output=self.shortId()).inc()
 
                    return
 

	
 
                if e.errno == 5:  # i/o err
 
                    metrics('write_io_error', output=self.shortId()).inc()
 
                    return
 

	
 
                if e.errno == 32:  # pipe err
 
                    metrics('write_pipe_error', output=self.shortId()).inc()
 
                    return
 

	
 
                msg = 'usb: sending %s bytes to %r; error %r' % (len(buf),
 
                                                                 self.uri, e)
 
                msg = 'usb: sending %s bytes to %r; error %r' % (len(buf), self.uri, e)
 
                log.warn(msg)
 

	
 
                if e.errno == 13:  # permissions
 
                    return self.crash()
 

	
 
                if e.errno == 19:  # no such dev; usb hw restarted
 
                    self.reconnect()
 
                    return
 

	
 
                raise
 

	
 

	
light9/web/live/Light9LiveControls.ts
Show inline comments
 
@@ -87,25 +87,25 @@ export class Light9LiveControls extends 
 
      this.onEffectChoice();
 
    }
 
  }
 

	
 
  findDevices(patch?: Patch) {
 
    const U = this.graph.U();
 
    if (patch && !patchContainsPreds(patch, [U("rdf:type")])) {
 
      return;
 
    }
 

	
 
    this.devices = [];
 
    let classes = this.graph.subjects(U("rdf:type"), U(":DeviceClass"));
 
    log(`found ${classes.length} device classes`)
 
    log(`found ${classes.length} device classes`);
 
    uniq(sortBy(classes, "value"), true).forEach((dc) => {
 
      sortBy(this.graph.subjects(U("rdf:type"), dc), "value").forEach((dev) => {
 
        this.devices.push(dev as NamedNode);
 
      });
 
    });
 
    this.requestUpdate();
 
  }
 

	
 
  setEffectFromUrl() {
 
    // not a continuous bidi link between url and effect; it only reads
 
    // the url when the page loads.
 
    const effect = new URL(window.location.href).searchParams.get("effect");
light9/web/live/index.html
Show inline comments
 
<!doctype html>
 
<!DOCTYPE html>
 
<html>
 
  <head>
 
    <title>device control</title>
 
    <meta charset="utf-8" />
 
    <link rel="stylesheet" href="../style.css">
 
    <link rel="stylesheet" href="../style.css" />
 
    <script type="module" src="../live/Light9LiveControls"></script>
 
  </head>
 
  <body>
 
    <style>
 
     body, html {
 
         margin: 0;
 
     }
 
     light9-live-controls {
 
         position: absolute;
 
         left: 2px;
 
         top: 2px;
 
         right: 8px;
 
         bottom: 0;
 
     }
 
      body,
 
      html {
 
        margin: 0;
 
      }
 
      light9-live-controls {
 
        position: absolute;
 
        left: 2px;
 
        top: 2px;
 
        right: 8px;
 
        bottom: 0;
 
      }
 
    </style>
 
    <light9-live-controls></light9-live-controls>    
 
    <light9-live-controls></light9-live-controls>
 
  </body>
 
</html>
0 comments (0 inline, 0 general)