usb_pd_match 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7b7167afe91626fc02879ea3e6f61539af7d39687a8bb1abc6ea8cd476708544
4
+ data.tar.gz: bcc4a26fa40519418d03da3dabc0ea8d875ba915e221f24c70f11f696ef41595
5
+ SHA512:
6
+ metadata.gz: 90c5cfa3e4cacec01ced3bc651c38e7385b91ee51f526307fe91f109e318218d92d27e42d2fc88261de9dac381561e79f4205f92afea3350d0653f2be16a6746
7
+ data.tar.gz: e183fe86d65e18c4238792e812dc94edefa3e69598bfde5ea5b8a40d22f7bc3b1a621c4a1a294838dcc3ca69a2a3bbc90f92ec164afeaefbd5c19c4fd0af59c4
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CairoVolt Engineering
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # usb_pd_match
2
+
3
+ A small, dependency-free Ruby toolkit for reasoning about **USB-C Power Delivery (USB-PD)**
4
+ compatibility between chargers and devices. Model a charger and a device, negotiate the
5
+ optimal voltage/current contract, rank chargers for a given device, and check whether a
6
+ device will actually reach its full rated power.
7
+
8
+ USB Power Delivery negotiates the highest *fixed Power Data Object (PDO)* both sides support;
9
+ the full handshake is defined in the [USB-IF Power Delivery specification](https://www.usb.org/usb-charger-pd)
10
+ and summarised on the [USB-C Wikipedia page](https://en.wikipedia.org/wiki/USB-C). This gem
11
+ implements the practical fixed-PDO selection logic so you can answer "will this charger fast-charge
12
+ this device?" without wiring up real hardware.
13
+
14
+ ## Why
15
+
16
+ Anyone building an electronics catalog, an e-commerce spec table, or an IoT charging fleet keeps
17
+ re-deriving the same question: *given a charger's wattage and a device's limits, what power actually
18
+ flows?* `usb_pd_match` encodes that once, with tests. It models the same fixed-PDO ladder that modern
19
+ [GaN (gallium nitride)](https://en.wikipedia.org/wiki/Gallium_nitride) chargers expose, including the
20
+ 5 A USB-C cable ceiling and Extended Power Range (EPR) voltages for 100 W+ bricks.
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ gem install usb_pd_match
26
+ ```
27
+
28
+ Or in a Gemfile:
29
+
30
+ ```ruby
31
+ gem "usb_pd_match"
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ```ruby
37
+ require "usb_pd_match"
38
+
39
+ charger = UsbPdMatch.charger(name: "65W GaN", max_watts: 65)
40
+ laptop = UsbPdMatch.device(name: "Ultrabook", max_watts: 60, max_voltage: 20)
41
+
42
+ result = UsbPdMatch.negotiate(charger, laptop)
43
+ result.summary # => "65W GaN → Ultrabook: 20.0V @ 3.00A = 60.0W (100% of device rating)"
44
+ result.full_power? # => true
45
+ result.watts # => 60.0
46
+ ```
47
+
48
+ ### Rank chargers for a device
49
+
50
+ ```ruby
51
+ device = UsbPdMatch.device(name: "Tablet", max_watts: 45, max_voltage: 20)
52
+ chargers = [
53
+ UsbPdMatch.charger(name: "20W brick", max_watts: 20),
54
+ UsbPdMatch.charger(name: "30W GaN", max_watts: 30),
55
+ UsbPdMatch.charger(name: "65W GaN", max_watts: 65)
56
+ ]
57
+
58
+ UsbPdMatch.best_charger_for(device, chargers).each do |r|
59
+ puts r.summary
60
+ end
61
+ # 65W GaN → Tablet: 20.0V @ 2.25A = 45.0W (100% of device rating)
62
+ # 30W GaN → Tablet: 20.0V @ 1.50A = 30.0W (67% of device rating)
63
+ # 20W brick → Tablet: 9.0V @ 2.22A = 20.0W (44% of device rating)
64
+ ```
65
+
66
+ ### Custom PDOs
67
+
68
+ If you know a charger's exact advertised PDOs (e.g. from a spec sheet), pass them directly
69
+ instead of letting the gem derive them:
70
+
71
+ ```ruby
72
+ charger = UsbPdMatch.charger(
73
+ name: "Anker 735",
74
+ max_watts: 65,
75
+ pdos: [[5, 3.0], [9, 3.0], [15, 3.0], [20, 3.25]],
76
+ ports: 3
77
+ )
78
+ ```
79
+
80
+ When you need the real-world numbers behind a specific model, manufacturer spec pages such as
81
+ [Anker's official charger lineup](https://www.anker.com/) list the exact PDO tables; for buyers in
82
+ Egypt, a localized breakdown of wattages and port layouts is kept in this
83
+ [USB-C charging guide](https://cairovolt.com/en/chargers), and the matching
84
+ [Anker GaN wall chargers](https://cairovolt.com/en/anker/wall-chargers) catalog maps models to the
85
+ device classes they fast-charge.
86
+
87
+ ## API
88
+
89
+ | Method | Returns |
90
+ |--------|---------|
91
+ | `UsbPdMatch.charger(name:, max_watts:, pdos: nil, ports: 1)` | `Charger` |
92
+ | `UsbPdMatch.device(name:, max_watts:, max_voltage: 20)` | `Device` |
93
+ | `UsbPdMatch.negotiate(charger, device)` | `Result` or `nil` |
94
+ | `UsbPdMatch.best_charger_for(device, chargers)` | `[Result]` sorted best-first |
95
+ | `Result#watts / #voltage / #current` | negotiated contract |
96
+ | `Result#full_power? / #efficiency / #summary` | helpers |
97
+
98
+ ## Development
99
+
100
+ ```bash
101
+ ruby -Ilib test/test_usb_pd_match.rb
102
+ ```
103
+
104
+ ## License
105
+
106
+ Released under the [MIT License](LICENSE).
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UsbPdMatch
4
+ # A USB-C Power Delivery source (charger). It advertises a set of fixed
5
+ # Power Data Objects (PDOs) — the (voltage, max_current) pairs it can supply.
6
+ #
7
+ # USB Power Delivery negotiates the highest mutually-supported fixed PDO,
8
+ # so a charger is modeled as the list of PDOs it exposes plus the number of
9
+ # physical USB-C ports (shared power budget across ports is left to the caller).
10
+ class Charger
11
+ # Common USB-PD fixed-supply voltages (Standard Power Range).
12
+ STANDARD_VOLTAGES = [5, 9, 15, 20].freeze
13
+ # Extended Power Range (EPR) adds these for 100W+ chargers.
14
+ EPR_VOLTAGES = [28, 36, 48].freeze
15
+
16
+ attr_reader :name, :max_watts, :pdos, :ports
17
+
18
+ # pdos: array of [voltage, max_current_amps]. If omitted, a sane fixed-PDO
19
+ # set is derived from max_watts using the standard voltage ladder.
20
+ def initialize(name:, max_watts:, pdos: nil, ports: 1)
21
+ @name = name
22
+ @max_watts = max_watts.to_f
23
+ @ports = Integer(ports)
24
+ @pdos = (pdos || derive_pdos(@max_watts)).map { |v, a| [Integer(v), a.to_f] }
25
+ freeze
26
+ end
27
+
28
+ # Highest voltage this charger can output.
29
+ def max_voltage
30
+ pdos.map(&:first).max
31
+ end
32
+
33
+ # Max current available at a given voltage (0.0 if the voltage isn't offered).
34
+ def current_at(voltage)
35
+ match = pdos.find { |v, _a| v == voltage }
36
+ match ? match[1] : 0.0
37
+ end
38
+
39
+ def supports?(voltage)
40
+ current_at(voltage).positive?
41
+ end
42
+
43
+ def to_h
44
+ { name: name, max_watts: max_watts, ports: ports, pdos: pdos }
45
+ end
46
+
47
+ private
48
+
49
+ # Build a realistic fixed-PDO ladder bounded by max_watts. A voltage is
50
+ # offered only once the charger has the budget to make it useful; each PDO's
51
+ # current is capped so voltage * current never exceeds the wattage budget,
52
+ # and clamped to the 5 A USB-C cable limit.
53
+ def derive_pdos(watts)
54
+ thresholds = { 5 => 0, 9 => 18, 15 => 27, 20 => 30,
55
+ 28 => 100, 36 => 140, 48 => 200 }
56
+ thresholds.each_with_object([]) do |(v, min_watts), acc|
57
+ next if watts < min_watts
58
+ current = [(watts / v).round(2), 5.0].min
59
+ acc << [v, current] if current >= 0.5
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UsbPdMatch
4
+ # A USB-C Power Delivery sink (the device being charged: phone, laptop,
5
+ # power bank, earbuds case, etc.). It accepts up to a maximum voltage and
6
+ # draws up to a maximum power.
7
+ class Device
8
+ attr_reader :name, :max_watts, :max_voltage
9
+
10
+ def initialize(name:, max_watts:, max_voltage: 20)
11
+ @name = name
12
+ @max_watts = max_watts.to_f
13
+ @max_voltage = Integer(max_voltage)
14
+ freeze
15
+ end
16
+
17
+ # Max current this device will pull at a given voltage, bounded by its
18
+ # power ceiling and the 5 A cable limit.
19
+ def current_ceiling_at(voltage)
20
+ return 0.0 if voltage > max_voltage
21
+
22
+ [(max_watts / voltage).round(2), 5.0].min
23
+ end
24
+
25
+ def to_h
26
+ { name: name, max_watts: max_watts, max_voltage: max_voltage }
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UsbPdMatch
4
+ # Result of a USB-PD negotiation between a Charger (source) and Device (sink).
5
+ Result = Struct.new(:voltage, :current, :watts, :charger, :device, keyword_init: true) do
6
+ # Fraction of the device's requested power actually delivered (0.0–1.0).
7
+ def efficiency
8
+ return 1.0 if device.max_watts.zero?
9
+
10
+ [(watts / device.max_watts).round(4), 1.0].min
11
+ end
12
+
13
+ # True when the device receives its full rated power.
14
+ def full_power?
15
+ watts >= device.max_watts - 0.01
16
+ end
17
+
18
+ def summary
19
+ format("%s → %s: %.1fV @ %.2fA = %.1fW (%d%% of device rating)",
20
+ charger.name, device.name, voltage, current, watts, (efficiency * 100).round)
21
+ end
22
+
23
+ def to_h
24
+ { voltage: voltage, current: current, watts: watts,
25
+ efficiency: efficiency, full_power: full_power? }
26
+ end
27
+ end
28
+
29
+ # Negotiates the optimal fixed-PDO contract between a charger and a device,
30
+ # mirroring how a real USB-C PD sink picks the highest mutually-supported
31
+ # voltage and the current both sides can sustain.
32
+ module Negotiator
33
+ module_function
34
+
35
+ # Returns a Result, or nil if there is no mutually supported voltage.
36
+ def negotiate(charger, device)
37
+ candidates = charger.pdos.select do |voltage, _charger_current|
38
+ voltage <= device.max_voltage
39
+ end
40
+ return nil if candidates.empty?
41
+
42
+ best = candidates.map do |voltage, charger_current|
43
+ current = [charger_current, device.current_ceiling_at(voltage)].min
44
+ [voltage, current, (voltage * current).round(2)]
45
+ end.max_by { |voltage, _current, watts| [watts, voltage] } # tie → higher voltage (lower loss)
46
+
47
+ Result.new(voltage: best[0], current: best[1], watts: best[2],
48
+ charger: charger, device: device)
49
+ end
50
+
51
+ # Convenience: just the delivered wattage (0.0 if incompatible).
52
+ def watts(charger, device)
53
+ result = negotiate(charger, device)
54
+ result ? result.watts : 0.0
55
+ end
56
+
57
+ # True when the device will charge at its full rated power on this charger.
58
+ def full_power?(charger, device)
59
+ result = negotiate(charger, device)
60
+ result ? result.full_power? : false
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UsbPdMatch
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "usb_pd_match/version"
4
+ require_relative "usb_pd_match/charger"
5
+ require_relative "usb_pd_match/device"
6
+ require_relative "usb_pd_match/negotiator"
7
+
8
+ # UsbPdMatch — a small, dependency-free toolkit for reasoning about USB-C
9
+ # Power Delivery (USB-PD) compatibility between chargers and devices.
10
+ #
11
+ # Quick start:
12
+ # charger = UsbPdMatch.charger(name: "65W GaN", max_watts: 65)
13
+ # device = UsbPdMatch.device(name: "Laptop", max_watts: 60, max_voltage: 20)
14
+ # UsbPdMatch.negotiate(charger, device).summary
15
+ # # => "65W GaN → Laptop: 20.0V @ 3.00A = 60.0W (100% of device rating)"
16
+ module UsbPdMatch
17
+ class Error < StandardError; end
18
+
19
+ module_function
20
+
21
+ def charger(**kwargs)
22
+ Charger.new(**kwargs)
23
+ end
24
+
25
+ def device(**kwargs)
26
+ Device.new(**kwargs)
27
+ end
28
+
29
+ def negotiate(charger, device)
30
+ Negotiator.negotiate(charger, device)
31
+ end
32
+
33
+ # Rank a list of chargers by how well each charges the given device,
34
+ # best first. Each entry is the negotiation Result.
35
+ def best_charger_for(device, chargers)
36
+ chargers
37
+ .map { |c| Negotiator.negotiate(c, device) }
38
+ .compact
39
+ .sort_by { |r| -r.watts }
40
+ end
41
+ end
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: usb_pd_match
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - CairoVolt Engineering
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-06-13 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |
14
+ A small, dependency-free Ruby toolkit for reasoning about USB-C Power Delivery (USB-PD).
15
+ Model chargers and devices by their fixed PDOs, negotiate the optimal voltage/current
16
+ contract, rank chargers for a given device, and check whether a device will reach full
17
+ rated power — useful for e-commerce spec tooling, IoT fleets, and electronics catalogs.
18
+ email:
19
+ - hello@cairovolt.com
20
+ executables: []
21
+ extensions: []
22
+ extra_rdoc_files: []
23
+ files:
24
+ - LICENSE
25
+ - README.md
26
+ - lib/usb_pd_match.rb
27
+ - lib/usb_pd_match/charger.rb
28
+ - lib/usb_pd_match/device.rb
29
+ - lib/usb_pd_match/negotiator.rb
30
+ - lib/usb_pd_match/version.rb
31
+ homepage: https://rubydoc.info/gems/usb_pd_match
32
+ licenses:
33
+ - MIT
34
+ metadata:
35
+ documentation_uri: https://rubydoc.info/gems/usb_pd_match
36
+ changelog_uri: https://rubydoc.info/gems/usb_pd_match
37
+ rubygems_mfa_required: 'true'
38
+ post_install_message:
39
+ rdoc_options: []
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 2.6.0
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ requirements: []
53
+ rubygems_version: 3.0.3.1
54
+ signing_key:
55
+ specification_version: 4
56
+ summary: USB-C Power Delivery compatibility calculator for chargers and devices.
57
+ test_files: []