<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ethan Tan</title>
    <description>The latest articles on DEV Community by Ethan Tan (@recynex).</description>
    <link>https://dev.to/recynex</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4161690%2F750d6900-f1fe-4faf-9812-50c43835f99e.png</url>
      <title>DEV Community: Ethan Tan</title>
      <link>https://dev.to/recynex</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/recynex"/>
    <language>en</language>
    <item>
      <title>A minimal acceptance event for a bottle return machine</title>
      <dc:creator>Ethan Tan</dc:creator>
      <pubDate>Mon, 26 Oct 2026 17:25:26 +0000</pubDate>
      <link>https://dev.to/recynex/a-minimal-acceptance-event-for-a-bottle-return-machine-4lap</link>
      <guid>https://dev.to/recynex/a-minimal-acceptance-event-for-a-bottle-return-machine-4lap</guid>
      <description>&lt;p&gt;A return machine is only useful to the scheme operator if each insert becomes a record.&lt;/p&gt;

&lt;p&gt;The smallest record I would store:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;container id (barcode or scheme code)&lt;/li&gt;
&lt;li&gt;decision: accepted or refused&lt;/li&gt;
&lt;li&gt;reason code when refused&lt;/li&gt;
&lt;li&gt;machine id and timestamp&lt;/li&gt;
&lt;li&gt;refund id, only if a payout was committed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without the reason code, "it rejected my bottle" cannot be reconciled. Without the refund id, a retailer dispute is just two stories.&lt;/p&gt;

&lt;p&gt;I work on RECYNEX. Primer: &lt;a href="https://www.recynex.com/news/what-is-a-reverse-vending-machine/" rel="noopener noreferrer"&gt;https://www.recynex.com/news/what-is-a-reverse-vending-machine/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
    </item>
    <item>
      <title>Three events, not one, when a return machine accepts a bottle</title>
      <dc:creator>Ethan Tan</dc:creator>
      <pubDate>Mon, 26 Oct 2026 14:56:00 +0000</pubDate>
      <link>https://dev.to/recynex/three-events-not-one-when-a-return-machine-accepts-a-bottle-19pn</link>
      <guid>https://dev.to/recynex/three-events-not-one-when-a-return-machine-accepts-a-bottle-19pn</guid>
      <description>&lt;p&gt;A bottle-return point that writes a single "recycled" row will lie the first time the link drops.&lt;/p&gt;

&lt;p&gt;Split one visit into three facts the device can prove on its own:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;accept&lt;/strong&gt; — the machine matched a container (barcode or shape) and committed to taking it. Persist this on the device before the compactor or shutter moves. Key it on machine id plus a local sequence, not a server clock.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;store&lt;/strong&gt; — the container was actually retained (compacted or dropped in the bin). An accept with no store is a jam, or a person who pulled the bottle back out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;payout&lt;/strong&gt; — the deposit or reward was issued (cash, voucher, or scheme credit). This fails on its own when the scheme API is down. Queue it. Do not roll back the accept, or you end up with a bottle in the bin and no record.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Replays after reconnect should return the original ids. If accept and payout share one transaction, a timeout makes you either double-pay or delete a container you already kept.&lt;/p&gt;

&lt;p&gt;A short public note on what the machine checks per insert: &lt;a href="https://www.recynex.com/news/what-is-a-reverse-vending-machine/" rel="noopener noreferrer"&gt;how a reverse vending machine handles one container&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>iot</category>
      <category>api</category>
      <category>backend</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Queueing reverse-vending return events when the store link drops</title>
      <dc:creator>Ethan Tan</dc:creator>
      <pubDate>Mon, 26 Oct 2026 13:55:41 +0000</pubDate>
      <link>https://dev.to/recynex/queueing-reverse-vending-return-events-when-the-store-link-drops-15lg</link>
      <guid>https://dev.to/recynex/queueing-reverse-vending-return-events-when-the-store-link-drops-15lg</guid>
      <description>&lt;p&gt;A return machine in a supermarket is a small edge device with a bad network. Shoppers do not wait for your cloud. If the uplink blips, the machine still has to accept a bottle, print a receipt, and remember that event until the link is back.&lt;/p&gt;

&lt;p&gt;This note is the shape of that queue, not a product pitch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What has to survive offline
&lt;/h2&gt;

&lt;p&gt;Each accepted container is one event, not a row you update in place. A useful payload is small and idempotent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"01J9…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"device_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"store-14-rvm-2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"accepted_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-26T12:04:11Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"material"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pet"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"barcode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"9300675000000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"session_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s-8841"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scheme"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deposit"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;event_id&lt;/code&gt; is generated on the device (ULID or UUIDv7). The cloud treats a repeat of the same id as a no-op. That matters because the store Wi-Fi will deliver the same batch twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local queue
&lt;/h2&gt;

