Public: anyone can read this; the crew can change it.

Migrated from github.com/Trevato/SO-ARM100

Grow stills at 1920×1080, cropped to the camera's region and colour-corrected (split from #584) #603

Merged opened this

Check · Review · Land done · Live — Landed; nothing deploys from this branch, so this is the end of its path.

Merged

The grow box's stills are now taken at 1920×1080 when the camera is one of the Pi's, while the stream stays at 640×360. Each still is cropped to the camera's region, and a colour-corrected copy is kept beside the raw one. `bun test` passes (1210 pass, 3 skipped, 0 fail). Nothing here has run against a real camera or a real Pi: the grab, the sudo calls and the service restart are tested only with s…

shipped merged

opened this

The grow box's stills are now taken at 1920×1080 when the camera is one of the Pi's, while the stream stays at 640×360. Each still is cropped to the camera's region, and a colour-corrected copy is kept beside the raw one. bun test passes (1210 pass, 3 skipped, 0 fail). Nothing here has run against a real camera or a real Pi: the grab, the sudo calls and the service restart are tested only with stand-ins.

What changed

  • Full-size grab on the Pi (src/camera-still.ts, POST /camera-still/<n> in src/server.ts): ustreamer holds the camera, so the arm server stops so101-camera@<n>. It then grabs one MJPEG frame at 1920×1080 with v4l2-ctl, skipping the first 15 frames while the camera settles. The service is started again however the grab went, so the stream comes back at its own 640×360. The stream drops out for about two seconds every ten minutes. Commands are argument lists built from a checked unit number and are never run through a shell. One grab runs at a time: a second request gets 409, a failed grab 502, and the simulation 501.
  • Pausing needs a sudo rule: the arm server's user must be allowed to run systemctl stop and systemctl start on the camera units, and nothing more. Software/so101/README.md gives the exact sudoers lines.
  • Crop and correction (src/grow-frame.ts): the relay crops with cropRect(look.region, width, height) using the still's real size, so a camera that can't do 1080p still gets the right crop. When the camera has gains, it also keeps a copy with applyGains, and the raw still is never corrected. With no region and no gains, the camera's JPEG is kept byte for byte. A 1080p decode takes about half a second, so it runs in a worker (src/grow-frame-worker.ts) and pages aren't held up while it does.
  • Fallback: if the Pi can't grab a still (arm offline, no unit, no sudo rule), the stream's own snapshot is used and cropped the same way.
  • Storage and routes (src/grow-stills.ts): file names now record whether a still is cropped (k) and which file is the corrected copy (c). The copy is written first and pruned with its still. /grow/stills now reports cropped and corrected for each still, and GET /grow/stills/<ms>-corrected.jpg serves the copy.
  • Card (src/ui/grow-timelapse.js): shows the corrected copy with "· corrected" after its time, and a Corrected button switches back to the raw colours. A still that is already cropped isn't cropped again when the green area is measured.
  • One new dependency: jpeg-js, pure JavaScript with no dependencies of its own, because neither Bun nor Node can decode a JPEG. The reason is in both READMEs.

Not done: the optional white photo light around each still. The grow light's switch has no photo light yet, and the README says so.

What to look at: src/grow-frame.test.ts covers the issue's test:

  • A fixture still is cropped to the right half and its corrected copy is about 2× red; the raw one keeps its own colours and the input bytes are unchanged.
  • The worker gives the same result as the direct call.
  • Raw and corrected copies are stored together and pruned together.
  • The stream is started again after a failed grab, and a second grab while one runs is refused.
  • The /camera-still route answers correctly, including on the simulation.
  • An end-to-end relay run stores a cropped still with its -corrected.jpg copy.

To try it on the Pi: add the sudoers line, draw a region and press Measure colour on /admin → Cameras → Tune, then wait one capture interval. The grow card should then show a still cropped to the region, labelled "corrected".

🤖 Generated with Claude Code

Closes #589

commented

