Compare commits

..

2 Commits

85 changed files with 60 additions and 37685 deletions

View File

@@ -2,4 +2,3 @@ rotate_180.mp4 filter=lfs diff=lfs merge=lfs -text
rotate_360.mp4 filter=lfs diff=lfs merge=lfs -text rotate_360.mp4 filter=lfs diff=lfs merge=lfs -text
rotate_540.mp4 filter=lfs diff=lfs merge=lfs -text rotate_540.mp4 filter=lfs diff=lfs merge=lfs -text
rotate_90.mp4 filter=lfs diff=lfs merge=lfs -text rotate_90.mp4 filter=lfs diff=lfs merge=lfs -text
tilt_180.mp4 filter=lfs diff=lfs merge=lfs -text

View File

@@ -73,11 +73,6 @@ class MovingHead:
if __name__ == "__main__": if __name__ == "__main__":
lighting = MovingHead(43)
lighting.tilt = -0.5 * math.pi
lighting.rgbw = (0, 0, 0, 0xFF)
lighting.dimmer = 1
head = MovingHead(1) head = MovingHead(1)
head.rgbw = (0x00, 0x00, 0xFF, 0) head.rgbw = (0x00, 0x00, 0xFF, 0)
head.tilt = -0.5 * math.pi head.tilt = -0.5 * math.pi
@@ -99,8 +94,8 @@ if __name__ == "__main__":
t0 = time.time() t0 = time.time()
left = -0.5 * math.pi left = -1.5 * math.pi
right = 0.5 * math.pi right = 1.5 * math.pi
while True: while True:
@@ -108,14 +103,13 @@ if __name__ == "__main__":
if int(now) % 10 < 5: if int(now) % 10 < 5:
head.tilt = left head.pan = left
head.rgbw = (0xFF, 0x00, 0x00, 0) head.rgbw = (0xFF, 0x00, 0x00, 0)
else: else:
head.tilt = right head.pan = right
head.rgbw = (0x00, 0xFF, 0x00, 0) head.rgbw = (0x00, 0xFF, 0x00, 0)
head.render(dmx_data) head.render(dmx_data)
lighting.render(dmx_data)
ser.write(dmx_data) ser.write(dmx_data)
ser.flush() ser.flush()

View File

@@ -2,7 +2,7 @@
"cells": [ "cells": [
{ {
"cell_type": "markdown", "cell_type": "markdown",
"id": "da2f6efd", "id": "1f449a1b",
"metadata": {}, "metadata": {},
"source": [ "source": [
"# Fun with Aruco\n", "# Fun with Aruco\n",
@@ -12,7 +12,7 @@
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
"id": "23bed7f2", "id": "564b672c",
"metadata": {}, "metadata": {},
"source": [ "source": [
"## Imports" "## Imports"
@@ -21,7 +21,7 @@
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
"id": "91e61bf0", "id": "a949b314",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@@ -37,7 +37,7 @@
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
"id": "e1742fba", "id": "dd01b516",
"metadata": {}, "metadata": {},
"source": [ "source": [
"## Marker\n", "## Marker\n",
@@ -48,7 +48,7 @@
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
"id": "7337ad91", "id": "7a1ea892",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@@ -64,7 +64,7 @@
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
"id": "e5a9c142", "id": "1e9190d0",
"metadata": {}, "metadata": {},
"source": [ "source": [
"## Input\n", "## Input\n",
@@ -75,7 +75,7 @@
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
"id": "b574834e", "id": "49a9be85",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@@ -138,7 +138,7 @@
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
"id": "e431d5af", "id": "5feb4803",
"metadata": {}, "metadata": {},
"source": [ "source": [
"The input contains several periods of the head moving back and forth.\n", "The input contains several periods of the head moving back and forth.\n",
@@ -148,7 +148,7 @@
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
"id": "b5d07cc1", "id": "334b2d75",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@@ -176,7 +176,7 @@
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
"id": "7c461964", "id": "c7685829",
"metadata": {}, "metadata": {},
"source": [ "source": [
"Since our data is in the form `(timestamp, value)`, we need to group several data points into buckets before we can calculate the average." "Since our data is in the form `(timestamp, value)`, we need to group several data points into buckets before we can calculate the average."
@@ -185,7 +185,7 @@
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
"id": "c1c2def4", "id": "8386785a",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@@ -203,272 +203,7 @@
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
"id": "91576c76", "id": "6b3bda21",
"metadata": {},
"source": [
"# Tilt 180 Degrees"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "62131fc1",
"metadata": {},
"outputs": [],
"source": [
"cap = cv2.VideoCapture(\"tilt_180.mp4\")\n",
"fps = cap.get(cv2.CAP_PROP_FPS)\n",
"\n",
"phi = []\n",
"t = []\n",
"\n",
"frames_ok = 0\n",
"frames_err = 0\n",
"\n",
"report = int(fps + 0.5)\n",
"\n",
"while cap.isOpened():\n",
" # get frame data\n",
" frame_id = cap.get(1)\n",
" ret, frame = cap.read()\n",
" break\n",
"\n",
"del cap\n",
"\n",
"fig = plt.figure(figsize=(15, 7))\n",
"ax = fig.add_subplot(1, 2, 1)\n",
"ax.imshow(frame)\n",
"\n",
"roi_topleft = (1400, 600)\n",
"roi_bottomright = (2300, 1500)\n",
"\n",
"(a, b) = roi_topleft\n",
"(c, d) = roi_bottomright\n",
"roi = frame[b:d, a:c]\n",
"\n",
"ax = fig.add_subplot(1, 2, 2)\n",
"ax.imshow(roi)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8b9e1d0d",
"metadata": {},
"outputs": [],
"source": [
"global_t, global_phi = read_rotation_data(\"tilt_180.mp4\", (1400, 600), (2300, 1500))"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a9b830fc",
"metadata": {},
"outputs": [],
"source": [
"fig = plt.figure(figsize=(15, 7))\n",
"ax = fig.add_subplot()\n",
"ax.plot(global_t, global_phi)\n",
"ax.yaxis.set_major_locator(mpl.ticker.MultipleLocator(base=math.tau/2))\n",
"ax.set_xlabel(\"Time [s]\")\n",
"ax.set_ylabel(\"Angle [rad]\")\n",
"plt.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "91ac96c4",
"metadata": {},
"outputs": [],
"source": [
"# raw\n",
"\n",
"period = 5\n",
"t, phi = overlay(global_t, global_phi, 3.07, 2 * period, flip_even = False)\n",
"\n",
"# fix discontinuity and shift so 0 is vertical center\n",
"for (i, x) in enumerate(t):\n",
" if phi[i] < -1:\n",
" phi[i] += math.tau\n",
" phi[i] -= math.tau / 2\n",
" \n",
"# overlay again to merge both motions\n",
"\n",
"t, phi = overlay(t, phi, 0, period)\n",
"\n",
"phi_0 = phi[np.argmin(t)]\n",
"\n",
"for i in range(len(phi)):\n",
" phi[i] -= phi_0\n",
"\n",
"# average\n",
"\n",
"bucket_size = dt = 0.025\n",
"buckets = get_buckets(t, phi, bucket_size)\n",
"bucket_offset = bucket_size / 2\n",
"\n",
"t_tilt = np.linspace(0 + bucket_offset, period + bucket_offset, len(buckets))\n",
"pos_tilt = [sum(bucket) / len(bucket) for bucket in buckets]\n",
"\n",
"# velocity\n",
"\n",
"vel_tilt = np.diff(pos_tilt, prepend=0) / dt\n",
"\n",
"# acceleration\n",
"\n",
"n = 7 # the larger n is, the smoother curve will be\n",
"b = [1.0 / n] * n\n",
"a = 1\n",
"vel_tilt_filtered = scipy.signal.lfilter(b,a,vel_tilt)\n",
"\n",
"acc_tilt = np.diff(vel_tilt_filtered, prepend=0) / dt"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3df2b5ce",
"metadata": {},
"outputs": [],
"source": [
"fig = plt.figure(figsize=(15,15))\n",
"\n",
"# raw\n",
" \n",
"ax = fig.add_subplot(3,1,1)\n",
"ax.yaxis.set_major_locator(mpl.ticker.MultipleLocator(base=math.tau/2))\n",
"ax.set_ylabel(\"Angle [rad]\")\n",
"ax.scatter(t, phi, color=\"lightgray\")\n",
"\n",
"# average\n",
"\n",
"ax.plot(t_tilt, pos_tilt)\n",
"\n",
"# velocity\n",
"\n",
"ax = fig.add_subplot(3,1,2)\n",
"ax.yaxis.set_major_locator(mpl.ticker.MultipleLocator(base=math.tau/2))\n",
"ax.set_ylabel(\"Angular Velocity [rad / s]\")\n",
"ax.plot(t_tilt, vel_tilt)\n",
"ax.plot(t_tilt, vel_tilt_filtered, color=\"gray\")\n",
"\n",
"acc_start_time = 0.65\n",
"acc_stop_time = 0.9\n",
"dec_start_time = 1.5\n",
"dec_stop_time = 1.85\n",
"\n",
"for x in [acc_start_time, acc_stop_time, dec_start_time, dec_stop_time]:\n",
" ax.axvline(x, color='gray', linestyle='dotted')\n",
"\n",
"# acceleration\n",
"\n",
"ax = fig.add_subplot(3,1,3)\n",
"ax.yaxis.set_major_locator(mpl.ticker.MultipleLocator(base=math.tau/2))\n",
"ax.set_ylabel(\"Acceleration [rad / $s^2$]\")\n",
"ax.set_xlabel(\"Time [s]\")\n",
"ax.plot(t_tilt, acc_tilt)\n",
"\n",
"plt.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "89e17f29",
"metadata": {},
"outputs": [],
"source": [
"# approximated constant acceleration\n",
"\n",
"acc_tilt_approx = np.zeros(len(acc_tilt))\n",
"\n",
"t_0 = acc_stop_time - acc_start_time\n",
"t_1 = dec_start_time - acc_stop_time\n",
"t_2 = dec_stop_time - dec_start_time\n",
"h = pos_tilt[-1] # 0.5 * math.tau\n",
"\n",
"acc_start = np.searchsorted(t_tilt, acc_start_time)\n",
"acc_stop = np.searchsorted(t_tilt, acc_stop_time)\n",
"\n",
"acc_value = h / ((t_0 / 2 + t_1 + t_2 / 2) * t_0)\n",
"\n",
"print(f\"acceleration: {acc_value: .2f} ({acc_value / math.pi: .2f} pi)\")\n",
"\n",
"dec_start = np.searchsorted(t_tilt, dec_start_time)\n",
"dec_stop = np.searchsorted(t_tilt, dec_stop_time)\n",
"\n",
"dec_value = -1 * acc_value * (acc_stop - acc_start) / (dec_stop - dec_start)\n",
"\n",
"print(f\"deceleration: {dec_value: .2f} ({dec_value / math.pi: .2f} pi)\")\n",
"\n",
"acc_tilt_approx[acc_start:acc_stop].fill(acc_value)\n",
"acc_tilt_approx[dec_start:dec_stop].fill(dec_value)\n",
"\n",
"# approximated velocity\n",
"\n",
"vel_tilt_approx = np.cumsum(acc_tilt_approx) * dt\n",
"\n",
"# approximated position\n",
"\n",
"pos_tilt_approx = np.cumsum(vel_tilt_approx) * dt"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b0bc02f4",
"metadata": {},
"outputs": [],
"source": [
"fig = plt.figure(figsize=(15,15))\n",
"\n",
"# raw\n",
" \n",
"ax = fig.add_subplot(3,1,1)\n",
"ax.yaxis.set_major_locator(mpl.ticker.MultipleLocator(base=math.tau/2))\n",
"ax.set_ylabel(\"Angle [rad]\")\n",
"ax.scatter(t, phi, color=\"lightgray\")\n",
"\n",
"# average\n",
"\n",
"ax.plot(t_tilt, pos_tilt)\n",
"\n",
"ax.plot(t_tilt, pos_tilt_approx)\n",
"\n",
"# velocity\n",
"\n",
"ax = fig.add_subplot(3,1,2)\n",
"ax.yaxis.set_major_locator(mpl.ticker.MultipleLocator(base=math.tau/2))\n",
"ax.set_ylabel(\"Angular Velocity [rad / s]\")\n",
"ax.plot(t_tilt, vel_tilt)\n",
"ax.plot(t_tilt, vel_tilt_filtered, color=\"gray\", linestyle=\"dotted\")\n",
"\n",
"for x in [acc_start_time, acc_stop_time, dec_start_time, dec_stop_time]:\n",
" ax.axvline(x, color='gray', linestyle='dotted')\n",
" \n",
"ax.plot(t_tilt, vel_tilt_approx)\n",
"\n",
"# acceleration\n",
"\n",
"ax = fig.add_subplot(3,1,3)\n",
"ax.yaxis.set_major_locator(mpl.ticker.MultipleLocator(base=math.tau/2))\n",
"ax.set_ylabel(\"Acceleration [rad / $s^2$]\")\n",
"ax.set_xlabel(\"Time [s]\")\n",
"ax.plot(t_tilt, acc_tilt, color=\"lightgray\")\n",
"\n",
"for x in [acc_start_time, acc_stop_time, dec_start_time, dec_stop_time]:\n",
" ax.axvline(x, color='gray', linestyle='dotted')\n",
" \n",
"ax.plot(t_tilt, acc_tilt_approx, color=\"C1\")\n",
"\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"id": "163e4dca",
"metadata": {}, "metadata": {},
"source": [ "source": [
"# 360 Degrees" "# 360 Degrees"
@@ -477,7 +212,7 @@
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
"id": "e7c5bd8b", "id": "9888eedd",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@@ -487,7 +222,7 @@
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
"id": "4be22807", "id": "0f4a9e22",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@@ -503,7 +238,7 @@
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
"id": "7d6f3694", "id": "861859fa",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@@ -543,7 +278,7 @@
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
"id": "f659cc9a", "id": "a52705aa",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@@ -584,7 +319,7 @@
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
"id": "6e7da8e8", "id": "9f0e11e1",
"metadata": {}, "metadata": {},
"source": [ "source": [
"## Approximation\n", "## Approximation\n",
@@ -601,7 +336,7 @@
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
"id": "106d2335", "id": "8dc88112",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@@ -648,7 +383,7 @@
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
"id": "48716f50", "id": "887a4959",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@@ -698,7 +433,7 @@
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
"id": "9b0adcee", "id": "2699200d",
"metadata": {}, "metadata": {},
"source": [ "source": [
"# 540 Degrees" "# 540 Degrees"
@@ -707,7 +442,7 @@
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
"id": "8b75f733", "id": "267344a0",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@@ -717,7 +452,7 @@
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
"id": "bef17bf1", "id": "552dd8b3",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@@ -733,7 +468,7 @@
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
"id": "08df7358", "id": "1c10cff1",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@@ -785,7 +520,7 @@
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
"id": "4b226630", "id": "8261dffd",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@@ -826,7 +561,7 @@
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
"id": "252158d8", "id": "b2bbd83f",
"metadata": {}, "metadata": {},
"source": [ "source": [
"## Approximation" "## Approximation"
@@ -835,7 +570,7 @@
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
"id": "245fc545", "id": "65cbe584",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@@ -882,7 +617,7 @@
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
"id": "d2baff7c", "id": "d59c9632",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@@ -932,7 +667,7 @@
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
"id": "6dba7e18", "id": "4eda53a3",
"metadata": {}, "metadata": {},
"source": [ "source": [
"# 180 degrees" "# 180 degrees"
@@ -941,7 +676,7 @@
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
"id": "1aa14192", "id": "02da4e87",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@@ -951,7 +686,7 @@
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
"id": "126c1611", "id": "91a2fda0",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@@ -967,7 +702,7 @@
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
"id": "8634be80", "id": "1b56893a",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@@ -1017,7 +752,7 @@
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
"id": "0e85ff00", "id": "342d9551",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@@ -1063,7 +798,7 @@
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
"id": "e9c0a9ef", "id": "18c3f7b1",
"metadata": {}, "metadata": {},
"source": [ "source": [
"## Approximation" "## Approximation"
@@ -1072,7 +807,7 @@
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
"id": "d1a7564e", "id": "84ece5d2",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@@ -1114,7 +849,7 @@
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
"id": "89c99381", "id": "5fec98f9",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@@ -1164,7 +899,7 @@
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
"id": "82c5bfa6", "id": "c6811894",
"metadata": {}, "metadata": {},
"source": [ "source": [
"## Comparison" "## Comparison"
@@ -1173,7 +908,7 @@
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
"id": "04772b01", "id": "faba1d94",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@@ -1222,151 +957,9 @@
"\n", "\n",
"plt.show()" "plt.show()"
] ]
},
{
"cell_type": "markdown",
"id": "374c818e",
"metadata": {},
"source": [
"# Model"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f2790606",
"metadata": {},
"outputs": [],
"source": [
"v_max = 3.61 # rad / s\n",
"\n",
"a_acc = 4.51 # rad / s^2\n",
"a_dec = 3.83 # rad / s^2\n",
"\n",
"def time_taken(delta_phi, v_0):\n",
"\n",
" time_acc = (v_max - v_0) / a_acc\n",
" print(f\"{time_acc=}\")\n",
"\n",
" traveled_while_acc = (v_max ** 2 - v_0 ** 2) / (2 * a_acc)\n",
" print(f\"{traveled_while_acc=}\")\n",
"\n",
" time_dec = v_max / a_dec\n",
" print(f\"{time_dec=}\")\n",
"\n",
" traveled_while_dec = (v_max ** 2) / (2 * a_dec)\n",
" print(f\"{traveled_while_dec=}\")\n",
"\n",
" traveled_while_cruising = delta_phi - (traveled_while_acc + traveled_while_dec)\n",
" print(f\"{traveled_while_cruising=}\")\n",
"\n",
" time_cruise = traveled_while_cruising / v_max\n",
" print(f\"{time_cruise=}\")\n",
"\n",
" time_total = time_acc + time_cruise + time_dec\n",
" print(f\"{time_total=}\")\n",
" "
]
},
{
"cell_type": "markdown",
"id": "84b206bf",
"metadata": {},
"source": [
"## Comparison 180 degrees"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b5e37028",
"metadata": {},
"outputs": [],
"source": [
"# model\n",
"print(\"Model:\")\n",
"time_taken(math.pi, 0)\n",
"\n",
"# actual\n",
"acc_start_time = 0.65\n",
"acc_stop_time = 1.45\n",
"dec_start_time = 1.45\n",
"dec_stop_time = 2.4\n",
"print(\"\\nActual:\")\n",
"print(f\"time acc: {acc_stop_time - acc_start_time}\")\n",
"print(f\"time dec: {dec_stop_time - dec_start_time}\")\n",
"print(f\"time cruising: {dec_start_time - acc_stop_time}\")\n",
"print(f\"time total: {dec_stop_time - acc_start_time}\")"
]
},
{
"cell_type": "markdown",
"id": "4b71b1c6",
"metadata": {},
"source": [
"## Comparison 360 degrees"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a286ed66",
"metadata": {},
"outputs": [],
"source": [
"# model\n",
"print(\"Model:\")\n",
"time_taken(math.tau, 0)\n",
"\n",
"# actual\n",
"acc_start_time = 0.65\n",
"acc_stop_time = 1.45\n",
"dec_start_time = 2.3\n",
"dec_stop_time = 3.25\n",
"print(\"\\nActual:\")\n",
"print(f\"time acc: {acc_stop_time - acc_start_time}\")\n",
"print(f\"time dec: {dec_stop_time - dec_start_time}\")\n",
"print(f\"time cruising: {dec_start_time - acc_stop_time}\")\n",
"print(f\"time total: {dec_stop_time - acc_start_time}\")"
]
},
{
"cell_type": "markdown",
"id": "4e808bfe",
"metadata": {},
"source": [
"## Comparison 540 degrees"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6b10a04f",
"metadata": {},
"outputs": [],
"source": [
"\n",
"# model\n",
"print(\"Model:\")\n",
"time_taken(3 * math.pi, 0)\n",
"\n",
"# actual\n",
"acc_start_time = 0.65\n",
"acc_stop_time = 1.45\n",
"dec_start_time = 3.2\n",
"dec_stop_time = 4.15\n",
"print(\"\\nActual:\")\n",
"print(f\"time acc: {acc_stop_time - acc_start_time}\")\n",
"print(f\"time dec: {dec_stop_time - dec_start_time}\")\n",
"print(f\"time cruising: {dec_start_time - acc_stop_time}\")\n",
"print(f\"time total: {dec_stop_time - acc_start_time}\")"
]
} }
], ],
"metadata": { "metadata": {
"interpreter": {
"hash": "fce917ee5987da4ee7f9f72fa808934308e0fb8739a0828f8cbf1cedb5699222"
},
"kernelspec": { "kernelspec": {
"display_name": "Python 3 (ipykernel)", "display_name": "Python 3 (ipykernel)",
"language": "python", "language": "python",

Binary file not shown.

View File

@@ -1,14 +0,0 @@
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb

View File

@@ -1,17 +0,0 @@
[package]
name = "beat_detection"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
pulse = { version = "2.23.1", package = "libpulse-binding" }
psimple = { version = "2.23.0", package = "libpulse-simple-binding" }
anyhow = "1.0.44"
sdl2 = "0.35.1"
ringbuffer = "0.8.3"
num = "0.4.0"
serialport = "4.0.1"
palette = "0.6.0"
rand = "0.8.4"

View File

@@ -1,95 +0,0 @@
use anyhow::{anyhow, Result};
use psimple::Simple;
use pulse::context::{Context, FlagSet as ContextFlagSet};
use pulse::mainloop::standard::{IterateResult, Mainloop};
use pulse::sample::Spec;
use pulse::stream::Direction;
use std::cell::RefCell;
use std::ops::Deref;
use std::rc::Rc;
/*
Some manual poking around in PulseAudio to get the name of the default sink
*/
fn poll_mainloop(mainloop: &Rc<RefCell<Mainloop>>) -> Result<()> {
match mainloop.borrow_mut().iterate(true) {
IterateResult::Quit(_) | IterateResult::Err(_) => {
return Err(anyhow!("Iterate state was not success"));
}
IterateResult::Success(_) => {
return Ok(());
}
}
}
fn get_pulse_default_sink() -> Result<String> {
let mainloop = Rc::new(RefCell::new(
Mainloop::new().ok_or(anyhow!("Failed to create mainloop"))?,
));
let ctx = Rc::new(RefCell::new(
Context::new(mainloop.borrow().deref(), "gib_default_sink")
.ok_or(anyhow!("Failed to create context"))?,
));
ctx.borrow_mut()
.connect(None, ContextFlagSet::NOFLAGS, None)?;
// Wait for context to be ready
loop {
poll_mainloop(&mainloop)?;
match ctx.borrow().get_state() {
pulse::context::State::Ready => {
break;
}
pulse::context::State::Failed | pulse::context::State::Terminated => {
return Err(anyhow!("Context was in failed/terminated state"));
}
_ => {}
}
}
let result = Rc::new(RefCell::new(None));
let cb_result_ref = result.clone();
ctx.borrow().introspect().get_server_info(move |info| {
*cb_result_ref.borrow_mut() = if let Some(ref sink_name) = info.default_sink_name {
Some(Ok(sink_name.to_string()))
} else {
Some(Err(()))
}
});
loop {
if let Some(result) = result.borrow().deref() {
return result
.to_owned()
.map_err(|_| anyhow!("Default sink name was empty"));
}
poll_mainloop(&mainloop)?;
}
}
/*
Get a PASimple instance which reads from the default sink monitor
*/
pub fn get_audio_reader(spec: &Spec) -> Result<Simple> {
let mut default_sink_name = get_pulse_default_sink()?;
default_sink_name.push_str(".monitor");
let simple = Simple::new(
None,
"piano_thingy",
Direction::Record,
Some(&default_sink_name),
"sample_yoinker",
&spec,
None,
None,
)?;
Ok(simple)
}

View File

@@ -1,113 +0,0 @@
use std::{
io,
sync::{Arc, Mutex},
thread,
time::{Duration, Instant},
};
use anyhow::{anyhow, Result};
use serialport::SerialPort;
use crate::fixtures::{DMXFixture, MovingHead};
const FPS: u32 = 50;
enum MCUResponse {
Sync,
Ack,
#[allow(dead_code)]
Info {
num_pkts: u32,
},
}
fn poll_response(ser: &mut dyn SerialPort) -> Result<MCUResponse> {
let mut read_buffer = vec![0u8; 32];
let bytes_read;
loop {
match ser.read(read_buffer.as_mut_slice()) {
Ok(t) => {
bytes_read = t;
break;
}
Err(ref e) if e.kind() == io::ErrorKind::TimedOut => continue,
Err(e) => Err(e),
}?
}
let response = std::str::from_utf8(&read_buffer[..bytes_read])?;
match response.trim() {
"Sync." => Ok(MCUResponse::Sync),
"Ack." => Ok(MCUResponse::Ack),
s if s.starts_with("Info") => Ok(MCUResponse::Info { num_pkts: 69 }),
s => Err(anyhow!("Unknown response: \"{}\"", s)),
}
}
pub fn controller_thread(
running: Arc<Mutex<bool>>,
movingheads: Arc<Mutex<[MovingHead; 4]>>,
) -> Result<()> {
let frame_time = Duration::from_secs_f64(1.0 / FPS as f64);
let mut dmx_buffer = [0u8; 512];
let mut ser = serialport::new("/dev/ttyUSB0", 500_000)
.timeout(Duration::from_millis(10))
.open()?;
// wait for initial sync
loop {
match poll_response(&mut *ser) {
Ok(MCUResponse::Sync) => break,
_ => continue,
}
}
'main: loop {
{
let running = running.lock().unwrap();
if !*running {
break Ok(());
}
}
let loop_start = Instant::now();
{
let movingheads = movingheads.lock().unwrap();
for head in movingheads.iter() {
head.render(&mut dmx_buffer);
}
}
let write_result = ser.write(&dmx_buffer);
if write_result.is_err() {
loop {
match poll_response(&mut *ser) {
Ok(MCUResponse::Sync) => continue 'main,
_ => continue,
}
}
}
loop {
match poll_response(&mut *ser) {
Ok(MCUResponse::Ack) => break,
Ok(MCUResponse::Info { .. }) | Err(_) => continue,
Ok(MCUResponse::Sync) => continue 'main,
}
}
let loop_time = loop_start.elapsed();
if loop_time < frame_time {
thread::sleep(frame_time - loop_time);
} else {
println!("loop took too long!");
}
}
}

View File

@@ -1,71 +0,0 @@
/*
Taken from Till's magic Arduino Sketch
*/
pub trait ZTransformFilter {
fn process(&mut self, sample: f32) -> f32;
}
// 20 - 200Hz Single Pole Bandpass IIR Filter
#[derive(Default)]
pub struct BassFilter {
xv: [f32; 3],
yv: [f32; 3],
}
impl ZTransformFilter for BassFilter {
fn process(&mut self, sample: f32) -> f32 {
self.xv[0] = self.xv[1];
self.xv[1] = self.xv[2];
self.xv[2] = sample / 3.0f32;
self.yv[0] = self.yv[1];
self.yv[1] = self.yv[2];
self.yv[2] = (self.xv[2] - self.xv[0])
+ (-0.7960060012f32 * self.yv[0])
+ (1.7903124146f32 * self.yv[1]);
self.yv[2]
}
}
// 10Hz Single Pole Lowpass IIR Filter
#[derive(Default)]
pub struct EnvelopeFilter {
xv: [f32; 2],
yv: [f32; 2],
}
impl ZTransformFilter for EnvelopeFilter {
fn process(&mut self, sample: f32) -> f32 {
self.xv[0] = self.xv[1];
self.xv[1] = sample / 50.0f32;
self.yv[0] = self.yv[1];
self.yv[1] = (self.xv[0] + self.xv[1]) + (0.9875119299f32 * self.yv[0]);
self.yv[1]
}
}
// 1.7 - 3.0Hz Single Pole Bandpass IIR Filter
#[derive(Default)]
pub struct BeatFilter {
xv: [f32; 3],
yv: [f32; 3],
}
impl ZTransformFilter for BeatFilter {
fn process(&mut self, sample: f32) -> f32 {
self.xv[0] = self.xv[1];
self.xv[1] = self.xv[2];
self.xv[2] = sample / 2.7f32;
self.yv[0] = self.yv[1];
self.yv[1] = self.yv[2];
self.yv[2] = (self.xv[2] - self.xv[0])
+ (-0.7169861741f32 * self.yv[0])
+ (1.4453653501f32 * self.yv[1]);
self.yv[2]
}
}

View File

@@ -1,53 +0,0 @@
use std::f32::consts::{FRAC_PI_2, PI};
use crate::util::rescale;
pub struct MovingHead {
start_addr: usize,
pub pan: f32, // -3pi/2 to 3pi/2
pub tilt: f32, // -pi/2 to pi/2
pub dimmer: f32, // 0 to 1
pub rgbw: (u8, u8, u8, u8),
pub speed: u8, // reversed
}
impl MovingHead {
pub fn new(start_addr: usize) -> Self {
Self {
start_addr,
pan: 0f32,
tilt: 0f32,
speed: 0u8,
dimmer: 0f32,
rgbw: (0u8, 0u8, 0u8, 0u8),
}
}
}
pub trait DMXFixture {
fn render(&self, dst: &mut [u8]);
}
impl DMXFixture for MovingHead {
fn render(&self, dst: &mut [u8]) {
let pan = rescale(self.pan, (-1.5 * PI, 1.5 * PI), (255.0, 0.0)) as u8;
let pan_fine = 0;
let tilt = rescale(self.tilt, (-1.0 * FRAC_PI_2, FRAC_PI_2), (0.0, 255.0)) as u8;
let tilt_fine = 0;
let dimmer = (7 + (127.0 * self.dimmer) as u8).clamp(7, 134);
let (r, g, b, w) = self.rgbw;
let offset = self.start_addr - 1;
let channels = [
pan, pan_fine, tilt, tilt_fine, self.speed, dimmer, r, g, b, w, 0, 0, 0, 0,
];
dst[offset..offset + channels.len()].copy_from_slice(&channels);
}
}

View File

@@ -1,288 +0,0 @@
use std::{
sync::{Arc, Mutex},
thread::{self, JoinHandle},
};
use anyhow::Result;
use pulse::sample::{Format, Spec};
use ringbuffer::{ConstGenericRingBuffer, RingBufferExt, RingBufferWrite};
use sdl2::{
event::Event,
keyboard::Keycode,
pixels::Color,
rect::{Point, Rect},
};
use crate::{
capture,
dsp::{self, ZTransformFilter},
};
use super::OutputLayer;
const SAMPLE_RATE: usize = 5000;
const PULSE_UPDATES_PER_SECOND: usize = 50;
const BUFFER_SIZE: usize = SAMPLE_RATE / PULSE_UPDATES_PER_SECOND;
const POINT_COUNT: usize = SAMPLE_RATE / 200;
const POINT_BUFFER_SIZE: usize = POINT_COUNT.next_power_of_two();
const MIN_MAX_SAMPLE_COUNT: usize = 10;
pub struct BeatDetectinator {
join_handle: Option<JoinHandle<Result<()>>>,
shared_state: Arc<Mutex<BeatDetectinatorSharedState>>,
}
struct BeatDetectinatorSharedState {
running: bool,
brightness: f32,
threshold: f32,
point_buf: ConstGenericRingBuffer<f32, POINT_BUFFER_SIZE>,
}
impl BeatDetectinator {
pub fn new() -> Self {
let shared_state = Arc::new(Mutex::new(BeatDetectinatorSharedState {
running: true,
brightness: 0.0,
threshold: 1.5,
point_buf: Default::default(),
}));
{
shared_state.lock().unwrap().point_buf.fill_default();
}
let join_handle = {
let shared_state = shared_state.clone();
Some(thread::spawn(move || audio_loop(shared_state)))
};
println!("Audio thread started.");
Self {
join_handle,
shared_state,
}
}
}
impl Drop for BeatDetectinator {
fn drop(&mut self) {
{
self.shared_state.lock().unwrap().running = false;
}
match self.join_handle.take().unwrap().join().unwrap() {
Ok(_) => println!("Audio thread stopped."),
Err(e) => println!("Audio thread died: {:?}", e),
}
}
}
fn audio_loop(shared_state: Arc<Mutex<BeatDetectinatorSharedState>>) -> Result<()> {
let spec = Spec {
format: Format::F32le,
rate: SAMPLE_RATE as u32,
channels: 1,
};
assert!(spec.is_valid());
let reader = capture::get_audio_reader(&spec)?;
let mut buffer = [0u8; 4 * BUFFER_SIZE];
let mut bass_filter = dsp::BassFilter::default();
let mut envelope_filter = dsp::EnvelopeFilter::default();
let mut beat_filter = dsp::BeatFilter::default();
let mut j = 0;
loop {
{
if shared_state.lock().unwrap().running == false {
break Ok(());
}
}
reader.read(&mut buffer)?;
for i in 0..BUFFER_SIZE {
let mut float_bytes = [0u8; 4];
float_bytes.copy_from_slice(&buffer[4 * i..4 * i + 4]);
j += 1;
let sample = f32::from_le_bytes(float_bytes);
let mut value = bass_filter.process(sample);
if value < 0f32 {
value = -value;
}
let envelope = envelope_filter.process(value);
if j == 200 {
let beat = beat_filter.process(envelope);
shared_state.lock().unwrap().point_buf.push(beat);
j = 0;
}
}
}
}
impl OutputLayer for BeatDetectinator {
fn tick(&mut self, _dt: std::time::Duration) {}
fn draw_sdl(&self, canvas: &mut sdl2::render::Canvas<sdl2::video::Window>, texture_size: u32) {
let min_y = 0f32;
let max_y = 3f32;
let beat = {
self.shared_state
.lock()
.unwrap()
.point_buf
.back()
.unwrap()
.clone()
};
let get_y = |y: &f32| {
let mut y = y.clone();
if y <= 0f32 {
y = 0f32;
}
y = (1f32 + y).log2();
let y = (y - min_y) / (max_y - min_y);
((1f32 - y) * texture_size as f32) as u32
};
// background
let v;
let (threshold, points) = {
let mut shared_state = self.shared_state.lock().unwrap();
shared_state.brightness = if beat > shared_state.threshold {
1f32
} else {
0.75f32 * shared_state.brightness
};
v = (255f32 * shared_state.brightness) as u8;
(
shared_state.threshold.clone(),
shared_state.point_buf.to_vec(),
)
};
canvas.set_draw_color(Color::RGB(v, v, v));
canvas.clear();
// zero
canvas.set_draw_color(Color::RGB(0, 128, 0));
let y = get_y(&0f32);
canvas
.draw_line(
Point::new(0, y as i32),
Point::new(texture_size as i32, y as i32),
)
.unwrap();
// min / max lines
canvas.set_draw_color(Color::RGB(255, 0, 0));
let min_beat = points
.iter()
.skip(points.len() - MIN_MAX_SAMPLE_COUNT)
.reduce(|a, b| if a < b { a } else { b })
.unwrap();
let x = texture_size - MIN_MAX_SAMPLE_COUNT as u32 * 10;
let y = get_y(min_beat);
canvas
.draw_line(
Point::new(x as i32, y as i32),
Point::new(texture_size as i32, y as i32),
)
.unwrap();
let max_beat = points
.iter()
.skip(points.len() - MIN_MAX_SAMPLE_COUNT)
.reduce(|a, b| if a > b { a } else { b })
.unwrap();
let y = get_y(max_beat);
canvas
.draw_line(
Point::new(x as i32, y as i32),
Point::new(texture_size as i32, y as i32),
)
.unwrap();
// threshhold line
canvas.set_draw_color(Color::RGB(0, 0, 255));
let y = get_y(&threshold);
canvas
.draw_line(
Point::new(0, y as i32),
Point::new(texture_size as i32, y as i32),
)
.unwrap();
// values
canvas.set_draw_color(Color::RGB(0, 255, 0));
for (i, beat) in points.iter().skip(points.len() - POINT_COUNT).enumerate() {
let x = 10 * i;
let y = get_y(beat);
canvas
.fill_rect(Rect::new((x + 1) as i32, (y - 1) as i32, 8, 3))
.unwrap();
}
}
fn on_sdl_event(&mut self, event: &Event) {
match event {
Event::KeyDown {
keycode: Some(Keycode::Up),
..
} => {
let mut shared_state = self.shared_state.lock().unwrap();
shared_state.threshold += 0.01f32;
println!("threshold: {:.2}", shared_state.threshold);
}
Event::KeyDown {
keycode: Some(Keycode::Down),
..
} => {
let mut shared_state = self.shared_state.lock().unwrap();
shared_state.threshold -= 0.01f32;
println!("threshold: {:.2}", shared_state.threshold);
}
_ => {}
}
}
fn update_dmx(&self, output: &mut [crate::fixtures::MovingHead; 4]) {
let shared_state = self.shared_state.lock().unwrap();
for head in output.iter_mut() {
head.dimmer = 0.2 + 0.8 * shared_state.brightness;
}
}
}

View File

@@ -1,24 +0,0 @@
use super::OutputLayer;
pub struct ConstantBrightness {
pub brightness: f32,
}
impl OutputLayer for ConstantBrightness {
fn tick(&mut self, _dt: std::time::Duration) {}
fn draw_sdl(
&self,
_canvas: &mut sdl2::render::Canvas<sdl2::video::Window>,
_texture_size: u32,
) {
}
fn on_sdl_event(&mut self, _event: &sdl2::event::Event) {}
fn update_dmx(&self, output: &mut [crate::fixtures::MovingHead; 4]) {
for head in output.iter_mut() {
head.dimmer = self.brightness;
}
}
}

View File

@@ -1,53 +0,0 @@
use std::time::Duration;
use palette::{Hsl, IntoColor, Pixel, Srgb};
use sdl2::{pixels::Color, rect::Rect};
use crate::layers::OutputLayer;
pub struct HSLCycle {
cycle_millis: usize,
time: usize,
rgb: [u8; 3],
}
impl HSLCycle {
pub fn new(cycle_length: Duration) -> Self {
Self {
cycle_millis: cycle_length.as_millis() as usize,
time: 0,
rgb: [0, 0, 0],
}
}
}
impl OutputLayer for HSLCycle {
fn tick(&mut self, dt: std::time::Duration) {
self.time += dt.as_millis() as usize;
self.time %= self.cycle_millis;
let hsl: Srgb = Hsl::new(
360.0 * (self.time as f32 / self.cycle_millis as f32),
1.0,
0.5,
)
.into_color();
self.rgb = hsl.into_format().into_raw();
}
fn draw_sdl(&self, canvas: &mut sdl2::render::Canvas<sdl2::video::Window>, _texture_size: u32) {
let [r, g, b] = self.rgb;
canvas.set_draw_color(Color::RGB(r, g, b));
canvas.fill_rect(Rect::new(0, 0, 5, 5)).unwrap();
}
fn on_sdl_event(&mut self, _event: &sdl2::event::Event) {}
fn update_dmx(&self, output: &mut [crate::fixtures::MovingHead; 4]) {
let [r, g, b] = self.rgb;
for head in output.iter_mut() {
head.rgbw = (r, g, b, 0);
}
}
}

View File

@@ -1,21 +0,0 @@
pub mod beat_detector;
pub mod constant_brightness;
pub mod hsl_cycle;
pub mod movement;
pub mod tap;
use std::time::Duration;
use sdl2::{event::Event, render::Canvas, video::Window};
use crate::fixtures::MovingHead;
pub trait OutputLayer {
fn tick(&mut self, dt: Duration);
fn draw_sdl(&self, canvas: &mut Canvas<Window>, texture_size: u32);
fn on_sdl_event(&mut self, event: &Event);
fn update_dmx(&self, output: &mut [MovingHead; 4]);
}

View File

@@ -1,78 +0,0 @@
use std::{
f32::consts::PI,
time::{Duration, Instant},
};
use rand::random;
use sdl2::{event::Event, keyboard::Keycode};
use crate::util::rescale;
use super::OutputLayer;
pub struct HeadMover {
movement_duration: Duration,
last_timestamp: Instant,
targets: [(f32, f32); 4],
}
impl HeadMover {
pub fn new(movement_duration: Duration) -> Self {
Self {
movement_duration,
last_timestamp: Instant::now() - movement_duration,
targets: Default::default(),
}
}
}
fn sample_random_point() -> (f32, f32) {
let pan = rescale(random::<f32>(), (0.0, 1.0), (-0.5 * PI, 0.5 * PI));
let tilt = random::<f32>().acos();
(pan, tilt)
}
impl OutputLayer for HeadMover {
fn tick(&mut self, _dt: Duration) {
let now = Instant::now();
if now - self.last_timestamp >= self.movement_duration {
for target in self.targets.iter_mut() {
*target = sample_random_point();
}
self.last_timestamp = now;
}
}
fn draw_sdl(
&self,
_canvas: &mut sdl2::render::Canvas<sdl2::video::Window>,
_texture_size: u32,
) {
}
fn on_sdl_event(&mut self, event: &sdl2::event::Event) {
match event {
Event::KeyDown {
keycode: Some(Keycode::Return),
..
} => {
let now = Instant::now();
for target in self.targets.iter_mut() {
*target = sample_random_point();
}
self.last_timestamp = now;
}
_ => {}
}
}
fn update_dmx(&self, output: &mut [crate::fixtures::MovingHead; 4]) {
for (head, target) in output.iter_mut().zip(self.targets.iter()) {
let (pan, tilt) = target;
head.pan = *pan;
head.tilt = *tilt;
}
}
}

View File

@@ -1,102 +0,0 @@
use std::{
collections::VecDeque,
time::{Duration, Instant},
};
use sdl2::{event::Event, keyboard::Keycode, pixels::Color};
use super::OutputLayer;
pub struct TapMetronome {
max_time: Duration,
timestamps: VecDeque<Instant>,
seconds_per_beat: Option<f32>,
brightness: f32,
decay: f32,
}
impl TapMetronome {
pub fn new(max_time: Duration) -> Self {
Self {
max_time,
timestamps: VecDeque::new(),
seconds_per_beat: None,
brightness: 0.0,
decay: 1.0,
}
}
}
impl OutputLayer for TapMetronome {
fn tick(&mut self, _dt: std::time::Duration) {
let now = Instant::now();
if let (Some(stamp), Some(spb)) = (self.timestamps.back(), self.seconds_per_beat) {
let dt = (now - *stamp).as_secs_f32();
let beat_offset = dt % spb;
self.brightness = (-1.0 * self.decay * beat_offset).exp()
} else {
self.brightness = 0.0;
}
}
fn draw_sdl(&self, canvas: &mut sdl2::render::Canvas<sdl2::video::Window>, _texture_size: u32) {
let v = (255.0 * self.brightness) as u8;
canvas.set_draw_color(Color::RGB(v, v, v));
canvas.clear();
}
fn on_sdl_event(&mut self, event: &sdl2::event::Event) {
match event {
Event::KeyDown {
keycode: Some(Keycode::Space),
..
} => {
let now = Instant::now();
while let Some(stamp) = self.timestamps.front() {
if now - *stamp < self.max_time {
break;
}
self.timestamps.pop_front();
}
self.timestamps.push_back(now);
if self.timestamps.len() >= 2 {
let dt = *self.timestamps.back().unwrap() - *self.timestamps.front().unwrap();
let spb = dt.as_secs_f32() / (self.timestamps.len() - 1) as f32;
println!("Detected BPM: {:.2}", 60.0 / spb);
self.seconds_per_beat = Some(spb);
} else {
self.seconds_per_beat = None;
}
}
Event::KeyDown {
keycode: Some(Keycode::Down),
..
} => {
self.decay += 0.1;
println!("Decay: {:.3}", self.decay)
}
Event::KeyDown {
keycode: Some(Keycode::Up),
..
} => {
self.decay -= 0.1;
println!("Decay: {:.3}", self.decay)
}
_ => {}
}
}
fn update_dmx(&self, output: &mut [crate::fixtures::MovingHead; 4]) {
for head in output.iter_mut() {
head.dimmer = 0.2 + 0.8 * self.brightness;
}
}
}

View File

@@ -1,183 +0,0 @@
mod capture;
mod dmx_controller;
mod dsp;
mod fixtures;
mod layers;
mod util;
use std::{
sync::{Arc, Mutex},
thread,
time::Duration,
};
use anyhow::Result;
use layers::{
beat_detector::BeatDetectinator, constant_brightness::ConstantBrightness, hsl_cycle::HSLCycle,
movement::HeadMover, tap::TapMetronome, OutputLayer,
};
use sdl2::{event::Event, keyboard::Keycode, pixels::Color, rect::Rect};
use crate::dmx_controller::controller_thread;
use crate::fixtures::MovingHead;
const FPS: usize = 50;
enum BrightnessControl {
BeatDetection,
TapMetronome,
ConstantBrightness,
}
fn main() -> Result<()> {
// dmx thread
let dmx_running = Arc::new(Mutex::new(true));
let movingheads = Arc::new(Mutex::new([
MovingHead::new(1),
MovingHead::new(15),
MovingHead::new(29),
MovingHead::new(43),
]));
let dmx_thread = {
let dmx_running = dmx_running.clone();
let movingheads = movingheads.clone();
thread::spawn(move || controller_thread(dmx_running, movingheads))
};
// output layers
let mut beat_detector = BeatDetectinator::new();
let mut tap_metronome = TapMetronome::new(Duration::from_secs(2));
let mut constant_brightness = ConstantBrightness { brightness: 1.0 };
let mut active_brightness_control = BrightnessControl::BeatDetection;
let mut hsl_cycle = HSLCycle::new(Duration::from_secs(5));
let mut head_mover = HeadMover::new(Duration::from_secs_f32(0.5));
// sdl
let sdl = sdl2::init().unwrap();
let sdl_video = sdl.video().unwrap();
let window = sdl_video
.window("Beat Detectinator", 800, 600)
.resizable()
.build()?;
let mut canvas = window.into_canvas().build()?;
let texture_creator = canvas.texture_creator();
let texture_size = 250;
let mut texture = texture_creator.create_texture_target(
canvas.default_pixel_format(),
texture_size,
texture_size,
)?;
let frame_duration = Duration::from_secs_f64(1.0 / FPS as f64);
let mut event_pump = sdl.event_pump().unwrap();
'running: loop {
for event in event_pump.poll_iter() {
match event {
Event::Quit { .. }
| Event::KeyDown {
keycode: Some(Keycode::Escape),
..
} => break 'running,
Event::KeyDown {
keycode: Some(Keycode::F1),
..
} => {
active_brightness_control = BrightnessControl::BeatDetection;
println!("Using automatic beat detection.")
}
Event::KeyDown {
keycode: Some(Keycode::F2),
..
} => {
active_brightness_control = BrightnessControl::TapMetronome;
println!("Using tap metronome.")
}
Event::KeyDown {
keycode: Some(Keycode::F3),
..
} => {
active_brightness_control = BrightnessControl::ConstantBrightness;
println!("Using constant brightness.")
}
event => {
match active_brightness_control {
BrightnessControl::BeatDetection => beat_detector.on_sdl_event(&event),
BrightnessControl::TapMetronome => tap_metronome.on_sdl_event(&event),
BrightnessControl::ConstantBrightness => {
constant_brightness.on_sdl_event(&event)
}
}
hsl_cycle.on_sdl_event(&event);
head_mover.on_sdl_event(&event);
}
}
}
beat_detector.tick(frame_duration);
tap_metronome.tick(frame_duration);
hsl_cycle.tick(frame_duration);
head_mover.tick(frame_duration);
{
let mut output = movingheads.lock().unwrap();
match active_brightness_control {
BrightnessControl::BeatDetection => beat_detector.update_dmx(&mut *output),
BrightnessControl::TapMetronome => tap_metronome.update_dmx(&mut *output),
BrightnessControl::ConstantBrightness => {
constant_brightness.update_dmx(&mut *output)
}
}
hsl_cycle.update_dmx(&mut *output);
head_mover.update_dmx(&mut *output);
}
canvas.set_draw_color(Color::RGB(0, 0, 0));
canvas.clear();
canvas.with_texture_canvas(&mut texture, |canvas| {
match active_brightness_control {
BrightnessControl::BeatDetection => beat_detector.draw_sdl(canvas, texture_size),
BrightnessControl::TapMetronome => tap_metronome.draw_sdl(canvas, texture_size),
BrightnessControl::ConstantBrightness => {
constant_brightness.draw_sdl(canvas, texture_size)
}
}
hsl_cycle.draw_sdl(canvas, texture_size);
head_mover.draw_sdl(canvas, texture_size);
})?;
let (w, h) = canvas.window().drawable_size();
let target_rect = if w > h {
Rect::new(((w - h) / 2) as i32, 0, h, h)
} else {
Rect::new(0, ((h - w) / 2) as i32, w, w)
};
canvas.copy(&texture, None, Some(target_rect)).unwrap();
canvas.present();
thread::sleep(frame_duration);
}
{
let mut dmx_running = dmx_running.lock().unwrap();
*dmx_running = false;
}
dmx_thread.join().unwrap()?;
Ok(())
}

View File

@@ -1,8 +0,0 @@
use num::traits::float::Float;
pub fn rescale<T: Float>(x: T, from: (T, T), to: (T, T)) -> T {
let (a, b) = from;
let (c, d) = to;
c + (d - c) * (x - a) / (b - a)
}

View File

@@ -1,764 +0,0 @@
# Blender v2.83.5 OBJ File: ''
# www.blender.org
mtllib untitled.mtl
o base_Cube.001
v 0.069532 0.101673 0.035000
v 0.069532 0.111109 0.035000
v 0.069532 0.101673 -0.035000
v 0.069532 0.111109 -0.035000
v 0.058653 0.101673 -0.035000
v 0.058653 0.111109 -0.035000
v 0.058653 0.101673 0.035000
v 0.058653 0.111109 0.035000
v 0.058653 0.189265 -0.035000
v 0.069532 0.189265 -0.035000
v 0.069532 0.189265 0.035000
v 0.058653 0.189265 0.035000
v -0.069532 0.101673 0.035000
v -0.069532 0.111109 0.035000
v -0.069532 0.101673 -0.035000
v -0.069532 0.111109 -0.035000
v -0.058653 0.101673 -0.035000
v -0.058653 0.111109 -0.035000
v -0.058653 0.101673 0.035000
v -0.058653 0.111109 0.035000
v -0.058653 0.189265 -0.035000
v -0.069532 0.189265 -0.035000
v -0.069532 0.189265 0.035000
v -0.058653 0.189265 0.035000
v 0.000000 0.101673 -0.035000
v 0.000000 0.111109 0.035000
v 0.000000 0.111109 -0.035000
v 0.000000 0.101673 0.035000
v 0.000000 0.000000 -0.080000
v 0.000000 0.092347 -0.080000
v 0.015607 0.000000 -0.078463
v 0.015607 0.092347 -0.078463
v 0.030615 0.000000 -0.073910
v 0.030615 0.092347 -0.073910
v 0.044446 0.000000 -0.066518
v 0.044446 0.092347 -0.066518
v 0.056569 0.000000 -0.056569
v 0.056569 0.092347 -0.056569
v 0.066518 0.000000 -0.044446
v 0.066518 0.092347 -0.044446
v 0.073910 0.000000 -0.030615
v 0.073910 0.092347 -0.030615
v 0.078463 0.000000 -0.015607
v 0.078463 0.092347 -0.015607
v 0.080000 0.000000 -0.000000
v 0.080000 0.092347 -0.000000
v 0.078463 0.000000 0.015607
v 0.078463 0.092347 0.015607
v 0.073910 0.000000 0.030615
v 0.073910 0.092347 0.030615
v 0.066518 0.000000 0.044446
v 0.066518 0.092347 0.044446
v 0.056569 0.000000 0.056569
v 0.056569 0.092347 0.056569
v 0.044446 0.000000 0.066518
v 0.044446 0.092347 0.066518
v 0.030615 0.000000 0.073910
v 0.030615 0.092347 0.073910
v 0.015607 0.000000 0.078463
v 0.015607 0.092347 0.078463
v -0.000000 0.000000 0.080000
v -0.000000 0.092347 0.080000
v -0.015607 0.000000 0.078463
v -0.015607 0.092347 0.078463
v -0.030615 0.000000 0.073910
v -0.030615 0.092347 0.073910
v -0.044446 0.000000 0.066518
v -0.044446 0.092347 0.066518
v -0.056569 0.000000 0.056569
v -0.056569 0.092347 0.056569
v -0.066518 0.000000 0.044446
v -0.066518 0.092347 0.044446
v -0.073910 0.000000 0.030615
v -0.073910 0.092347 0.030615
v -0.078463 0.000000 0.015607
v -0.078463 0.092347 0.015607
v -0.080000 0.000000 -0.000000
v -0.080000 0.092347 -0.000000
v -0.078463 0.000000 -0.015607
v -0.078463 0.092347 -0.015607
v -0.073910 0.000000 -0.030615
v -0.073910 0.092347 -0.030615
v -0.066518 0.000000 -0.044446
v -0.066518 0.092347 -0.044446
v -0.056568 0.000000 -0.056569
v -0.056568 0.092347 -0.056569
v -0.044446 0.000000 -0.066518
v -0.044446 0.092347 -0.066518
v -0.030615 0.000000 -0.073910
v -0.030615 0.092347 -0.073910
v -0.015607 0.000000 -0.078463
v -0.015607 0.092347 -0.078463
vt 0.375000 0.461557
vt 0.625000 0.461557
vt 0.625000 0.500000
vt 0.375000 0.500000
vt 0.625000 0.750000
vt 0.375000 0.750000
vt 0.336557 0.500000
vt 0.336557 0.750000
vt 0.625000 0.461557
vt 0.625000 0.500000
vt 0.129294 0.500000
vt 0.129294 0.750000
vt 0.625000 0.788443
vt 0.375000 0.788443
vt 0.375000 0.254294
vt 0.625000 0.254294
vt 0.663443 0.500000
vt 0.663443 0.750000
vt 0.625000 0.750000
vt 0.663443 0.500000
vt 0.663443 0.750000
vt 0.625000 0.788443
vt 0.625000 0.995706
vt 0.375000 0.995706
vt 0.870706 0.500000
vt 0.870706 0.750000
vt 0.375000 0.461557
vt 0.375000 0.500000
vt 0.625000 0.500000
vt 0.625000 0.461557
vt 0.375000 0.750000
vt 0.625000 0.750000
vt 0.336557 0.500000
vt 0.336557 0.750000
vt 0.625000 0.500000
vt 0.625000 0.461557
vt 0.375000 0.788443
vt 0.625000 0.788443
vt 0.625000 0.750000
vt 0.663443 0.750000
vt 0.663443 0.500000
vt 0.663443 0.500000
vt 0.663443 0.750000
vt 0.625000 0.788443
vt 1.000000 0.500000
vt 1.000000 1.000000
vt 0.968750 1.000000
vt 0.968750 0.500000
vt 0.937500 1.000000
vt 0.937500 0.500000
vt 0.906250 1.000000
vt 0.906250 0.500000
vt 0.875000 1.000000
vt 0.875000 0.500000
vt 0.843750 1.000000
vt 0.843750 0.500000
vt 0.812500 1.000000
vt 0.812500 0.500000
vt 0.781250 1.000000
vt 0.781250 0.500000
vt 0.750000 1.000000
vt 0.750000 0.500000
vt 0.718750 1.000000
vt 0.718750 0.500000
vt 0.687500 1.000000
vt 0.687500 0.500000
vt 0.656250 1.000000
vt 0.656250 0.500000
vt 0.625000 1.000000
vt 0.625000 0.500000
vt 0.593750 1.000000
vt 0.593750 0.500000
vt 0.562500 1.000000
vt 0.562500 0.500000
vt 0.531250 1.000000
vt 0.531250 0.500000
vt 0.500000 1.000000
vt 0.500000 0.500000
vt 0.468750 1.000000
vt 0.468750 0.500000
vt 0.437500 1.000000
vt 0.437500 0.500000
vt 0.406250 1.000000
vt 0.406250 0.500000
vt 0.375000 1.000000
vt 0.375000 0.500000
vt 0.343750 1.000000
vt 0.343750 0.500000
vt 0.312500 1.000000
vt 0.312500 0.500000
vt 0.281250 1.000000
vt 0.281250 0.500000
vt 0.250000 1.000000
vt 0.250000 0.500000
vt 0.218750 1.000000
vt 0.218750 0.500000
vt 0.187500 1.000000
vt 0.187500 0.500000
vt 0.156250 1.000000
vt 0.156250 0.500000
vt 0.125000 1.000000
vt 0.125000 0.500000
vt 0.093750 1.000000
vt 0.093750 0.500000
vt 0.062500 1.000000
vt 0.062500 0.500000
vt 0.296822 0.485388
vt 0.250000 0.490000
vt 0.203179 0.485389
vt 0.158156 0.471731
vt 0.116663 0.449553
vt 0.080295 0.419706
vt 0.050447 0.383337
vt 0.028269 0.341844
vt 0.014612 0.296822
vt 0.010000 0.250000
vt 0.014611 0.203179
vt 0.028269 0.158156
vt 0.050447 0.116663
vt 0.080294 0.080294
vt 0.116663 0.050447
vt 0.158156 0.028269
vt 0.203178 0.014612
vt 0.250000 0.010000
vt 0.296822 0.014612
vt 0.341844 0.028269
vt 0.383337 0.050447
vt 0.419706 0.080294
vt 0.449553 0.116663
vt 0.471731 0.158156
vt 0.485388 0.203178
vt 0.490000 0.250000
vt 0.485388 0.296822
vt 0.471731 0.341844
vt 0.449553 0.383337
vt 0.419706 0.419706
vt 0.383337 0.449553
vt 0.341844 0.471731
vt 0.031250 1.000000
vt 0.031250 0.500000
vt 0.000000 1.000000
vt 0.000000 0.500000
vt 0.750000 0.490000
vt 0.796822 0.485388
vt 0.841844 0.471731
vt 0.883337 0.449553
vt 0.919706 0.419706
vt 0.949553 0.383337
vt 0.971731 0.341844
vt 0.985388 0.296822
vt 0.990000 0.250000
vt 0.985388 0.203178
vt 0.971731 0.158156
vt 0.949553 0.116663
vt 0.919706 0.080294
vt 0.883337 0.050447
vt 0.841844 0.028269
vt 0.796822 0.014612
vt 0.750000 0.010000
vt 0.703178 0.014612
vt 0.658156 0.028269
vt 0.616663 0.050447
vt 0.580294 0.080294
vt 0.550447 0.116663
vt 0.528269 0.158156
vt 0.514611 0.203179
vt 0.510000 0.250000
vt 0.514612 0.296822
vt 0.528269 0.341844
vt 0.550447 0.383337
vt 0.580295 0.419706
vt 0.616663 0.449553
vt 0.658156 0.471731
vt 0.703179 0.485389
vn 0.0000 0.0000 -1.0000
vn 1.0000 0.0000 0.0000
vn 0.0000 -1.0000 0.0000
vn 0.0000 0.0000 1.0000
vn 0.0000 1.0000 0.0000
vn -1.0000 0.0000 0.0000
vn 0.0980 0.0000 -0.9952
vn 0.2903 0.0000 -0.9569
vn 0.4714 0.0000 -0.8819
vn 0.6344 0.0000 -0.7730
vn 0.7730 0.0000 -0.6344
vn 0.8819 0.0000 -0.4714
vn 0.9569 0.0000 -0.2903
vn 0.9952 0.0000 -0.0980
vn 0.9952 0.0000 0.0980
vn 0.9569 0.0000 0.2903
vn 0.8819 0.0000 0.4714
vn 0.7730 0.0000 0.6344
vn 0.6344 0.0000 0.7730
vn 0.4714 0.0000 0.8819
vn 0.2903 0.0000 0.9569
vn 0.0980 0.0000 0.9952
vn -0.0980 0.0000 0.9952
vn -0.2903 0.0000 0.9569
vn -0.4714 0.0000 0.8819
vn -0.6344 0.0000 0.7730
vn -0.7730 0.0000 0.6344
vn -0.8819 0.0000 0.4714
vn -0.9569 0.0000 0.2903
vn -0.9952 0.0000 0.0980
vn -0.9952 0.0000 -0.0980
vn -0.9569 0.0000 -0.2903
vn -0.8819 0.0000 -0.4714
vn -0.7730 0.0000 -0.6344
vn -0.6344 0.0000 -0.7730
vn -0.4714 0.0000 -0.8819
vn -0.2903 0.0000 -0.9569
vn -0.0980 0.0000 -0.9952
usemtl None
s off
f 5/1/1 6/2/1 4/3/1 3/4/1
f 3/4/2 4/3/2 2/5/2 1/6/2
f 5/7/3 3/4/3 1/6/3 7/8/3
f 4/3/1 6/2/1 9/9/1 10/10/1
f 25/11/3 5/7/3 7/8/3 28/12/3
f 1/6/4 2/5/4 8/13/4 7/14/4
f 25/15/1 27/16/1 6/2/1 5/1/1
f 10/10/5 9/17/5 12/18/5 11/19/5
f 6/20/6 8/21/6 12/18/6 9/17/6
f 2/5/2 4/3/2 10/10/2 11/19/2
f 8/13/4 2/5/4 11/19/4 12/22/4
f 7/14/4 8/13/4 26/23/4 28/24/4
f 6/20/5 27/25/5 26/26/5 8/21/5
f 17/27/1 15/28/1 16/29/1 18/30/1
f 15/28/6 13/31/6 14/32/6 16/29/6
f 17/33/3 19/34/3 13/31/3 15/28/3
f 16/29/1 22/35/1 21/36/1 18/30/1
f 25/11/3 28/12/3 19/34/3 17/33/3
f 13/31/4 19/37/4 20/38/4 14/32/4
f 25/15/1 17/27/1 18/30/1 27/16/1
f 22/35/5 23/39/5 24/40/5 21/41/5
f 18/42/2 21/41/2 24/40/2 20/43/2
f 14/32/6 23/39/6 22/35/6 16/29/6
f 20/38/4 24/44/4 23/39/4 14/32/4
f 19/37/4 28/24/4 26/23/4 20/38/4
f 18/42/5 20/43/5 26/26/5 27/25/5
f 29/45/7 30/46/7 32/47/7 31/48/7
f 31/48/8 32/47/8 34/49/8 33/50/8
f 33/50/9 34/49/9 36/51/9 35/52/9
f 35/52/10 36/51/10 38/53/10 37/54/10
f 37/54/11 38/53/11 40/55/11 39/56/11
f 39/56/12 40/55/12 42/57/12 41/58/12
f 41/58/13 42/57/13 44/59/13 43/60/13
f 43/60/14 44/59/14 46/61/14 45/62/14
f 45/62/15 46/61/15 48/63/15 47/64/15
f 47/64/16 48/63/16 50/65/16 49/66/16
f 49/66/17 50/65/17 52/67/17 51/68/17
f 51/68/18 52/67/18 54/69/18 53/70/18
f 53/70/19 54/69/19 56/71/19 55/72/19
f 55/72/20 56/71/20 58/73/20 57/74/20
f 57/74/21 58/73/21 60/75/21 59/76/21
f 59/76/22 60/75/22 62/77/22 61/78/22
f 61/78/23 62/77/23 64/79/23 63/80/23
f 63/80/24 64/79/24 66/81/24 65/82/24
f 65/82/25 66/81/25 68/83/25 67/84/25
f 67/84/26 68/83/26 70/85/26 69/86/26
f 69/86/27 70/85/27 72/87/27 71/88/27
f 71/88/28 72/87/28 74/89/28 73/90/28
f 73/90/29 74/89/29 76/91/29 75/92/29
f 75/92/30 76/91/30 78/93/30 77/94/30
f 77/94/31 78/93/31 80/95/31 79/96/31
f 79/96/32 80/95/32 82/97/32 81/98/32
f 81/98/33 82/97/33 84/99/33 83/100/33
f 83/100/34 84/99/34 86/101/34 85/102/34
f 85/102/35 86/101/35 88/103/35 87/104/35
f 87/104/36 88/103/36 90/105/36 89/106/36
f 32/107/5 30/108/5 92/109/5 90/110/5 88/111/5 86/112/5 84/113/5 82/114/5 80/115/5 78/116/5 76/117/5 74/118/5 72/119/5 70/120/5 68/121/5 66/122/5 64/123/5 62/124/5 60/125/5 58/126/5 56/127/5 54/128/5 52/129/5 50/130/5 48/131/5 46/132/5 44/133/5 42/134/5 40/135/5 38/136/5 36/137/5 34/138/5
f 89/106/37 90/105/37 92/139/37 91/140/37
f 91/140/38 92/139/38 30/141/38 29/142/38
f 29/143/3 31/144/3 33/145/3 35/146/3 37/147/3 39/148/3 41/149/3 43/150/3 45/151/3 47/152/3 49/153/3 51/154/3 53/155/3 55/156/3 57/157/3 59/158/3 61/159/3 63/160/3 65/161/3 67/162/3 69/163/3 71/164/3 73/165/3 75/166/3 77/167/3 79/168/3 81/169/3 83/170/3 85/171/3 87/172/3 89/173/3 91/174/3
o head_Cylinder.003
v 0.000000 0.128498 0.043429
v 0.000000 0.139373 -0.043429
v 0.010167 0.129500 0.043429
v 0.008046 0.140165 -0.043429
v 0.019944 0.132465 0.043429
v 0.015782 0.142512 -0.043429
v 0.028954 0.137281 0.043429
v 0.022912 0.146323 -0.043429
v 0.036851 0.143762 0.043429
v 0.029161 0.151452 -0.043429
v 0.043332 0.151660 0.043429
v 0.034290 0.157701 -0.043429
v 0.048148 0.160670 0.043429
v 0.038101 0.164831 -0.043429
v 0.051114 0.170446 0.043429
v 0.040448 0.172567 -0.043429
v 0.052115 0.180613 0.043429
v 0.041241 0.180613 -0.043429
v 0.051114 0.190780 0.043429
v 0.040448 0.188659 -0.043429
v 0.048148 0.200557 0.043429
v 0.038101 0.196395 -0.043429
v 0.043332 0.209567 0.043429
v 0.034290 0.203525 -0.043429
v 0.036851 0.217464 0.043429
v 0.029161 0.209775 -0.043429
v 0.028954 0.223945 0.043429
v 0.022912 0.214903 -0.043429
v 0.019944 0.228761 0.043429
v 0.015782 0.218714 -0.043429
v 0.010167 0.231727 0.043429
v 0.008046 0.221061 -0.043429
v -0.000000 0.232728 0.043429
v -0.000000 0.221854 -0.043429
v -0.010167 0.231727 0.043429
v -0.008046 0.221061 -0.043429
v -0.019944 0.228761 0.043429
v -0.015782 0.218714 -0.043429
v -0.028954 0.223945 0.043429
v -0.022912 0.214903 -0.043429
v -0.036851 0.217464 0.043429
v -0.029161 0.209775 -0.043429
v -0.043332 0.209567 0.043429
v -0.034290 0.203525 -0.043429
v -0.048148 0.200557 0.043429
v -0.038101 0.196395 -0.043429
v -0.051114 0.190780 0.043429
v -0.040448 0.188659 -0.043429
v -0.052115 0.180613 0.043429
v -0.041241 0.180613 -0.043429
v -0.051114 0.170446 0.043429
v -0.040448 0.172567 -0.043429
v -0.048148 0.160670 0.043429
v -0.038101 0.164831 -0.043429
v -0.043332 0.151660 0.043429
v -0.034290 0.157701 -0.043429
v -0.036851 0.143762 0.043429
v -0.029161 0.151452 -0.043429
v -0.028953 0.137281 0.043429
v -0.022912 0.146323 -0.043429
v -0.019943 0.132465 0.043429
v -0.015782 0.142512 -0.043429
v -0.010167 0.129500 0.043429
v -0.008046 0.140165 -0.043429
v 0.000000 0.128498 -0.021164
v 0.010167 0.129500 -0.021164
v 0.019944 0.132465 -0.021164
v 0.028954 0.137281 -0.021164
v 0.036851 0.143762 -0.021164
v 0.043332 0.151660 -0.021164
v 0.048148 0.160670 -0.021164
v 0.051114 0.170446 -0.021164
v 0.052115 0.180613 -0.021164
v 0.051114 0.190780 -0.021164
v 0.048148 0.200557 -0.021164
v 0.043332 0.209567 -0.021164
v 0.036851 0.217464 -0.021164
v 0.028954 0.223945 -0.021164
v 0.019944 0.228761 -0.021164
v 0.010167 0.231727 -0.021164
v -0.000000 0.232728 -0.021164
v -0.010167 0.231727 -0.021164
v -0.019944 0.228761 -0.021164
v -0.028954 0.223945 -0.021164
v -0.036851 0.217464 -0.021164
v -0.043332 0.209567 -0.021164
v -0.048148 0.200557 -0.021164
v -0.051114 0.190780 -0.021164
v -0.052115 0.180613 -0.021164
v -0.051114 0.170446 -0.021164
v -0.048148 0.160670 -0.021164
v -0.043332 0.151660 -0.021164
v -0.036851 0.143762 -0.021164
v -0.028953 0.137281 -0.021164
v -0.019943 0.132465 -0.021164
v -0.010167 0.129500 -0.021164
vt 1.000000 0.871829
vt 1.000000 1.000000
vt 0.968750 1.000000
vt 0.968750 0.871829
vt 0.937500 1.000000
vt 0.937500 0.871829
vt 0.906250 1.000000
vt 0.906250 0.871829
vt 0.875000 1.000000
vt 0.875000 0.871829
vt 0.843750 1.000000
vt 0.843750 0.871829
vt 0.812500 1.000000
vt 0.812500 0.871829
vt 0.781250 1.000000
vt 0.781250 0.871829
vt 0.750000 1.000000
vt 0.750000 0.871829
vt 0.718750 1.000000
vt 0.718750 0.871829
vt 0.687500 1.000000
vt 0.687500 0.871829
vt 0.656250 1.000000
vt 0.656250 0.871829
vt 0.625000 1.000000
vt 0.625000 0.871829
vt 0.593750 1.000000
vt 0.593750 0.871829
vt 0.562500 1.000000
vt 0.562500 0.871829
vt 0.531250 1.000000
vt 0.531250 0.871829
vt 0.500000 1.000000
vt 0.500000 0.871829
vt 0.468750 1.000000
vt 0.468750 0.871829
vt 0.437500 1.000000
vt 0.437500 0.871829
vt 0.406250 1.000000
vt 0.406250 0.871829
vt 0.375000 1.000000
vt 0.375000 0.871829
vt 0.343750 1.000000
vt 0.343750 0.871829
vt 0.312500 1.000000
vt 0.312500 0.871829
vt 0.281250 1.000000
vt 0.281250 0.871829
vt 0.250000 1.000000
vt 0.250000 0.871829
vt 0.218750 1.000000
vt 0.218750 0.871829
vt 0.187500 1.000000
vt 0.187500 0.871829
vt 0.156250 1.000000
vt 0.156250 0.871829
vt 0.125000 1.000000
vt 0.125000 0.871829
vt 0.093750 1.000000
vt 0.093750 0.871829
vt 0.062500 1.000000
vt 0.062500 0.871829
vt 0.296822 0.485388
vt 0.250000 0.490000
vt 0.203179 0.485389
vt 0.158156 0.471731
vt 0.116663 0.449553
vt 0.080295 0.419706
vt 0.050447 0.383337
vt 0.028269 0.341844
vt 0.014612 0.296822
vt 0.010000 0.250000
vt 0.014611 0.203179
vt 0.028269 0.158156
vt 0.050447 0.116663
vt 0.080294 0.080295
vt 0.116663 0.050447
vt 0.158156 0.028269
vt 0.203178 0.014612
vt 0.250000 0.010000
vt 0.296822 0.014612
vt 0.341844 0.028269
vt 0.383337 0.050447
vt 0.419706 0.080294
vt 0.449553 0.116663
vt 0.471731 0.158156
vt 0.485388 0.203178
vt 0.490000 0.250000
vt 0.485388 0.296822
vt 0.471731 0.341844
vt 0.449553 0.383337
vt 0.419706 0.419706
vt 0.383337 0.449553
vt 0.341844 0.471731
vt 0.031250 1.000000
vt 0.031250 0.871829
vt 0.000000 1.000000
vt 0.000000 0.871829
vt 0.750000 0.490000
vt 0.796822 0.485388
vt 0.841844 0.471731
vt 0.883337 0.449553
vt 0.919706 0.419706
vt 0.949553 0.383337
vt 0.971731 0.341844
vt 0.985388 0.296822
vt 0.990000 0.250000
vt 0.985388 0.203178
vt 0.971731 0.158156
vt 0.949553 0.116663
vt 0.919706 0.080294
vt 0.883337 0.050447
vt 0.841844 0.028269
vt 0.796822 0.014612
vt 0.750000 0.010000
vt 0.703178 0.014612
vt 0.658156 0.028269
vt 0.616663 0.050447
vt 0.580294 0.080295
vt 0.550447 0.116663
vt 0.528269 0.158156
vt 0.514611 0.203179
vt 0.510000 0.250000
vt 0.514612 0.296822
vt 0.528269 0.341844
vt 0.550447 0.383337
vt 0.580295 0.419706
vt 0.616663 0.449553
vt 0.658156 0.471731
vt 0.703179 0.485389
vt 0.031250 0.500000
vt 0.000000 0.500000
vt 0.062500 0.500000
vt 0.093750 0.500000
vt 0.125000 0.500000
vt 0.156250 0.500000
vt 0.187500 0.500000
vt 0.218750 0.500000
vt 0.250000 0.500000
vt 0.281250 0.500000
vt 0.312500 0.500000
vt 0.343750 0.500000
vt 0.375000 0.500000
vt 0.406250 0.500000
vt 0.437500 0.500000
vt 0.468750 0.500000
vt 0.500000 0.500000
vt 0.531250 0.500000
vt 0.562500 0.500000
vt 0.593750 0.500000
vt 0.625000 0.500000
vt 0.656250 0.500000
vt 0.687500 0.500000
vt 0.718750 0.500000
vt 0.750000 0.500000
vt 0.781250 0.500000
vt 0.812500 0.500000
vt 0.843750 0.500000
vt 0.875000 0.500000
vt 0.906250 0.500000
vt 0.937500 0.500000
vt 0.968750 0.500000
vt 1.000000 0.500000
vn 0.0882 -0.8951 -0.4371
vn 0.2611 -0.8607 -0.4371
vn 0.4240 -0.7932 -0.4371
vn 0.5706 -0.6952 -0.4371
vn 0.6952 -0.5706 -0.4371
vn 0.7932 -0.4240 -0.4371
vn 0.8607 -0.2611 -0.4371
vn 0.8951 -0.0882 -0.4371
vn 0.8951 0.0882 -0.4371
vn 0.8607 0.2611 -0.4371
vn 0.7932 0.4240 -0.4371
vn 0.6952 0.5706 -0.4371
vn 0.5706 0.6952 -0.4371
vn 0.4240 0.7932 -0.4371
vn 0.2611 0.8607 -0.4371
vn 0.0882 0.8951 -0.4371
vn -0.0882 0.8951 -0.4371
vn -0.2611 0.8607 -0.4371
vn -0.4240 0.7932 -0.4371
vn -0.5706 0.6952 -0.4371
vn -0.6952 0.5706 -0.4371
vn -0.7932 0.4240 -0.4371
vn -0.8607 0.2611 -0.4371
vn -0.8951 0.0882 -0.4371
vn -0.8951 -0.0882 -0.4371
vn -0.8607 -0.2611 -0.4371
vn -0.7932 -0.4240 -0.4371
vn -0.6952 -0.5706 -0.4371
vn -0.5706 -0.6952 -0.4371
vn -0.4240 -0.7932 -0.4371
vn 0.0000 0.0000 -1.0000
vn -0.2611 -0.8607 -0.4371
vn -0.0882 -0.8951 -0.4371
vn 0.0000 0.0000 1.0000
vn -0.0980 -0.9952 0.0000
vn -0.2903 -0.9569 0.0000
vn -0.4714 -0.8819 0.0000
vn -0.6344 -0.7730 0.0000
vn -0.7730 -0.6344 0.0000
vn -0.8819 -0.4714 0.0000
vn -0.9569 -0.2903 0.0000
vn -0.9952 -0.0980 0.0000
vn -0.9952 0.0980 0.0000
vn -0.9569 0.2903 0.0000
vn -0.8819 0.4714 0.0000
vn -0.7730 0.6344 0.0000
vn -0.6344 0.7730 0.0000
vn -0.4714 0.8819 -0.0000
vn -0.2903 0.9569 0.0000
vn -0.0980 0.9952 -0.0000
vn 0.0980 0.9952 0.0000
vn 0.2903 0.9569 0.0000
vn 0.4714 0.8819 0.0000
vn 0.6344 0.7730 0.0000
vn 0.7730 0.6344 -0.0000
vn 0.8819 0.4714 0.0000
vn 0.9569 0.2903 -0.0000
vn 0.9952 0.0980 0.0000
vn 0.9952 -0.0980 0.0000
vn 0.9569 -0.2903 0.0000
vn 0.8819 -0.4714 0.0000
vn 0.7730 -0.6344 0.0000
vn 0.6344 -0.7730 0.0000
vn 0.4714 -0.8819 0.0000
vn 0.2903 -0.9569 0.0000
vn 0.0980 -0.9952 0.0000
usemtl None
s off
f 157/175/39 94/176/39 96/177/39 158/178/39
f 158/178/40 96/177/40 98/179/40 159/180/40
f 159/180/41 98/179/41 100/181/41 160/182/41
f 160/182/42 100/181/42 102/183/42 161/184/42
f 161/184/43 102/183/43 104/185/43 162/186/43
f 162/186/44 104/185/44 106/187/44 163/188/44
f 163/188/45 106/187/45 108/189/45 164/190/45
f 164/190/46 108/189/46 110/191/46 165/192/46
f 165/192/47 110/191/47 112/193/47 166/194/47
f 166/194/48 112/193/48 114/195/48 167/196/48
f 167/196/49 114/195/49 116/197/49 168/198/49
f 168/198/50 116/197/50 118/199/50 169/200/50
f 169/200/51 118/199/51 120/201/51 170/202/51
f 170/202/52 120/201/52 122/203/52 171/204/52
f 171/204/53 122/203/53 124/205/53 172/206/53
f 172/206/54 124/205/54 126/207/54 173/208/54
f 173/208/55 126/207/55 128/209/55 174/210/55
f 174/210/56 128/209/56 130/211/56 175/212/56
f 175/212/57 130/211/57 132/213/57 176/214/57
f 176/214/58 132/213/58 134/215/58 177/216/58
f 177/216/59 134/215/59 136/217/59 178/218/59
f 178/218/60 136/217/60 138/219/60 179/220/60
f 179/220/61 138/219/61 140/221/61 180/222/61
f 180/222/62 140/221/62 142/223/62 181/224/62
f 181/224/63 142/223/63 144/225/63 182/226/63
f 182/226/64 144/225/64 146/227/64 183/228/64
f 183/228/65 146/227/65 148/229/65 184/230/65
f 184/230/66 148/229/66 150/231/66 185/232/66
f 185/232/67 150/231/67 152/233/67 186/234/67
f 186/234/68 152/233/68 154/235/68 187/236/68
f 96/237/69 94/238/69 156/239/69 154/240/69 152/241/69 150/242/69 148/243/69 146/244/69 144/245/69 142/246/69 140/247/69 138/248/69 136/249/69 134/250/69 132/251/69 130/252/69 128/253/69 126/254/69 124/255/69 122/256/69 120/257/69 118/258/69 116/259/69 114/260/69 112/261/69 110/262/69 108/263/69 106/264/69 104/265/69 102/266/69 100/267/69 98/268/69
f 187/236/70 154/235/70 156/269/70 188/270/70
f 188/270/71 156/269/71 94/271/71 157/272/71
f 93/273/72 95/274/72 97/275/72 99/276/72 101/277/72 103/278/72 105/279/72 107/280/72 109/281/72 111/282/72 113/283/72 115/284/72 117/285/72 119/286/72 121/287/72 123/288/72 125/289/72 127/290/72 129/291/72 131/292/72 133/293/72 135/294/72 137/295/72 139/296/72 141/297/72 143/298/72 145/299/72 147/300/72 149/301/72 151/302/72 153/303/72 155/304/72
f 155/305/73 188/270/73 157/272/73 93/306/73
f 153/307/74 187/236/74 188/270/74 155/305/74
f 151/308/75 186/234/75 187/236/75 153/307/75
f 149/309/76 185/232/76 186/234/76 151/308/76
f 147/310/77 184/230/77 185/232/77 149/309/77
f 145/311/78 183/228/78 184/230/78 147/310/78
f 143/312/79 182/226/79 183/228/79 145/311/79
f 141/313/80 181/224/80 182/226/80 143/312/80
f 139/314/81 180/222/81 181/224/81 141/313/81
f 137/315/82 179/220/82 180/222/82 139/314/82
f 135/316/83 178/218/83 179/220/83 137/315/83
f 133/317/84 177/216/84 178/218/84 135/316/84
f 131/318/85 176/214/85 177/216/85 133/317/85
f 129/319/86 175/212/86 176/214/86 131/318/86
f 127/320/87 174/210/87 175/212/87 129/319/87
f 125/321/88 173/208/88 174/210/88 127/320/88
f 123/322/89 172/206/89 173/208/89 125/321/89
f 121/323/90 171/204/90 172/206/90 123/322/90
f 119/324/91 170/202/91 171/204/91 121/323/91
f 117/325/92 169/200/92 170/202/92 119/324/92
f 115/326/93 168/198/93 169/200/93 117/325/93
f 113/327/94 167/196/94 168/198/94 115/326/94
f 111/328/95 166/194/95 167/196/95 113/327/95
f 109/329/96 165/192/96 166/194/96 111/328/96
f 107/330/97 164/190/97 165/192/97 109/329/97
f 105/331/98 163/188/98 164/190/98 107/330/98
f 103/332/99 162/186/99 163/188/99 105/331/99
f 101/333/100 161/184/100 162/186/100 103/332/100
f 99/334/101 160/182/101 161/184/101 101/333/101
f 97/335/102 159/180/102 160/182/102 99/334/102
f 95/336/103 158/178/103 159/180/103 97/335/103
f 93/337/104 157/175/104 158/178/104 95/336/104

Binary file not shown.

View File

@@ -1,6 +1,7 @@
import * as THREE from 'three'; import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'; import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader';
import * as Stats from 'stats.js'; import * as Stats from 'stats.js';
const canvas = document.getElementById('threejs-preview')! as HTMLCanvasElement; const canvas = document.getElementById('threejs-preview')! as HTMLCanvasElement;
@@ -10,25 +11,16 @@ const scene = new THREE.Scene();
scene.background = new THREE.Color(0x161618); scene.background = new THREE.Color(0x161618);
scene.fog = new THREE.FogExp2(0x161618, 0.002); scene.fog = new THREE.FogExp2(0x161618, 0.002);
// model loading
const objLoader = new OBJLoader(THREE.DefaultLoadingManager);
objLoader.load('assets/movinghead.obj', (objs: THREE.Group) => {
console.log('LOADER AYAYA');
objs.traverse((obj) => {
console.log(`traverse ${obj}`);
});
});
// camera controls
const camera = new THREE.PerspectiveCamera(75); const camera = new THREE.PerspectiveCamera(75);
camera.position.set(400, 200, 0); camera.position.set(400, 200, 0);
// controls
const controls = new OrbitControls(camera, renderer.domElement); const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; controls.enableDamping = true;
controls.dampingFactor = 0.05; controls.dampingFactor = 0.05;
controls.screenSpacePanning = false; controls.screenSpacePanning = false;
// controls.maxPolarAngle = Math.PI / 2;
// dbg // dbg
const cylinder = new THREE.CylinderGeometry(0, 10, 30, 4, 1); const cylinder = new THREE.CylinderGeometry(0, 10, 30, 4, 1);

View File

@@ -1,10 +0,0 @@
# Blender MTL File: 'None'
# Material Count: 1
newmtl None
Ns 500
Ka 0.8 0.8 0.8
Kd 0.8 0.8 0.8
Ks 0.8 0.8 0.8
d 1
illum 2

View File

@@ -1,5 +1,4 @@
.vscode .vscode
.mypy_cache .mypy_cache
venv venv
build build
__pycache__

View File

@@ -5,25 +5,24 @@ import colorsys
import sys import sys
channels = [ channels = [
255, # dimmer 192, # pan
0, # R 0, # tilt
0, # G 134, # dimmer
0, # B 255, # R
0, # W 0x88, # G
0, # A 0, # B
0, # UV 0, # W
0, # Strobe 1, # movement speed
0, # function 0, # RST
0, # function speed
] ]
start_addr = 1 start_addr = 10
with serial.Serial("/dev/ttyUSB0", 500000) as ser: with serial.Serial("/dev/ttyUSB0", 500000) as ser:
payload = bytearray(512) payload = bytearray(512)
FPS = 50 FPS = 30
if len(sys.argv) > 1: if len(sys.argv) > 1:
FPS = int(sys.argv[1]) FPS = int(sys.argv[1])
@@ -47,9 +46,9 @@ with serial.Serial("/dev/ttyUSB0", 500000) as ser:
r, g, b = colorsys.hls_to_rgb(t, 0.5, 1) r, g, b = colorsys.hls_to_rgb(t, 0.5, 1)
channels[1] = int(255 * r) channels[3] = int(255 * r)
channels[2] = int(255 * g) channels[4] = int(255 * g)
channels[3] = int(255 * b) channels[5] = int(255 * b)
payload[(start_addr - 1) : (start_addr - 1 + len(channels))] = channels payload[(start_addr - 1) : (start_addr - 1 + len(channels))] = channels

View File

@@ -1,69 +0,0 @@
import importlib
import time
import os
import sys
import traceback
import serial
import scene
start_addr = 1
with serial.Serial("/dev/ttyUSB0", 500000) as ser:
payload = bytearray(512)
FPS = 50
if len(sys.argv) > 1:
FPS = int(sys.argv[1])
FRAME_TIME = 1 / FPS
t = 0
def sync():
# wait for sync
while True:
b = ser.readline()
if b.strip() == b"Sync.":
return
sync()
print("initial sync.")
last_edit_timestamp = os.stat(scene.__file__).st_mtime_ns
while True:
loop_start = time.time()
try:
timestamp = os.stat(scene.__file__).st_mtime_ns
if timestamp > last_edit_timestamp:
print("[importlib.reload]")
importlib.reload(scene)
last_edit_timestamp = timestamp
scene.display(t, payload)
except Exception:
traceback.print_exc()
ser.write(payload)
ser.flush()
response = ser.readline()
if response.strip() != b"Ack.":
print(f"received bad response: {response!r}")
sync()
continue
t += FRAME_TIME
t %= 1
loop_time = time.time() - loop_start
if loop_time < FRAME_TIME:
time.sleep(FRAME_TIME - loop_time)
else:
print("loop took too long!")
print(f"loop time: {1000 * loop_time:0.2f}ms busy, {1000 * (time.time() - loop_start):0.2f}ms total")
# print(ser.read_all())

View File

@@ -1,29 +0,0 @@
import colorsys
channels = [
255, # dimmer
0, # R
0, # G
0, # B
0, # W
0, # A
0, # UV
0, # Strobe
0, # function
0, # function speed
]
start_addr = 1
def display(t, payload):
r, g, b = colorsys.hls_to_rgb(t, 0.5, 1)
#channels[1] = int(255 * r)
#channels[2] = int(255 * g)
#channels[3] = int(255 * b)
channels[4] = 0
channels[5] = 0
channels[6] = 255
channels[7] = 0
payload[(start_addr - 1) : (start_addr - 1 + len(channels))] = channels

View File

@@ -1,153 +0,0 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

View File

@@ -1 +0,0 @@
../frontend/build

View File

@@ -1,239 +0,0 @@
import asyncio
from typing import *
from queue import Queue
from threading import Thread, Lock
import time
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from pydantic import BaseModel, Field, ValidationError
import serial
app = FastAPI()
class Slider:
def __init__(self):
self.value = 0
self.owner: Optional[WebSocket] = None
self.release_timer: Optional[asyncio.Task] = None
self.release_event = asyncio.Event()
def cancel_release_timer(self):
if self.release_timer is not None:
self.release_timer.cancel()
self.release_timer = None
def reset_release_timer(self):
self.cancel_release_timer()
self.release_timer = asyncio.create_task(self._release_timer())
async def _release_timer(self):
await asyncio.sleep(1)
self.release_event.set()
dmx_state = [Slider() for _ in range(8)]
class GrabAction(BaseModel):
action_type: Literal["grab"]
slider: int
class ReleaseAction(BaseModel):
action_type: Literal["release"]
slider: int
class MoveAction(BaseModel):
action_type: Literal["move"]
slider: int
new_value: int
class ClientAction(BaseModel):
action: Union[GrabAction, ReleaseAction, MoveAction] = Field(
..., discriminator="action_type"
)
class SocketManager:
def __init__(self):
self.sockets = set()
async def on_connect(self, ws: WebSocket):
self.sockets.add(ws)
await self.push_state(ws)
def on_disconnect(self, ws: WebSocket):
self.sockets.remove(ws)
for slider in dmx_state:
if slider.owner == ws:
slider.owner = None
async def on_action(
self, ws: WebSocket, action: Union[GrabAction, ReleaseAction, MoveAction]
):
slider = dmx_state[action.slider]
if action.action_type == "grab":
print(f"grab {action.slider}")
if slider.owner is None:
slider.owner = ws
slider.reset_release_timer()
elif action.action_type == "release":
print(f"release {action.slider}")
if slider.owner == ws:
slider.owner = None
slider.cancel_release_timer()
elif action.action_type == "move":
print(f"move {action.slider} -> {action.new_value}")
if slider.owner == ws:
slider.value = action.new_value
slider.reset_release_timer()
await self.push_all()
async def push_state(self, ws: WebSocket):
response = []
for slider in dmx_state:
value = slider.value
if slider.owner == ws:
status = "owned"
elif slider.owner is not None:
status = "locked"
else:
status = "open"
response.append({"value": value, "status": status})
await ws.send_json(response)
async def push_all(self):
await asyncio.gather(*[self.push_state(ws) for ws in self.sockets])
async def watch_auto_release(self):
async def _watch(slider):
while True:
await slider.release_event.wait()
print("resetteroni")
slider.release_event.clear()
slider.owner = slider.release_timer = None
await self.push_all()
await asyncio.gather(*[_watch(slider) for slider in dmx_state])
socket_manager = SocketManager()
@app.websocket("/ws")
async def ws_handler(ws: WebSocket):
await ws.accept()
await socket_manager.on_connect(ws)
try:
while True:
data = await ws.receive_json()
try:
action = ClientAction.parse_obj(data)
await socket_manager.on_action(ws, action.action)
except ValidationError as e:
print(e)
except WebSocketDisconnect as e:
pass
finally:
socket_manager.on_disconnect(ws)
app.mount("/", StaticFiles(directory="frontend", html=True))
dmx_data_lock = Lock()
dmx_data = [0 for _ in range(len(dmx_state))]
async def dmx_watcher():
while True:
with dmx_data_lock:
for (i, slider) in enumerate(dmx_state):
dmx_data[i] = slider.value
await asyncio.sleep(1/50)
class DmxWriter(Thread):
def __init__(self):
super().__init__()
self.running = True
def run(self):
FPS = 50
FRAME_TIME = 1 / FPS
with serial.Serial("/dev/ttyUSB0", 500_000) as ser:
payload = bytearray(512)
def sync():
# wait for sync
while True:
b = ser.readline()
if b.strip() == b"Sync.":
return
sync()
print("initial sync.")
while self.running:
loop_start = time.time()
with dmx_data_lock:
for (i, value) in enumerate(dmx_data):
payload[i] = value
ser.write(payload)
ser.flush()
response = ser.readline()
if response.strip() != b"Ack.":
print(f"received bad response: {response!r}")
sync()
continue
loop_time = time.time() - loop_start
if loop_time < FRAME_TIME:
time.sleep(FRAME_TIME - loop_time)
else:
print("loop took too long!")
print(f"loop time: {1000 * loop_time:0.2f}ms busy, {1000 * (time.time() - loop_start):0.2f}ms total")
def stop(self):
self.running = False
dmx_writer = DmxWriter()
@app.on_event("startup")
async def on_startup():
asyncio.create_task(socket_manager.watch_auto_release())
asyncio.create_task(dmx_watcher())
dmx_writer.start()
@app.on_event("shutdown")
async def on_shutdown():
print("shutdown")
dmx_writer.stop()
dmx_writer.join()

View File

@@ -1,11 +0,0 @@
anyio==3.5.0
asgiref==3.5.0
click==8.0.3
fastapi==0.73.0
h11==0.13.0
idna==3.3
pydantic==1.9.0
sniffio==1.2.0
starlette==0.17.1
typing-extensions==4.1.1
uvicorn==0.17.5

View File

@@ -1,23 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@@ -1,46 +0,0 @@
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.\
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).

File diff suppressed because it is too large Load Diff

View File

@@ -1,48 +0,0 @@
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"@emotion/react": "^11.7.1",
"@emotion/styled": "^11.6.0",
"@fontsource/roboto": "^4.5.3",
"@mui/icons-material": "^5.4.1",
"@mui/material": "^5.4.1",
"@testing-library/jest-dom": "^5.16.2",
"@testing-library/react": "^12.1.2",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.4.0",
"@types/node": "^16.11.24",
"@types/react": "^17.0.39",
"@types/react-dom": "^17.0.11",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "5.0.0",
"typescript": "^4.5.5",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

View File

@@ -1,22 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@@ -1,107 +0,0 @@
import React, { useState, useMemo, createContext, useContext } from "react";
import { createTheme, ThemeProvider } from "@mui/material/styles";
import useMediaQuery from "@mui/material/useMediaQuery";
import blueGrey from "@mui/material/colors/blueGrey"
import teal from "@mui/material/colors/teal";
import AppBar from "@mui/material/AppBar";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Brightness4Icon from "@mui/icons-material/Brightness4";
import Container from "@mui/material/Container";
import CssBaseline from "@mui/material/CssBaseline";
import IconButton from "@mui/material/IconButton";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import Toolbar from "@mui/material/Toolbar";
import Typography from "@mui/material/Typography";
import Sliders from "./Sliders";
// Light / Dark mode
type ThemeMode = 'system' | 'light' | 'dark';
const ThemeModeContext = createContext((_: ThemeMode) => { })
const App: React.FC = () => {
const systemDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
const [themeMode, setThemeMode] = useState<ThemeMode>('system');
const theme = useMemo(
() => createTheme({
palette: {
mode: themeMode === 'system' ? (systemDarkMode ? 'dark' : 'light') : themeMode,
primary: blueGrey,
secondary: teal,
}
}),
[themeMode, systemDarkMode],
);
return <ThemeModeContext.Provider value={setThemeMode}>
<ThemeProvider theme={theme}>
<CssBaseline />
<Layout />
</ThemeProvider>
</ThemeModeContext.Provider>
}
// Layout
const Layout: React.FC = () => {
return <>
<TopBar />
<main>
<Box sx={{
pt: 8,
pb: 6,
}}>
<Container>
<Sliders />
</Container>
</Box>
</main>
</>
}
// Top Bar
const TopBar: React.FC = () => {
const setThemeMode = useContext(ThemeModeContext);
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
const open = Boolean(anchorEl);
return <AppBar position="relative">
<Toolbar>
<Typography variant="h6" sx={{ flexGrow: 1 }}>
DMX Controllinator
</Typography>
<IconButton
onClick={(event: React.MouseEvent<HTMLButtonElement>) => { setAnchorEl(event.currentTarget) }}
color="inherit"
>
<Brightness4Icon />
</IconButton>
<Menu
open={open}
anchorEl={anchorEl}
onClose={() => { setAnchorEl(null) }}
>
<MenuItem onClick={() => { setAnchorEl(null); setThemeMode('light'); }}>Light</MenuItem>
<MenuItem onClick={() => { setAnchorEl(null); setThemeMode('system'); }}>System</MenuItem>
<MenuItem onClick={() => { setAnchorEl(null); setThemeMode('dark'); }}>Dark</MenuItem>
</Menu>
<Button variant="contained" href="docs">
API
</Button>
</Toolbar>
</AppBar >
}
// Content
export default App;

View File

@@ -1,120 +0,0 @@
import MuiSlider from "@mui/material/Slider";
import React from "react";
import { useState, useEffect, useRef } from "react";
type StateItem = {
value: number,
status: "open" | "owned" | "locked",
};
type State = Array<StateItem>;
type ClientAction = {
action_type: "grab" | "release",
slider: number,
} | {
action_type: "move",
slider: number,
new_value: number,
}
const Sliders: React.FC = () => {
const ws = useRef<WebSocket>();
const reconnectInterval = useRef<number>();
const [state, setState] = useState<State>();
const connect = () => {
// https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState
if (ws.current !== undefined && ws.current.readyState !== 3) return;
const wsURL = new URL("ws", window.location.href);
wsURL.protocol = wsURL.protocol.replace("http", "ws");
ws.current = new WebSocket(wsURL.href);
ws.current.onmessage = (ev) => {
setState(JSON.parse(ev.data));
}
}
useEffect(() => {
connect();
reconnectInterval.current = window.setInterval(connect, 1000);
return () => {
if (reconnectInterval.current !== undefined) window.clearInterval(reconnectInterval.current);
if (ws.current !== undefined) ws.current.close();
};
}, []);
const cb = (action: ClientAction) => {
if (ws.current !== undefined && ws.current.readyState !== 3) {
ws.current.send(JSON.stringify({
action: action,
}));
}
}
return <>
{state?.map((item, index) => <Slider key={index} item={item} index={index} cb={cb} />)}
</>
}
const styleOverride = {
"& .MuiSlider-track": { transition: "none" },
"& .MuiSlider-thumb": { transition: "none" },
};
const Slider: React.FC<{
item: StateItem,
index: number,
cb: (action: ClientAction) => void,
}> = ({ item, index, cb }) => {
const disabled = item.status === "locked";
const [value, setValue] = useState(item.value);
useEffect(() => {
if (item.status !== "owned") setValue(item.value);
}, [item]);
const onChange = (n: number) => {
setValue(n);
cb({
action_type: "move",
slider: index,
new_value: n,
});
};
const onGrab = () => {
cb({
action_type: "grab",
slider: index,
});
};
const onRelease = () => {
cb({
action_type: "release",
slider: index,
});
};
return <MuiSlider
min={0}
max={255}
sx={disabled ? styleOverride : {}}
disabled={disabled}
value={value}
onChange={(_, n) => { onChange(n as number) }}
onMouseDown={onGrab}
onTouchStart={onGrab}
onMouseUp={onRelease}
onTouchEnd={onRelease}
/>
};
export default Sliders;

View File

@@ -1,15 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);

View File

@@ -1 +0,0 @@
/// <reference types="react-scripts" />

View File

@@ -1,26 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}

View File

@@ -1,3 +0,0 @@
venv
.vscode
.ipynb_checkpoints

File diff suppressed because one or more lines are too long

24
webserial/.gitignore vendored
View File

@@ -1,24 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -1,3 +0,0 @@
{
"recommendations": ["svelte.svelte-vscode"]
}

View File

@@ -1,47 +0,0 @@
# Svelte + TS + Vite
This template should help get you started developing with Svelte and TypeScript in Vite.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode).
## Need an official Svelte framework?
Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more.
## Technical considerations
**Why use this over SvelteKit?**
- It brings its own routing solution which might not be preferable for some users.
- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app.
This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project.
Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate.
**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?**
Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information.
**Why include `.vscode/extensions.json`?**
Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project.
**Why enable `allowJs` in the TS template?**
While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant.
**Why is HMR not preserving my local component state?**
HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr).
If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR.
```ts
// store.ts
// An extremely simple external store
import { writable } from 'svelte/store'
export default writable(0)
```

View File

@@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Light Maymays</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +0,0 @@
{
"name": "webserial",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^2.0.0",
"@tsconfig/svelte": "^3.0.0",
"@types/w3c-web-serial": "^1.0.3",
"bulma": "^0.9.4",
"monaco-editor": "^0.34.1",
"sass": "^1.57.1",
"svelte": "^3.54.0",
"svelte-check": "^2.10.0",
"tslib": "^2.4.1",
"typescript": "^4.9.3",
"vite": "^4.0.0"
}
}

View File

@@ -1,28 +0,0 @@
<script lang="ts">
import Monaco from "./editor/Monaco.svelte";
import EvalLoop from "./eval/EvalLoop.svelte";
import SerialManager from "./serial/SerialManager.svelte";
</script>
<main>
<Monaco />
<SerialManager />
<EvalLoop />
</main>
<style lang="scss">
:global(body) {
margin: 0;
height: 100vh;
}
:global(#app) {
height: 100%;
}
main {
height: 100%;
display: flex;
flex-direction: column;
}
</style>

View File

@@ -1,9 +0,0 @@
/* Import only what you need from Bulma */
@import "bulma/sass/utilities/_all";
@import "bulma/sass/base/_all";
@import "bulma/sass/elements/_all";
@import "bulma/sass/form/_all";
@import "bulma/sass/components/_all";
@import "bulma/sass/grid/_all";
@import "bulma/sass/helpers/_all";
@import "bulma/sass/layout/_all";

View File

@@ -1,7 +0,0 @@
<script lang="ts">
import { dmxData } from "./store";
$: dbg = new Array(10).fill(0).map((_, i) => `${i}: ${$dmxData[i].toString().padStart(3, " ")}`).join(" ");
</script>
<pre>dbg: "{dbg}"</pre>

View File

@@ -1,38 +0,0 @@
<script lang="ts">
import { dmxData } from "../store";
export let address: number;
let dimmer = 255,
r = 0,
g = 0,
b = 0,
w = 0,
a = 0,
uv = 0,
strobe = 0;
$: {
$dmxData[address - 1 + 0] = dimmer;
$dmxData[address - 1 + 1] = r;
$dmxData[address - 1 + 2] = g;
$dmxData[address - 1 + 3] = b;
$dmxData[address - 1 + 4] = w;
$dmxData[address - 1 + 5] = a;
$dmxData[address - 1 + 6] = uv;
$dmxData[address - 1 + 7] = strobe;
$dmxData = $dmxData;
}
</script>
<div>
<h3>Par @ {address}</h3>
<input bind:value={dimmer} type="range" min="0" max="255" />
<input bind:value={r} type="range" min="0" max="255" />
<input bind:value={g} type="range" min="0" max="255" />
<input bind:value={b} type="range" min="0" max="255" />
<input bind:value={w} type="range" min="0" max="255" />
<input bind:value={a} type="range" min="0" max="255" />
<input bind:value={uv} type="range" min="0" max="255" />
</div>

View File

@@ -1,3 +0,0 @@
import { writable } from "svelte/store";
export const dmxData = writable(new Uint8Array(512));

View File

@@ -1,75 +0,0 @@
<script lang="ts">
import * as monaco from "monaco-editor";
import { onMount } from "svelte";
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";
import defaultCode from "./defaultCode.ts?raw";
import defaultEnv from "./defaultEnv.d.ts?raw";
import { code } from "./code";
let divEl: HTMLDivElement = null;
let editor: monaco.editor.IStandaloneCodeEditor;
onMount(async () => {
// @ts-ignore
self.MonacoEnvironment = {
getWorker: function (_moduleId: any, label: string) {
if (label === "typescript" || label === "javascript") {
return new tsWorker();
}
return new editorWorker();
},
};
editor = monaco.editor.create(divEl, {
value: $code,
language: "typescript",
automaticLayout: true,
});
let lib =
monaco.languages.typescript.typescriptDefaults.addExtraLib(
defaultEnv
);
editor.onKeyUp((_e) => ($code = editor.getValue()));
return () => {
lib.dispose();
editor.dispose();
};
});
function resetCode() {
$code = defaultCode;
editor.setValue($code);
}
</script>
<nav class="navbar">
<div class="navbar-brand">
<div class="navbar-item"><strong>DMX Memes</strong></div>
</div>
<div class="navbar-end">
<div class="navbar-item">
<div class="buttons">
<button class="button is-danger" on:click={resetCode}>Reset to default</button>
<button class="button is-danger" on:click={() => localStorage.clear()}>Clear local storage</button>
</div>
</div>
</div>
</nav>
<div class="parent">
<div class="editor" bind:this={divEl} />
</div>
<style lang="scss">
.parent {
flex-grow: 1;
}
.editor {
height: 100%;
resize: both;
}
</style>

View File

@@ -1,9 +0,0 @@
import { writable } from "svelte/store";
import defaultCode from "./defaultCode.ts?raw";
const stored = localStorage.getItem("dmxCode") || defaultCode;
/** the TypeScript code as seen in the editor */
export const code = writable(stored);
code.subscribe((value) => localStorage.setItem("dmxCode", value))

View File

@@ -1,166 +0,0 @@
// Connect the interface with the button below and modify the code here.
// The output should update in real time as you type :)
// Fixtures library. TODO move this into a separate file
class Fixture {
startAddress: number
constructor(startAddress: number) {
this.startAddress = startAddress;
}
/**
* Write to a DMX channel
* @param address Address offset (1 = start address)
* @param value between 0 and 255
*/
setChannel(address: number, value: number) {
ctx.set(this.startAddress + address - 1, value);
}
}
interface GenericRGBW {
/**
* Set the brightness
*
* @param value between 0 and 1
* @param strobe enable strobe (turn off if unsupported)
*/
setBrightness(value: number, strobe?: boolean): void
setRGBW(rgb: [number, number, number], w?: number): void
}
class Par extends Fixture implements GenericRGBW {
setBrightness(value: number, strobe?: boolean) {
if (strobe || false) {
this.setChannel(1, 255);
this.setChannel(8, value * 255);
} else {
this.setChannel(1, value * 255);
this.setChannel(8, 0);
}
}
setRGBW(rgb: [number, number, number], w?: number) {
let [r, g, b] = rgb;
this.setChannel(2, r);
this.setChannel(3, g);
this.setChannel(4, b);
this.setChannel(5, w || 0);
}
setAUV(a: number, uv: number) {
this.setChannel(6, a);
this.setChannel(7, uv);
}
}
class MovingHead extends Fixture implements GenericRGBW {
setBrightness(value: number, strobe?: boolean) {
let val = (strobe || false)
? 135 + (239 - 135) * value
: 8 + (134 - 8) * value;
this.setChannel(6, val);
}
setRGBW(rgb: [number, number, number], w?: number) {
let [r, g, b] = rgb;
this.setChannel(7, r);
this.setChannel(8, g);
this.setChannel(9, b);
this.setChannel(10, w || 0);
}
/**
* Rotate the moving head. Pan and tilt are in radians.
* @param pan between -1.5pi and 1.5pi
* @param tilt between -0.5pi and 0.5pi
* @param speed between 0 (fast) and 255 (slow)
*/
setPanTilt(pan: number, tilt: number, speed?: number) {
let panRough = (pan + 3 * Math.PI / 2) / (Math.PI * 3) * 256
panRough = Math.max(0, Math.min(255, panRough))
let tiltRough = (tilt + Math.PI / 2) / (Math.PI) * 256
tiltRough = Math.max(0, Math.min(255, tiltRough))
// TODO
let panFine = 0
let tiltFine = 0
this.setChannel(1, panRough);
this.setChannel(2, panFine);
this.setChannel(3, tiltRough);
this.setChannel(4, tiltFine);
this.setChannel(5, speed || 0);
}
}
// class Flower extends Fixture implements GenericRGBW {
//
// setBrightness(value: number, strobe?: boolean): void {
// // dimmer seems unsupported :(
// this.setChannel(7, (strobe || false) ? 255 * value : 0);
// }
//
// setRGBW(rgb: [number, number, number], w?: number): void {
// const [r, g, b] = rgb;
// this.setChannel(1, r);
// this.setChannel(2, g);
// this.setChannel(3, b);
// this.setChannel(4, w || 0);
// }
//
// setAP(a: number, p: number) {
// this.setChannel(5, a);
// this.setChannel(6, p);
// }
//
// /**
// * Set the rotation speed
// * @param speed Between -1 (clockwise) and 1 (counterclockwise)
// */
// setRotation(direction: number) {
// const val = (direction < 0)
// ? /* clockwise */ lib.remap(direction, [0, -1], [0, 128], true)
// : /* counterclockwise */ lib.remap(direction, [0, 1], [129, 255], true);
//
// this.setChannel(8, val);
// }
//
// setMacro(pattern: number, speed: number) {
// this.setChannel(9, pattern);
// this.setChannel(10, speed);
// }
//
// }
// ******************
// * CODE GOES HERE *
// ******************
let color = lib.hsl2rgb(360 * (t / 7), 100, 50);
const pars = [
new Par(1),
new Par(41),
];
const heads = [
new MovingHead(120),
new MovingHead(140),
new MovingHead(160),
new MovingHead(100),
];
for (let par of pars) {
par.setBrightness(1);
par.setRGBW(color);
}
for (let head of heads) {
head.setBrightness(1);
head.setRGBW(color);
}

View File

@@ -1,41 +0,0 @@
type Context = {
/**
* Set DMX address `address` to `value`
* @param address between 1 and 511
* @param value between 0 and 255
*/
set(address: number, value: number): void,
};
/** The global Context object */
declare const ctx: Context;
/** The current time (in seconds, e.g. 1.25 after 1250 milliseconds) */
declare const t: number;
type Lib = {
/**
* Converts from the HSL color space to RGB
*
* Outputs [r, g, b] between 0 and 255 each
*
* @param h in degrees, i.e. 0 to 360
* @param s between 0 and 100
* @param l between 0 and 100
*/
hsl2rgb(h: number, s: number, l: number): [number, number, number],
clamp(value: number, boundaries: [number, number]),
/**
* Map a value from one range to the other
* @param value to be remapped. Does not need to be in the source range.
* @param from [lower, upper] boundaries of source range.
* @param to [lower, upper] boundaries of target range.
* @param clamp clamp the output to the target range boundaries
*/
remap(value: number, from: [number, number], to: [number, number], clamp?: boolean),
}
/** The standard library */
declare const lib: Lib;

View File

@@ -1,49 +0,0 @@
<script lang="ts">
/// <reference path="../editor/defaultEnv.d.ts" />
import { onMount } from "svelte";
import { code } from "../editor/code";
import { dmxData } from "../dmx/store";
import { lib } from "./lib";
import TS from "typescript";
import tsOptions from "./tsOptions";
$: transpiled = TS.transpile($code, tsOptions);
let startTime: number;
let payload = new Uint8Array(512);
let ctx: Context = {
set(address: number, value: number) {
let rounded = Math.round(value);
payload[address - 1] = Math.max(0, Math.min(255, rounded));
},
};
let result: string;
function dmxFrame() {
payload = new Uint8Array(512);
const t = (performance.now() - startTime) / 1000;
try {
let ret = Function("ctx", "t", "lib", transpiled).call({}, ctx, t, lib);
result = JSON.stringify(ret);
} catch (err) {
result = JSON.stringify(err);
}
$dmxData = payload;
}
const FPS = 50;
const FRAME_TIME = 1000 / FPS; // milliseconds
onMount(() => {
startTime = performance.now();
let int = setInterval(dmxFrame, FRAME_TIME);
return () => {
clearInterval(int);
};
});
</script>
<!-- <pre>{result}</pre> -->

View File

@@ -1,42 +0,0 @@
/// <reference path="../editor/defaultEnv.d.ts" />
function hsl2rgb(h, s, l): [number, number, number] {
// https://stackoverflow.com/a/44134328
l /= 100;
const a = s * Math.min(l, 1 - l) / 100;
const f = n => {
const k = (n + h / 30) % 12;
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
return Math.round(255 * color)
};
return [f(0), f(8), f(4)];
}
function clamp(value: number, boundaries: [number, number]) {
let [l, r] = boundaries;
return Math.max(l, Math.min(value, r));
}
const _clamp = clamp;
function remap(value: number, from: [number, number], to: [number, number], clamp?: boolean) {
const [a, b] = from;
const normalized = (value - a) / (b - a);
const [c, d] = to;
let remapped = c + (d - c) * normalized;
if (clamp) {
remapped = _clamp(remapped, to)
};
return remapped;
}
export const lib: Lib = {
hsl2rgb: hsl2rgb,
clamp: clamp,
remap: remap,
}

View File

@@ -1,7 +0,0 @@
import type { CompilerOptions } from "typescript"
const tsOptions: CompilerOptions = {
strict: true
}
export default tsOptions

View File

@@ -1,8 +0,0 @@
import App from './App.svelte'
import "./app.scss";
const app = new App({
target: document.getElementById('app'),
})
export default app

View File

@@ -1,133 +0,0 @@
<script lang="ts">
import { onMount } from "svelte";
import { dmxData } from "../dmx/store";
export let port: SerialPort;
export let fps: number = 50;
const FRAME_TIME = 1000 / fps; // milliseconds
let lines = [];
let linesResolve: (value: string | PromiseLike<string>) => void = null;
function readLine(): Promise<string> {
if (lines.length > 0) {
return Promise.resolve(lines.shift());
} else {
// lines is empty, register callbacks
return new Promise((resolve) => {
linesResolve = resolve;
});
}
}
const handleLine = (line: string) => {
if (linesResolve !== null) {
linesResolve(line);
linesResolve = null;
} else {
lines.push(line);
}
};
let running = true;
async function readLoop() {
console.log("runner started");
await port.open({ baudRate: 500_000 });
let reader = port.readable.getReader();
let data = "";
try {
while (running) {
const { value, done } = await reader.read();
if (done) break;
const chunk = new TextDecoder().decode(value);
data += chunk;
const lines = data.split("\r\n");
data = lines.pop();
for (let line of lines) {
handleLine(line);
}
}
} finally {
reader.releaseLock();
}
await port.close();
console.log("runner stopped");
}
let readLoopHandle: Promise<void> = null;
async function sync() {
while (running) {
const line = await readLine();
if (line === "Sync.") {
return;
}
}
}
const sleep = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms));
let lastLoopTime: number = null;
async function writeLoop() {
await sync();
while (running) {
const loopStart = performance.now();
// write out payload
let writer = port.writable.getWriter();
try {
await writer.write($dmxData);
} finally {
writer.releaseLock();
}
// read response
const response = await readLine();
if (response !== "Ack.") {
console.log(`received bad response: "${response}"`);
lastLoopTime = null;
await sync();
continue;
}
const loopTime = performance.now() - loopStart;
if (loopTime < FRAME_TIME) {
await sleep(FRAME_TIME - loopTime);
} else {
console.warn(
`loop took too long (+${(loopTime - FRAME_TIME).toFixed(
2
)} ms)`
);
}
lastLoopTime = loopTime;
}
}
let writeLoopHandle: Promise<void> = null;
onMount(() => {
console.log("mount");
readLoopHandle = readLoop();
writeLoopHandle = writeLoop();
return () => {
console.log("unmount");
running = false;
if (linesResolve !== null) {
linesResolve("");
}
};
});
</script>
<span>
{#if lastLoopTime !== null}
{lastLoopTime.toFixed(2).padStart(5)}
{:else}
syncing
{/if}
</span>

View File

@@ -1,83 +0,0 @@
<script lang="ts">
import SerialConnection from "./SerialConnection.svelte";
let port: SerialPort = null;
async function connect() {
port = await navigator.serial.requestPort();
port.addEventListener("disconnect", disconnect);
}
async function disconnect() {
port = null;
}
</script>
<section class="section bg-footer">
<div class="container">
{#if navigator.serial}
<div class="box parent">
<span class="left">Serial available &#x1f680;</span>
<span class="middle">
{#if port !== null}
<button class="button is-warning" on:click={disconnect}
>disconnect</button
>
{:else}
<button class="button is-primary" on:click={connect}
>connect</button
>
{/if}
</span>
<span class="right">
{#if port !== null}
<SerialConnection {port} />
{:else}
Not connected
{/if}
</span>
</div>
{:else}
<div class="notification is-danger text-center">
<p>
Looks like your browser does not support WebSerial &#x1f622;
</p>
<p>
Check <a
href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility"
>here</a
> for a list of compatible browsers
</p>
</div>
{/if}
</div>
</section>
<style lang="scss">
.parent {
display: flex;
flex-direction: row;
}
.left,
.middle,
.right {
flex: 1;
}
.middle {
text-align: center;
}
.right {
text-align: right;
}
.text-center {
text-align: center;
}
.bg-footer {
background-color: hsl(0, 0%, 98%);
}
</style>

View File

@@ -1,2 +0,0 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />

View File

@@ -1,7 +0,0 @@
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
export default {
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
// for more information about preprocessors
preprocess: vitePreprocess(),
}

View File

@@ -1,20 +0,0 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"resolveJsonModule": true,
/**
* Typecheck JS in `.svelte` and `.js` files by default.
* Disable checkJs if you'd like to use dynamic types in JS.
* Note that setting allowJs false does not prevent the use
* of JS in `.svelte` files.
*/
"allowJs": true,
"checkJs": true,
"isolatedModules": true
},
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -1,8 +0,0 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node"
},
"include": ["vite.config.ts"]
}

View File

@@ -1,8 +0,0 @@
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [svelte()],
base: '',
})

24
webui/.gitignore vendored
View File

@@ -1,24 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -1,3 +0,0 @@
{
"recommendations": ["svelte.svelte-vscode"]
}

View File

@@ -1,47 +0,0 @@
# Svelte + TS + Vite
This template should help get you started developing with Svelte and TypeScript in Vite.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode).
## Need an official Svelte framework?
Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more.
## Technical considerations
**Why use this over SvelteKit?**
- It brings its own routing solution which might not be preferable for some users.
- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app.
This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project.
Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate.
**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?**
Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information.
**Why include `.vscode/extensions.json`?**
Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project.
**Why enable `allowJs` in the TS template?**
While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant.
**Why is HMR not preserving my local component state?**
HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr).
If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR.
```ts
// store.ts
// An extremely simple external store
import { writable } from 'svelte/store'
export default writable(0)
```

View File

@@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Svelte + TS</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

2274
webui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,24 +0,0 @@
{
"name": "webui",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^2.0.4",
"@tsconfig/svelte": "^4.0.1",
"svelte": "^3.58.0",
"svelte-check": "^3.3.1",
"tslib": "^2.5.0",
"typescript": "^5.1.3",
"vite": "^4.3.9"
},
"dependencies": {
"monaco-editor": "^0.39.0"
}
}

View File

@@ -1,45 +0,0 @@
<script lang="ts">
import Editor from "./Editor.svelte";
import { parseFunctionInfo } from "./analysis";
const moduleCode = "export function run(x: number): number {\n return x + 69;\n}\n";
// const blob = new Blob([moduleCode], { type: "text/javascript" });
// const url = URL.createObjectURL(blob);
// const module = import(/* @vite-ignore */ url);
let text = moduleCode;
$: info = parseFunctionInfo(text, "run");
</script>
<main>
<div class="yeet">
<Editor bind:content={text} />
</div>
{#if info !== null}
<h3>Args:</h3>
<ul>
{#each info.arguments as arg}
<li>{arg.name}: {arg.type}</li>
{/each}
</ul>
<h3>Return:</h3>
{info.returnType}
{/if}
<!--
{#await module then m}
<pre>{JSON.stringify(m)}</pre>
<pre>{JSON.stringify(m.run())}</pre>
{/await}
-->
</main>
<style>
.yeet {
width: 40vw;
height: 400px;
display: flex;
}
</style>

View File

@@ -1,69 +0,0 @@
<script lang="ts" context="module">
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";
// @ts-ignore
self.MonacoEnvironment = {
getWorker: function (_moduleId: any, label: string) {
if (label === "typescript" || label === "javascript") {
return new tsWorker();
}
return new editorWorker();
},
};
</script>
<script lang="ts">
// Adapted from https://gist.github.com/KTibow/77da4597dcb22cf80be525df284e6d72
import { onMount } from "svelte";
export let content: string;
let editor;
let divEl: HTMLElement;
onMount(async () => {
const monaco = await import("monaco-editor");
editor = monaco.editor.create(divEl, {
value: content,
language: "typescript",
scrollBeyondLastLine: false,
theme: "vs-dark",
});
editor.onDidChangeModelContent(() => {
content = editor.getValue();
});
return () => {
editor.dispose();
};
});
</script>
<div class="container">
<div bind:this={divEl} class="editor" />
</div>
<svelte:window
on:resize={() => {
editor.layout({ width: 0, height: 0 });
window.requestAnimationFrame(() => {
const rect = divEl.parentElement.getBoundingClientRect();
editor.layout({ width: rect.width, height: rect.height });
});
}}
/>
<style>
.container {
flex-grow: 1;
}
.editor {
width: 100%;
height: 100%;
text-align: left;
}
</style>

View File

@@ -1,24 +0,0 @@
import ts from 'typescript';
export function parseFunctionInfo(sourceCode: string, functionName: string): { arguments: { name: string, type: string }[], returnType: string } | null {
const sourceFile = ts.createSourceFile('temp.ts', sourceCode, ts.ScriptTarget.ES2015, true);
let functionInfo: { arguments: { name: string, type: string }[], returnType: string } | null = null;
for (const node of sourceFile.statements) {
if (ts.isFunctionDeclaration(node) && node.name && node.name.text === functionName) {
const argumentTypes: { name: string, type: string }[] = node.parameters.map(parameter => {
const name = parameter.name.getText();
const type = parameter.type ? sourceCode.substring(parameter.type.pos, parameter.type.end) : 'any';
return { name, type };
});
const returnType = node.type ? sourceCode.substring(node.type.pos, node.type.end) : 'any';
functionInfo = { arguments: argumentTypes, returnType };
break;
}
}
return functionInfo;
}

View File

@@ -1,80 +0,0 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
.card {
padding: 2em;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

View File

@@ -1,8 +0,0 @@
import './app.css'
import App from './App.svelte'
const app = new App({
target: document.getElementById('app'),
})
export default app

View File

@@ -1,2 +0,0 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />

View File

@@ -1,7 +0,0 @@
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
export default {
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
// for more information about preprocessors
preprocess: vitePreprocess(),
}

View File

@@ -1,20 +0,0 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"resolveJsonModule": true,
/**
* Typecheck JS in `.svelte` and `.js` files by default.
* Disable checkJs if you'd like to use dynamic types in JS.
* Note that setting allowJs false does not prevent the use
* of JS in `.svelte` files.
*/
"allowJs": true,
"checkJs": true,
"isolatedModules": true
},
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -1,9 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler"
},
"include": ["vite.config.ts"]
}

View File

@@ -1,7 +0,0 @@
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [svelte()],
})