&lt;p&gt;Keep an append-only log on disk, not in memory:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Validate the container (barcode or vision result) before you enqueue. A rejected item is not an event.&lt;/li&gt;
&lt;li&gt;Append the event and fsync. The receipt printer should run only after that write succeeds. Otherwise a power cut prints a voucher the cloud will never see.&lt;/li&gt;
&lt;li&gt;A worker drains the log when the uplink is healthy. Send batches of a few dozen, in &lt;code&gt;accepted_at&lt;/code&gt; order, with the last acked &lt;code&gt;event_id&lt;/code&gt; as a cursor.&lt;/li&gt;
&lt;li&gt;The server acks ids, not "the whole request". Drop only the acked prefix. Leave the rest for the next attempt.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Clock skew is normal. Do not sort the fleet by device clock alone. Store both &lt;code&gt;accepted_at&lt;/code&gt; (device) and &lt;code&gt;received_at&lt;/code&gt; (server), and let settlement use the scheme's rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  What not to do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Do not hold the shopper UI on an HTTP call to the cloud. Recognition and the receipt are local. Sync is background.&lt;/li&gt;
&lt;li&gt;Do not key the event on barcode + timestamp in seconds. Two cans in the same second collide, and a retry looks like a new return.&lt;/li&gt;
&lt;li&gt;Do not compact or delete the on-device log until the ack is durable. A full disk should refuse new returns with a clear message, not drop history.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Vision sits beside this, not inside it
&lt;/h2&gt;

&lt;p&gt;If the machine uses a camera to tell PET from a look-alike, that score belongs on the event as evidence (&lt;code&gt;vision_label&lt;/code&gt;, &lt;code&gt;confidence&lt;/code&gt;), but the accept/reject decision should already be made before enqueue. Re-running the model in the cloud is for audits, not for changing a receipt the shopper already holds.&lt;/p&gt;

&lt;p&gt;Operators who run a fleet of these machines end up with the same three questions: is the device online, how far behind is its cursor, and which sessions never got an ack. That is a queue-depth problem more than a recycling problem.&lt;/p&gt;

&lt;p&gt;I work on this class of system at RECYNEX. A higher-level description of the return flow (not the queue) is on our site: &lt;a href="https://www.recynex.com/news/what-is-a-reverse-vending-machine/" rel="noopener noreferrer"&gt;https://www.recynex.com/news/what-is-a-reverse-vending-machine/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>iot</category>
      <category>architecture</category>
      <category>backend</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Reading a STEP file's volume and weight in the browser: what actually happens</title>
      <dc:creator>Ethan Tan</dc:creator>
      <pubDate>Tue, 13 Oct 2026 12:50:32 +0000</pubDate>
      <link>https://dev.to/recynex/reading-a-step-files-volume-and-weight-in-the-browser-what-actually-happens-46o4</link>
      <guid>https://dev.to/recynex/reading-a-step-files-volume-and-weight-in-the-browser-what-actually-happens-46o4</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Correction (21 October 2026): The earlier text conflated a possible browser/WebAssembly implementation with CADProps, overstated what STEP files contain, and used an incorrect inch-to-millimetre volume factor. CADProps uploads files for server processing; it is not a local-only browser/WASM tool. This factual correction was prepared with AI assistance.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;STEP (ISO 10303) files can carry different geometric representations. AP242 supports tessellated part geometry as well as B-Rep geometry. So "how much does this part weigh?" is a geometry question before it's a math question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Parse and identify the representation.&lt;/strong&gt; Open CASCADE compiled to WebAssembly is one possible browser implementation, not a description of CADProps. A display mesh and the underlying B-Rep are different representations. Mesh-based volume estimates depend on tessellation; valid closed B-Rep solid volume can be calculated independently of display tessellation. CADProps uses server processing for uploaded files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Integrate the volume.&lt;/strong&gt; For a valid closed, consistently oriented triangle surface bounding the intended solid, volume can be calculated by summing signed tetrahedra from the origin: &lt;code&gt;V = Σ (v1 · (v2 × v3)) / 6&lt;/code&gt;. Check watertightness, orientation and self-intersections; an arbitrary positive sum is not proof of a valid solid.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Multiply by density.&lt;/strong&gt; Steel ≈ 7.85 g/cm³, aluminium ≈ 2.70, PLA ≈ 1.24 are approximate density assumptions, not material specifications. Confirm the file's units instead of assuming millimetres. For volume in mm³, conversion to cm³ is /1000. A solid-material estimate is not the filament mass of an infilled print.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Gotchas.&lt;/strong&gt; Summing component occurrences can double-count overlapping material; it is not the same as measuring their geometric union. Open surface shells do not establish an enclosed solid volume. One inch is exactly 25.4 mm: reading inch coordinates as millimetres makes lengths 25.4 times too small and volumes 25.4³ = 16,387.064 times too small.&lt;/p&gt;

&lt;p&gt;FreeCAD's &lt;code&gt;Shape.Volume&lt;/code&gt; is a geometry property, not a substitute for checking the shape's validity, units and component scope. Disclosure: I work on CADProps. The earlier phrase "built this way" was incorrect: CADProps uploads and processes files on a server. Do not upload confidential geometry without the owner's permission.&lt;/p&gt;

</description>
      <category>engineering</category>
      <category>webdev</category>
      <category>cad</category>
      <category>3dprinting</category>
    </item>
  </channel>
</rss>