The grow box's stills are now taken at 1920×1080 when the camera is one of the Pi's, while the stream stays at 640×360. Each still is cropped to the camera's region, and a colour-corrected copy is kept beside the raw one. bun test passes (1210 pass, 3 skipped, 0 fail). Nothing here has run against a real camera or a real Pi: the grab, the sudo calls and the service restart are tested only with stand-ins.

What changed

  • Full-size grab on the Pi (src/camera-still.ts, POST /camera-still/<n> in src/server.ts): ustreamer holds the camera, so the arm server stops so101-camera@<n>. It then grabs one MJPEG frame at 1920×1080 with v4l2-ctl, skipping the first 15 frames while the camera settles. The service is started again however the grab went, so the stream comes back at its own 640×360. The stream drops out for about two seconds every ten minutes. Commands are argument lists built from a checked unit number and are never run through a shell. One grab runs at a time: a second request gets 409, a failed grab 502, and the simulation 501.
  • Pausing needs a sudo rule: the arm server's user must be allowed to run systemctl stop and systemctl start on the camera units, and nothing more. Software/so101/README.md gives the exact sudoers lines.
  • Crop and correction (src/grow-frame.ts): the relay crops with cropRect(look.region, width, height) using the still's real size, so a camera that can't do 1080p still gets the right crop. When the camera has gains, it also keeps a copy with applyGains, and the raw still is never corrected. With no region and no gains, the camera's JPEG is kept byte for byte. A 1080p decode takes about half a second, so it runs in a worker (src/grow-frame-worker.ts) and pages aren't held up while it does.
  • Fallback: if the Pi can't grab a still (arm offline, no unit, no sudo rule), the stream's own snapshot is used and cropped the same way.
  • Storage and routes (src/grow-stills.ts): file names now record whether a still is cropped (k) and which file is the corrected copy (c). The copy is written first and pruned with its still. /grow/stills now reports cropped and corrected for each still, and GET /grow/stills/<ms>-corrected.jpg serves the copy.
  • Card (src/ui/grow-timelapse.js): shows the corrected copy with "· corrected" after its time, and a Corrected button switches back to the raw colours. A still that is already cropped isn't cropped again when the green area is measured.
  • One new dependency: jpeg-js, pure JavaScript with no dependencies of its own, because neither Bun nor Node can decode a JPEG. The reason is in both READMEs.

Not done: the optional white photo light around each still. The grow light's switch has no photo light yet, and the README says so.

What to look at: src/grow-frame.test.ts covers the issue's test:

  • A fixture still is cropped to the right half and its corrected copy is about 2× red; the raw one keeps its own colours and the input bytes are unchanged.
  • The worker gives the same result as the direct call.
  • Raw and corrected copies are stored together and pruned together.
  • The stream is started again after a failed grab, and a second grab while one runs is refused.
  • The /camera-still route answers correctly, including on the simulation.
  • An end-to-end relay run stores a cropped still with its -corrected.jpg copy.

To try it on the Pi: add the sudoers line, draw a region and press Measure colour on /admin → Cameras → Tune, then wait one capture interval. The grow card should then show a still cropped to the region, labelled "corrected".

🤖 Generated with Claude Code

Approved

Does what #584 asked: stills grabbed at 1920x1080, cropped by cropRect on the still own size, a corrected copy beside the raw one, a Corrected toggle on the card. The commands are argument lists with a checked unit, the stream is started again in a finally, one grab at a time, and the tests cover crop, gains, worker, store, route and an end-to-end relay run. The preview answers 200. The new route follows the same trust model as /camera-profile. jpeg-js is justified in both READMEs. The card keeps working without JavaScript (latest still plus link). Two things I would do next: (1) when systemctl start fails after a grab, the stream stays down until the next capture ten minutes later and only a console.error says so; retry the start once or twice and surface it on the Tune/camera status, since the live cameras must not stay dead silently. (2) In GrowStills.add, if the raw write throws after the corrected copy was written, the orphaned c-file is never listed or pruned (files() only counts copies that have a raw still); remove the copy on failure or prune orphans.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs